Merge branch 'develop' into sidv/contributors

This commit is contained in:
Knut Sveidqvist 2023-05-08 09:17:49 +02:00 committed by GitHub
commit 25a7164df2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1283 additions and 350 deletions

View File

@ -165,6 +165,13 @@ class Class10 {
int id int id
size() size()
} }
namespace Namespace01 {
class Class11
class Class12 {
int id
size()
}
}
``` ```
```mermaid ```mermaid
@ -184,6 +191,13 @@ class Class10 {
int id int id
size() size()
} }
namespace Namespace01 {
class Class11
class Class12 {
int id
size()
}
}
``` ```
### State diagram [<a href="https://mermaid-js.github.io/mermaid/#/stateDiagram">docs</a> - <a href="https://mermaid.live/edit#pako:eNpdkEFvgzAMhf8K8nEqpYSNthx22Xbcqcexg0sCiZQQlDhIFeK_L8A6TfXp6fOz9ewJGssFVOAJSbwr7ByadGR1n8T6evpO0vQ1uZDSekOrXGFsPqJPO6q-2-imH8f_0TeHXm50lfelsAMjnEHFY6xpMdRAUhhRQxUlFy0GTTXU_RytYeAx-AdXZB1ULWovdoCB7OXWN1CRC-Ju-r3uz6UtchGHJqDbsPygU57iysb2reoWHpyOWBINvsqypb3vFMlw3TfWZF5xiY7keC6zkpUnZIUojwW-FAVvrvn51LLnvOXHQ84Q5nn-AVtLcwk">live editor</a>] ### State diagram [<a href="https://mermaid-js.github.io/mermaid/#/stateDiagram">docs</a> - <a href="https://mermaid.live/edit#pako:eNpdkEFvgzAMhf8K8nEqpYSNthx22Xbcqcexg0sCiZQQlDhIFeK_L8A6TfXp6fOz9ewJGssFVOAJSbwr7ByadGR1n8T6evpO0vQ1uZDSekOrXGFsPqJPO6q-2-imH8f_0TeHXm50lfelsAMjnEHFY6xpMdRAUhhRQxUlFy0GTTXU_RytYeAx-AdXZB1ULWovdoCB7OXWN1CRC-Ju-r3uz6UtchGHJqDbsPygU57iysb2reoWHpyOWBINvsqypb3vFMlw3TfWZF5xiY7keC6zkpUnZIUojwW-FAVvrvn51LLnvOXHQ84Q5nn-AVtLcwk">live editor</a>]

View File

@ -25,6 +25,7 @@
"classdef", "classdef",
"codedoc", "codedoc",
"colour", "colour",
"commitlint",
"cpettitt", "cpettitt",
"customizability", "customizability",
"cuzon", "cuzon",
@ -65,13 +66,16 @@
"knut", "knut",
"knutsveidqvist", "knutsveidqvist",
"laganeckas", "laganeckas",
"linetype",
"lintstagedrc", "lintstagedrc",
"logmsg", "logmsg",
"lucida", "lucida",
"markdownish",
"matthieu", "matthieu",
"matthieumorel", "matthieumorel",
"mdast", "mdast",
"mdbook", "mdbook",
"mermaidjs",
"mermerd", "mermerd",
"mindaugas", "mindaugas",
"mindmap", "mindmap",
@ -94,6 +98,7 @@
"rect", "rect",
"rects", "rects",
"redmine", "redmine",
"rehype",
"roledescription", "roledescription",
"sandboxed", "sandboxed",
"setupgraphviewbox", "setupgraphviewbox",
@ -101,8 +106,12 @@
"sidharth", "sidharth",
"sidharthv", "sidharthv",
"sphinxcontrib", "sphinxcontrib",
"startx",
"starty",
"statediagram", "statediagram",
"steph", "steph",
"stopx",
"stopy",
"stylis", "stylis",
"substate", "substate",
"sveidqvist", "sveidqvist",
@ -112,11 +121,13 @@
"textlength", "textlength",
"treemap", "treemap",
"ts-nocheck", "ts-nocheck",
"tsdoc",
"tuleap", "tuleap",
"tylerlong", "tylerlong",
"ugge", "ugge",
"unist", "unist",
"unocss", "unocss",
"valign",
"verdana", "verdana",
"viewports", "viewports",
"vinod", "vinod",

View File

@ -548,4 +548,18 @@ class C13["With Città foreign language"]
` `
); );
}); });
it('should add classes namespaces', function () {
imgSnapshotTest(
`
classDiagram
namespace Namespace1 {
class C1
class C2
}
C1 --> C2
class C3
class C4
`
);
});
}); });

View File

@ -123,6 +123,29 @@ context('Sequence diagram', () => {
} }
); );
}); });
it('should render a sequence diagram with par_over', () => {
imgSnapshotTest(
`
sequenceDiagram
participant Alice
participant Bob
participant John
par_over Section title
Alice ->> Bob: Message 1<br>Second line
Bob ->> John: Message 2
end
par_over Two line<br>section title
Note over Alice: Alice note
Note over Bob: Bob note<br>Second line
Note over John: John note
end
par_over Mixed section
Alice ->> Bob: Message 1
Note left of Bob: Alice/Bob Note
end
`
);
});
context('font settings', () => { context('font settings', () => {
it('should render different note fonts when configured', () => { it('should render different note fonts when configured', () => {
imgSnapshotTest( imgSnapshotTest(

View File

@ -144,6 +144,26 @@
> >
<hr /> <hr />
<pre class="mermaid">
sequenceDiagram
participant Alice
participant Bob
participant John
par_over Section title
Alice ->> Bob: Message 1<br>Second line
Bob ->> John: Message 2
end
par_over Two line<br>section title
Note over Alice: Alice note
Note over Bob: Bob note<br>Second line
Note over John: John note
end
par_over Mixed section
Alice ->> Bob: Message 1
Note left of Bob: Alice/Bob Note
end
</pre>
<script type="module"> <script type="module">
import mermaid from './mermaid.esm.mjs'; import mermaid from './mermaid.esm.mjs';
mermaid.initialize({ mermaid.initialize({

View File

@ -348,10 +348,10 @@ mermaid.parseError = function (err, hash) {
displayErrorInGui(err); displayErrorInGui(err);
}; };
const textFieldUpdated = function () { const textFieldUpdated = async function () {
const textStr = getTextFromFormField('code'); const textStr = getTextFromFormField('code');
if (mermaid.parse(textStr)) { if (await mermaid.parse(textStr)) {
reRender(textStr); reRender(textStr);
} }
}; };

View File

@ -147,8 +147,12 @@ They also serve as proof of concept, for the variety of things that can be built
- [Sphinx](https://www.sphinx-doc.org/en/master/) - [Sphinx](https://www.sphinx-doc.org/en/master/)
- [sphinxcontrib-mermaid](https://github.com/mgaitan/sphinxcontrib-mermaid) - [sphinxcontrib-mermaid](https://github.com/mgaitan/sphinxcontrib-mermaid)
- [remark.js](https://remark.js.org/) - [remark](https://remark.js.org/)
- [remark-mermaid](https://github.com/temando/remark-mermaid) - [remark-mermaidjs](https://github.com/remcohaszing/remark-mermaidjs)
- [rehype](https://github.com/rehypejs/rehype)
- [rehype-mermaidjs](https://github.com/remcohaszing/rehype-mermaidjs)
- [Gatsby](https://www.gatsbyjs.com/)
- [gatsby-remark-mermaid](https://github.com/remcohaszing/gatsby-remark-mermaid)
- [jSDoc](https://jsdoc.app/) - [jSDoc](https://jsdoc.app/)
- [jsdoc-mermaid](https://github.com/Jellyvision/jsdoc-mermaid) - [jsdoc-mermaid](https://github.com/Jellyvision/jsdoc-mermaid)
- [MkDocs](https://www.mkdocs.org) - [MkDocs](https://www.mkdocs.org)
@ -189,6 +193,7 @@ They also serve as proof of concept, for the variety of things that can be built
- [bisheng-plugin-mermaid](https://github.com/yct21/bisheng-plugin-mermaid) - [bisheng-plugin-mermaid](https://github.com/yct21/bisheng-plugin-mermaid)
- [Reveal CK](https://github.com/jedcn/reveal-ck) - [Reveal CK](https://github.com/jedcn/reveal-ck)
- [reveal-ck-mermaid-plugin](https://github.com/tmtm/reveal-ck-mermaid-plugin) - [reveal-ck-mermaid-plugin](https://github.com/tmtm/reveal-ck-mermaid-plugin)
- [mermaid-isomorphic](https://github.com/remcohaszing/mermaid-isomorphic)
- [mermaid-server: Generate diagrams using a HTTP request](https://github.com/TomWright/mermaid-server) - [mermaid-server: Generate diagrams using a HTTP request](https://github.com/TomWright/mermaid-server)
- [ExDoc](https://github.com/elixir-lang/ex_doc) - [ExDoc](https://github.com/elixir-lang/ex_doc)
- [Rendering Mermaid graphs](https://github.com/elixir-lang/ex_doc#rendering-mermaid-graphs) - [Rendering Mermaid graphs](https://github.com/elixir-lang/ex_doc#rendering-mermaid-graphs)

View File

@ -421,6 +421,34 @@ And `Link` can be one of:
| -- | Solid | | -- | Solid |
| .. | Dashed | | .. | Dashed |
## Define Namespace
A namespace groups classes.
Code:
```mermaid-example
classDiagram
namespace BaseShapes {
class Triangle
class Rectangle {
double width
double height
}
}
```
```mermaid
classDiagram
namespace BaseShapes {
class Triangle
class Rectangle {
double width
double height
}
}
```
## Cardinality / Multiplicity on relations ## Cardinality / Multiplicity on relations
Multiplicity or cardinality in class diagrams indicates the number of instances of one class that can be linked to an instance of the other class. For example, each company will have one or more employees (not zero), and each employee currently works for zero or one companies. Multiplicity or cardinality in class diagrams indicates the number of instances of one class that can be linked to an instance of the other class. For example, each company will have one or more employees (not zero), and each employee currently works for zero or one companies.

View File

@ -1,28 +1,9 @@
import common from '../common/common.js'; import common from '../common/common.js';
import * as svgDrawCommon from '../common/svgDrawCommon';
import { sanitizeUrl } from '@braintree/sanitize-url'; import { sanitizeUrl } from '@braintree/sanitize-url';
export const drawRect = function (elem, rectData) { export const drawRect = function (elem, rectData) {
const rectElem = elem.append('rect'); return svgDrawCommon.drawRect(elem, rectData);
rectElem.attr('x', rectData.x);
rectElem.attr('y', rectData.y);
rectElem.attr('fill', rectData.fill);
rectElem.attr('stroke', rectData.stroke);
rectElem.attr('width', rectData.width);
rectElem.attr('height', rectData.height);
rectElem.attr('rx', rectData.rx);
rectElem.attr('ry', rectData.ry);
if (rectData.attrs !== 'undefined' && rectData.attrs !== null) {
for (let attrKey in rectData.attrs) {
rectElem.attr(attrKey, rectData.attrs[attrKey]);
}
}
if (rectData.class !== 'undefined') {
rectElem.attr('class', rectData.class);
}
return rectElem;
}; };
export const drawImage = function (elem, width, height, x, y, link) { export const drawImage = function (elem, width, height, x, y, link) {
@ -236,7 +217,8 @@ export const drawC4Shape = function (elem, c4Shape, conf) {
// <rect fill="#08427B" height="119.2188" rx="2.5" ry="2.5" stroke="#073B6F" stroke-width="0.5" width="110" x="120" y="7"/> // <rect fill="#08427B" height="119.2188" rx="2.5" ry="2.5" stroke="#073B6F" stroke-width="0.5" width="110" x="120" y="7"/>
// draw rect of c4Shape // draw rect of c4Shape
const rect = getNoteRect(); const rect = svgDrawCommon.getNoteRect();
switch (c4Shape.typeC4Shape.text) { switch (c4Shape.typeC4Shape.text) {
case 'person': case 'person':
case 'external_person': case 'external_person':
@ -479,6 +461,7 @@ export const insertArrowHead = function (elem) {
.append('path') .append('path')
.attr('d', 'M 0 0 L 10 5 L 0 10 z'); // this is actual shape for arrowhead .attr('d', 'M 0 0 L 10 5 L 0 10 z'); // this is actual shape for arrowhead
}; };
export const insertArrowEnd = function (elem) { export const insertArrowEnd = function (elem) {
elem elem
.append('defs') .append('defs')
@ -493,6 +476,7 @@ export const insertArrowEnd = function (elem) {
.append('path') .append('path')
.attr('d', 'M 10 0 L 0 5 L 10 10 z'); // this is actual shape for arrowhead .attr('d', 'M 10 0 L 0 5 L 10 10 z'); // this is actual shape for arrowhead
}; };
/** /**
* Setup arrow head and define the marker. The result is appended to the svg. * Setup arrow head and define the marker. The result is appended to the svg.
* *
@ -511,6 +495,7 @@ export const insertArrowFilledHead = function (elem) {
.append('path') .append('path')
.attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z'); .attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z');
}; };
/** /**
* Setup node number. The result is appended to the svg. * Setup node number. The result is appended to the svg.
* *
@ -532,6 +517,7 @@ export const insertDynamicNumber = function (elem) {
.attr('r', 6); .attr('r', 6);
// .style("fill", '#f00'); // .style("fill", '#f00');
}; };
/** /**
* Setup arrow head and define the marker. The result is appended to the svg. * Setup arrow head and define the marker. The result is appended to the svg.
* *
@ -568,20 +554,6 @@ export const insertArrowCrossHead = function (elem) {
// this is actual shape for arrowhead // this is actual shape for arrowhead
}; };
export const getNoteRect = function () {
return {
x: 0,
y: 0,
fill: '#EDF2AE',
stroke: '#666',
width: 100,
anchor: 'start',
height: 100,
rx: 0,
ry: 0,
};
};
const getC4ShapeFont = (cnf, typeC4Shape) => { const getC4ShapeFont = (cnf, typeC4Shape) => {
return { return {
fontFamily: cnf[typeC4Shape + 'FontFamily'], fontFamily: cnf[typeC4Shape + 'FontFamily'],
@ -714,6 +686,4 @@ export default {
insertDatabaseIcon, insertDatabaseIcon,
insertComputerIcon, insertComputerIcon,
insertClockIcon, insertClockIcon,
getNoteRect,
sanitizeUrl, // TODO why is this exported?
}; };

View File

@ -14,7 +14,14 @@ import {
setDiagramTitle, setDiagramTitle,
getDiagramTitle, getDiagramTitle,
} from '../../commonDb.js'; } from '../../commonDb.js';
import { ClassRelation, ClassNode, ClassNote, ClassMap } from './classTypes.js'; import {
ClassRelation,
ClassNode,
ClassNote,
ClassMap,
NamespaceMap,
NamespaceNode,
} from './classTypes.js';
const MERMAID_DOM_ID_PREFIX = 'classId-'; const MERMAID_DOM_ID_PREFIX = 'classId-';
@ -22,6 +29,8 @@ let relations: ClassRelation[] = [];
let classes: ClassMap = {}; let classes: ClassMap = {};
let notes: ClassNote[] = []; let notes: ClassNote[] = [];
let classCounter = 0; let classCounter = 0;
let namespaces: NamespaceMap = {};
let namespaceCounter = 0;
let functions: any[] = []; let functions: any[] = [];
@ -100,6 +109,8 @@ export const clear = function () {
notes = []; notes = [];
functions = []; functions = [];
functions.push(setupToolTips); functions.push(setupToolTips);
namespaces = {};
namespaceCounter = 0;
commonClear(); commonClear();
}; };
@ -237,7 +248,11 @@ const setTooltip = function (ids: string, tooltip?: string) {
}); });
}; };
export const getTooltip = function (id: string) { export const getTooltip = function (id: string, namespace?: string) {
if (namespace) {
return namespaces[namespace].classes[id].tooltip;
}
return classes[id].tooltip; return classes[id].tooltip;
}; };
/** /**
@ -395,6 +410,52 @@ const setDirection = (dir: string) => {
direction = dir; direction = dir;
}; };
/**
* Function called by parser when a namespace definition has been found.
*
* @param id - Id of the namespace to add
* @public
*/
export const addNamespace = function (id: string) {
if (namespaces[id] !== undefined) {
return;
}
namespaces[id] = {
id: id,
classes: {},
children: {},
domId: MERMAID_DOM_ID_PREFIX + id + '-' + namespaceCounter,
} as NamespaceNode;
namespaceCounter++;
};
const getNamespace = function (name: string): NamespaceNode {
return namespaces[name];
};
const getNamespaces = function (): NamespaceMap {
return namespaces;
};
/**
* Function called by parser when a namespace definition has been found.
*
* @param id - Id of the namespace to add
* @param classNames - Ids of the class to add
* @public
*/
export const addClassesToNamespace = function (id: string, classNames: string[]) {
if (namespaces[id] !== undefined) {
classNames.map((className) => {
namespaces[id].classes[className] = classes[className];
delete classes[className];
classCounter--;
});
}
};
export default { export default {
parseDirective, parseDirective,
setAccTitle, setAccTitle,
@ -428,4 +489,8 @@ export default {
setDiagramTitle, setDiagramTitle,
getDiagramTitle, getDiagramTitle,
setClassLabel, setClassLabel,
addNamespace,
addClassesToNamespace,
getNamespace,
getNamespaces,
}; };

View File

@ -816,6 +816,37 @@ describe('given a class diagram with generics, ', function () {
parser.parse(str); parser.parse(str);
}); });
it('should handle "namespace"', function () {
const str = `classDiagram
namespace Namespace1 { class Class1 }
namespace Namespace2 { class Class1
}
namespace Namespace3 {
class Class1 {
int : test
string : foo
test()
foo()
}
}
namespace Namespace4 {
class Class1 {
int : test
string : foo
test()
foo()
}
class Class2 {
int : test
string : foo
test()
foo()
}
}
`;
parser.parse(str);
});
}); });
describe('when parsing invalid generic classes', function () { describe('when parsing invalid generic classes', function () {
@ -1051,5 +1082,487 @@ describe('given a class diagram with relationships, ', function () {
expect(relations[3].relation.type2).toBe('none'); expect(relations[3].relation.type2).toBe('none');
expect(relations[3].relation.lineType).toBe(classDb.lineType.DOTTED_LINE); expect(relations[3].relation.lineType).toBe(classDb.lineType.DOTTED_LINE);
}); });
it('should handle generic class with relation definitions', function () {
const str = 'classDiagram\n' + 'Class01~T~ <|-- Class02';
parser.parse(str);
const relations = parser.yy.getRelations();
expect(parser.yy.getClass('Class01').id).toBe('Class01');
expect(parser.yy.getClass('Class01').type).toBe('T');
expect(parser.yy.getClass('Class02').id).toBe('Class02');
expect(relations[0].relation.type1).toBe(classDb.relationType.EXTENSION);
expect(relations[0].relation.type2).toBe('none');
expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE);
});
it('should handle class annotations', function () {
const str = 'classDiagram\n' + 'class Class1\n' + '<<interface>> Class1';
parser.parse(str);
const testClass = parser.yy.getClass('Class1');
expect(testClass.annotations.length).toBe(1);
expect(testClass.members.length).toBe(0);
expect(testClass.methods.length).toBe(0);
expect(testClass.annotations[0]).toBe('interface');
});
it('should handle class annotations with members and methods', function () {
const str =
'classDiagram\n' +
'class Class1\n' +
'Class1 : int test\n' +
'Class1 : test()\n' +
'<<interface>> Class1';
parser.parse(str);
const testClass = parser.yy.getClass('Class1');
expect(testClass.annotations.length).toBe(1);
expect(testClass.members.length).toBe(1);
expect(testClass.methods.length).toBe(1);
expect(testClass.annotations[0]).toBe('interface');
});
it('should handle class annotations in brackets', function () {
const str = 'classDiagram\n' + 'class Class1 {\n' + '<<interface>>\n' + '}';
parser.parse(str);
const testClass = parser.yy.getClass('Class1');
expect(testClass.annotations.length).toBe(1);
expect(testClass.members.length).toBe(0);
expect(testClass.methods.length).toBe(0);
expect(testClass.annotations[0]).toBe('interface');
});
it('should handle class annotations in brackets with members and methods', function () {
const str =
'classDiagram\n' +
'class Class1 {\n' +
'<<interface>>\n' +
'int : test\n' +
'test()\n' +
'}';
parser.parse(str);
const testClass = parser.yy.getClass('Class1');
expect(testClass.annotations.length).toBe(1);
expect(testClass.members.length).toBe(1);
expect(testClass.methods.length).toBe(1);
expect(testClass.annotations[0]).toBe('interface');
});
it('should add bracket members in right order', function () {
const str =
'classDiagram\n' +
'class Class1 {\n' +
'int : test\n' +
'string : foo\n' +
'test()\n' +
'foo()\n' +
'}';
parser.parse(str);
const testClass = parser.yy.getClass('Class1');
expect(testClass.members.length).toBe(2);
expect(testClass.methods.length).toBe(2);
expect(testClass.members[0]).toBe('int : test');
expect(testClass.members[1]).toBe('string : foo');
expect(testClass.methods[0]).toBe('test()');
expect(testClass.methods[1]).toBe('foo()');
});
it('should handle abstract methods', function () {
const str = 'classDiagram\n' + 'class Class1\n' + 'Class1 : someMethod()*';
parser.parse(str);
const testClass = parser.yy.getClass('Class1');
expect(testClass.annotations.length).toBe(0);
expect(testClass.members.length).toBe(0);
expect(testClass.methods.length).toBe(1);
expect(testClass.methods[0]).toBe('someMethod()*');
});
it('should handle static methods', function () {
const str = 'classDiagram\n' + 'class Class1\n' + 'Class1 : someMethod()$';
parser.parse(str);
const testClass = parser.yy.getClass('Class1');
expect(testClass.annotations.length).toBe(0);
expect(testClass.members.length).toBe(0);
expect(testClass.methods.length).toBe(1);
expect(testClass.methods[0]).toBe('someMethod()$');
});
it('should associate link and css appropriately', function () {
const str =
'classDiagram\n' +
'class Class1\n' +
'Class1 : someMethod()\n' +
'link Class1 "google.com"';
parser.parse(str);
const testClass = parser.yy.getClass('Class1');
expect(testClass.link).toBe('google.com');
expect(testClass.cssClasses.length).toBe(1);
expect(testClass.cssClasses[0]).toBe('clickable');
});
it('should associate click and href link and css appropriately', function () {
const str =
'classDiagram\n' +
'class Class1\n' +
'Class1 : someMethod()\n' +
'click Class1 href "google.com"';
parser.parse(str);
const testClass = parser.yy.getClass('Class1');
expect(testClass.link).toBe('google.com');
expect(testClass.cssClasses.length).toBe(1);
expect(testClass.cssClasses[0]).toBe('clickable');
});
it('should associate link with tooltip', function () {
const str =
'classDiagram\n' +
'class Class1\n' +
'Class1 : someMethod()\n' +
'link Class1 "google.com" "A tooltip"';
parser.parse(str);
const testClass = parser.yy.getClass('Class1');
expect(testClass.link).toBe('google.com');
expect(testClass.tooltip).toBe('A tooltip');
expect(testClass.cssClasses.length).toBe(1);
expect(testClass.cssClasses[0]).toBe('clickable');
});
it('should associate click and href link with tooltip', function () {
const str =
'classDiagram\n' +
'class Class1\n' +
'Class1 : someMethod()\n' +
'click Class1 href "google.com" "A tooltip"';
parser.parse(str);
const testClass = parser.yy.getClass('Class1');
expect(testClass.link).toBe('google.com');
expect(testClass.tooltip).toBe('A tooltip');
expect(testClass.cssClasses.length).toBe(1);
expect(testClass.cssClasses[0]).toBe('clickable');
});
it('should associate click and href link with tooltip and target appropriately', function () {
spyOn(classDb, 'setLink');
spyOn(classDb, 'setTooltip');
const str =
'classDiagram\n' +
'class Class1\n' +
'Class1 : someMethod()\n' +
'click Class1 href "google.com" "A tooltip" _self';
parser.parse(str);
expect(classDb.setLink).toHaveBeenCalledWith('Class1', 'google.com', '_self');
expect(classDb.setTooltip).toHaveBeenCalledWith('Class1', 'A tooltip');
});
it('should associate click and href link appropriately', function () {
spyOn(classDb, 'setLink');
const str =
'classDiagram\n' +
'class Class1\n' +
'Class1 : someMethod()\n' +
'click Class1 href "google.com"';
parser.parse(str);
expect(classDb.setLink).toHaveBeenCalledWith('Class1', 'google.com');
});
it('should associate click and href link with target appropriately', function () {
spyOn(classDb, 'setLink');
const str =
'classDiagram\n' +
'class Class1\n' +
'Class1 : someMethod()\n' +
'click Class1 href "google.com" _self';
parser.parse(str);
expect(classDb.setLink).toHaveBeenCalledWith('Class1', 'google.com', '_self');
});
it('should associate link appropriately', function () {
spyOn(classDb, 'setLink');
spyOn(classDb, 'setTooltip');
const str =
'classDiagram\n' +
'class Class1\n' +
'Class1 : someMethod()\n' +
'link Class1 "google.com" "A tooltip" _self';
parser.parse(str);
expect(classDb.setLink).toHaveBeenCalledWith('Class1', 'google.com', '_self');
expect(classDb.setTooltip).toHaveBeenCalledWith('Class1', 'A tooltip');
});
it('should associate callback appropriately', function () {
spyOn(classDb, 'setClickEvent');
const str =
'classDiagram\n' +
'class Class1\n' +
'Class1 : someMethod()\n' +
'callback Class1 "functionCall"';
parser.parse(str);
expect(classDb.setClickEvent).toHaveBeenCalledWith('Class1', 'functionCall');
});
it('should associate click and call callback appropriately', function () {
spyOn(classDb, 'setClickEvent');
const str =
'classDiagram\n' +
'class Class1\n' +
'Class1 : someMethod()\n' +
'click Class1 call functionCall()';
parser.parse(str);
expect(classDb.setClickEvent).toHaveBeenCalledWith('Class1', 'functionCall');
});
it('should associate callback appropriately with an arbitrary number of args', function () {
spyOn(classDb, 'setClickEvent');
const str =
'classDiagram\n' +
'class Class1\n' +
'Class1 : someMethod()\n' +
'click Class1 call functionCall("test0", test1, test2)';
parser.parse(str);
expect(classDb.setClickEvent).toHaveBeenCalledWith(
'Class1',
'functionCall',
'"test0", test1, test2'
);
});
it('should associate callback with tooltip', function () {
spyOn(classDb, 'setClickEvent');
spyOn(classDb, 'setTooltip');
const str =
'classDiagram\n' +
'class Class1\n' +
'Class1 : someMethod()\n' +
'click Class1 call functionCall() "A tooltip"';
parser.parse(str);
expect(classDb.setClickEvent).toHaveBeenCalledWith('Class1', 'functionCall');
expect(classDb.setTooltip).toHaveBeenCalledWith('Class1', 'A tooltip');
});
it('should add classes namespaces', function () {
const str = `classDiagram
namespace Namespace1 {
class Class1 {
int : test
string : foo
test()
foo()
}
class Class2
}`;
parser.parse(str);
const testNamespace = parser.yy.getNamespace('Namespace1');
expect(Object.keys(testNamespace.classes).length).toBe(2);
expect(Object.keys(testNamespace.children).length).toBe(0);
expect(testNamespace.classes['Class1'].id).toBe('Class1');
});
});
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.members[0]).toBe('+member1');
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.members[0]).toBe('+member1');
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,10 +1,10 @@
import { readFile } from 'node:fs/promises'; import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
// @ts-ignore - no types // @ts-ignore - no types
import { LALRGenerator } from 'jison'; import { LALRGenerator } from 'jison';
import path from 'path';
const getAbsolutePath = (relativePath: string) => { const getAbsolutePath = (relativePath: string) => {
return new URL(path.join(path.dirname(import.meta.url), relativePath)).pathname; return fileURLToPath(new URL(relativePath, import.meta.url));
}; };
describe('class diagram grammar', function () { describe('class diagram grammar', function () {

View File

@ -8,7 +8,7 @@ import utils from '../../utils.js';
import { interpolateToCurve, getStylesFromArray } from '../../utils.js'; import { interpolateToCurve, getStylesFromArray } from '../../utils.js';
import { setupGraphViewbox } from '../../setupGraphViewbox.js'; import { setupGraphViewbox } from '../../setupGraphViewbox.js';
import common from '../common/common.js'; import common from '../common/common.js';
import { ClassRelation, ClassNote, ClassMap, EdgeData } from './classTypes.js'; import { ClassRelation, ClassNote, ClassMap, EdgeData, NamespaceMap } from './classTypes.js';
const sanitizeText = (txt: string) => common.sanitizeText(txt, getConfig()); const sanitizeText = (txt: string) => common.sanitizeText(txt, getConfig());
@ -19,6 +19,59 @@ let conf = {
curve: undefined, curve: undefined,
}; };
interface RectParameters {
id: string;
shape: 'rect';
labelStyle: string;
domId: string;
labelText: string;
padding: number | undefined;
style?: string;
}
/**
* Function that adds the vertices found during parsing to the graph to be rendered.
*
* @param namespaces - 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 addNamespaces = function (
namespaces: NamespaceMap,
g: graphlib.Graph,
_id: string,
diagObj: any
) {
const keys = Object.keys(namespaces);
log.info('keys:', keys);
log.info(namespaces);
// Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
keys.forEach(function (id) {
const vertex = namespaces[id];
// parent node must be one of [rect, roundedWithTitle, noteGroup, divider]
const shape = 'rect';
const node: RectParameters = {
shape: shape,
id: vertex.id,
domId: vertex.domId,
labelText: sanitizeText(vertex.id),
labelStyle: '',
style: 'fill: none; stroke: black',
// 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);
addClasses(vertex.classes, g, _id, diagObj, vertex.id);
log.info('setNode', node);
});
};
/** /**
* Function that adds the vertices found during parsing to the graph to be rendered. * Function that adds the vertices found during parsing to the graph to be rendered.
* *
@ -26,12 +79,14 @@ let conf = {
* @param g - The graph that is to be drawn. * @param g - The graph that is to be drawn.
* @param _id - id of the graph * @param _id - id of the graph
* @param diagObj - The diagram object * @param diagObj - The diagram object
* @param parent - id of the parent namespace, if it exists
*/ */
export const addClasses = function ( export const addClasses = function (
classes: ClassMap, classes: ClassMap,
g: graphlib.Graph, g: graphlib.Graph,
_id: string, _id: string,
diagObj: any diagObj: any,
parent?: string
) { ) {
const keys = Object.keys(classes); const keys = Object.keys(classes);
log.info('keys:', keys); log.info('keys:', keys);
@ -55,6 +110,7 @@ export const addClasses = function (
const vertexText = vertex.label ?? vertex.id; const vertexText = vertex.label ?? vertex.id;
const radius = 0; const radius = 0;
const shape = 'class_box'; const shape = 'class_box';
// Add the node // Add the node
const node = { const node = {
labelStyle: styles.labelStyle, labelStyle: styles.labelStyle,
@ -67,7 +123,7 @@ export const addClasses = function (
style: styles.style, style: styles.style,
id: vertex.id, id: vertex.id,
domId: vertex.domId, domId: vertex.domId,
tooltip: diagObj.db.getTooltip(vertex.id) || '', tooltip: diagObj.db.getTooltip(vertex.id, parent) || '',
haveCallback: vertex.haveCallback, haveCallback: vertex.haveCallback,
link: vertex.link, link: vertex.link,
width: vertex.type === 'group' ? 500 : undefined, width: vertex.type === 'group' ? 500 : undefined,
@ -76,6 +132,11 @@ export const addClasses = function (
padding: getConfig().flowchart?.padding ?? getConfig().class?.padding, padding: getConfig().flowchart?.padding ?? getConfig().class?.padding,
}; };
g.setNode(vertex.id, node); g.setNode(vertex.id, node);
if (parent) {
g.setParent(vertex.id, parent);
}
log.info('setNode', node); log.info('setNode', node);
}); });
}; };
@ -275,10 +336,12 @@ export const draw = async function (text: string, id: string, _version: string,
}); });
// Fetch the vertices/nodes and edges/links from the parsed graph definition // Fetch the vertices/nodes and edges/links from the parsed graph definition
const namespaces: NamespaceMap = diagObj.db.getNamespaces();
const classes: ClassMap = diagObj.db.getClasses(); const classes: ClassMap = diagObj.db.getClasses();
const relations: ClassRelation[] = diagObj.db.getRelations(); const relations: ClassRelation[] = diagObj.db.getRelations();
const notes: ClassNote[] = diagObj.db.getNotes(); const notes: ClassNote[] = diagObj.db.getNotes();
log.info(relations); log.info(relations);
addNamespaces(namespaces, g, id, diagObj);
addClasses(classes, g, id, diagObj); addClasses(classes, g, id, diagObj);
addRelations(relations, g); addRelations(relations, g);
addNotes(notes, g, relations.length + 1, classes); addNotes(notes, g, relations.length + 1, classes);

View File

@ -52,4 +52,13 @@ export type ClassRelation = {
lineType: number; lineType: number;
}; };
}; };
export interface NamespaceNode {
id: string;
domId: string;
classes: ClassMap;
children: NamespaceMap;
}
export type ClassMap = Record<string, ClassNode>; export type ClassMap = Record<string, ClassNode>;
export type NamespaceMap = Record<string, NamespaceNode>;

View File

@ -19,6 +19,10 @@
%x acc_title %x acc_title
%x acc_descr %x acc_descr
%x acc_descr_multiline %x acc_descr_multiline
%x class
%x class-body
%x namespace
%x namespace-body
%% %%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; } \%\%\{ { this.begin('open_directive'); return 'open_directive'; }
.*direction\s+TB[^\n]* return 'direction_tb'; .*direction\s+TB[^\n]* return 'direction_tb';
@ -41,35 +45,41 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
\s*(\r?\n)+ return 'NEWLINE'; \s*(\r?\n)+ return 'NEWLINE';
\s+ /* skip whitespace */ \s+ /* skip whitespace */
"classDiagram-v2" return 'CLASS_DIAGRAM'; "classDiagram-v2" return 'CLASS_DIAGRAM';
"classDiagram" return 'CLASS_DIAGRAM'; "classDiagram" return 'CLASS_DIAGRAM';
[{] { this.begin("struct"); /*console.log('Starting struct');*/ return 'STRUCT_START';} "[*]" return 'EDGE_STATE';
<INITIAL,struct>"[*]" { /*console.log('EDGE_STATE=',yytext);*/ return 'EDGE_STATE';}
<struct><<EOF>> return "EOF_IN_STRUCT";
<struct>[{] return "OPEN_IN_STRUCT";
<struct>[}] { /*console.log('Ending struct');*/this.popState(); return 'STRUCT_STOP';}}
<struct>[\n] /* nothing */
<struct>[^{}\n]* { /*console.log('lex-member: ' + yytext);*/ return "MEMBER";}
"class" return 'CLASS'; <INITIAL,namespace>"namespace" { this.begin('namespace'); return 'NAMESPACE'; }
"cssClass" return 'CSSCLASS'; <namespace>\s*(\r?\n)+ { this.popState(); return 'NEWLINE'; }
"callback" return 'CALLBACK'; <namespace>\s+ /* skip whitespace */
"link" return 'LINK'; <namespace>[{] { this.begin("namespace-body"); return 'STRUCT_START';}
"click" return 'CLICK'; <namespace-body>[}] { this.popState(); return 'STRUCT_STOP'; }
"note for" return 'NOTE_FOR'; <namespace-body><<EOF>> return "EOF_IN_STRUCT";
"note" return 'NOTE'; <namespace-body>\s*(\r?\n)+ return 'NEWLINE';
"<<" return 'ANNOTATION_START'; <namespace-body>\s+ /* skip whitespace */
">>" return 'ANNOTATION_END'; <namespace-body>"[*]" return 'EDGE_STATE';
[~] this.begin("generic");
<generic>[~] this.popState();
<generic>[^~]* return "GENERICTYPE";
["] this.begin("string");
<string>["] this.popState();
<string>[^"]* return "STR";
[`] this.begin("bqstring"); <INITIAL,namespace-body>"class" { this.begin('class'); return 'CLASS';}
<bqstring>[`] this.popState(); <class>\s*(\r?\n)+ { this.popState(); return 'NEWLINE'; }
<bqstring>[^`]+ return "BQUOTE_STR"; <class>\s+ /* skip whitespace */
<class>[}] { this.popState(); this.popState(); return 'STRUCT_STOP';}
<class>[{] { this.begin("class-body"); return 'STRUCT_START';}
<class-body>[}] { this.popState(); return 'STRUCT_STOP'; }
<class-body><<EOF>> return "EOF_IN_STRUCT";
<class-body>"[*]" { return 'EDGE_STATE';}
<class-body>[{] return "OPEN_IN_STRUCT";
<class-body>[\n] /* nothing */
<class-body>[^{}\n]* { return "MEMBER";}
<*>"cssClass" return 'CSSCLASS';
<*>"callback" return 'CALLBACK';
<*>"link" return 'LINK';
<*>"click" return 'CLICK';
<*>"note for" return 'NOTE_FOR';
<*>"note" return 'NOTE';
<*>"<<" return 'ANNOTATION_START';
<*>">>" return 'ANNOTATION_END';
/* /*
---interactivity command--- ---interactivity command---
@ -77,7 +87,7 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
line was introduced with 'click'. line was introduced with 'click'.
'href "<link>"' attaches the specified link to the node that was specified by 'click'. 'href "<link>"' attaches the specified link to the node that was specified by 'click'.
*/ */
"href"[\s]+["] this.begin("href"); <*>"href"[\s]+["] this.begin("href");
<href>["] this.popState(); <href>["] this.popState();
<href>[^"]* return 'HREF'; <href>[^"]* return 'HREF';
@ -89,41 +99,53 @@ the line was introduced with 'click'.
arguments to the node that was specified by 'click'. arguments to the node that was specified by 'click'.
Function arguments are optional: 'call <callback_name>()' simply executes 'callback_name' without any arguments. Function arguments are optional: 'call <callback_name>()' simply executes 'callback_name' without any arguments.
*/ */
"call"[\s]+ this.begin("callback_name"); <*>"call"[\s]+ this.begin("callback_name");
<callback_name>\([\s]*\) this.popState(); <callback_name>\([\s]*\) this.popState();
<callback_name>\( this.popState(); this.begin("callback_args"); <callback_name>\( this.popState(); this.begin("callback_args");
<callback_name>[^(]* return 'CALLBACK_NAME'; <callback_name>[^(]* return 'CALLBACK_NAME';
<callback_args>\) this.popState(); <callback_args>\) this.popState();
<callback_args>[^)]* return 'CALLBACK_ARGS'; <callback_args>[^)]* return 'CALLBACK_ARGS';
"_self" return 'LINK_TARGET'; <generic>[~] this.popState();
"_blank" return 'LINK_TARGET'; <generic>[^~]* return "GENERICTYPE";
"_parent" return 'LINK_TARGET'; <*>[~] this.begin("generic");
"_top" return 'LINK_TARGET';
\s*\<\| return 'EXTENSION'; <string>["] this.popState();
\s*\|\> return 'EXTENSION'; <string>[^"]* return "STR";
\s*\> return 'DEPENDENCY'; <*>["] this.begin("string");
\s*\< return 'DEPENDENCY';
\s*\* return 'COMPOSITION'; <bqstring>[`] this.popState();
\s*o return 'AGGREGATION'; <bqstring>[^`]+ return "BQUOTE_STR";
\s*\(\) return 'LOLLIPOP'; <*>[`] this.begin("bqstring");
\-\- return 'LINE';
\.\. return 'DOTTED_LINE'; <*>"_self" return 'LINK_TARGET';
":"{1}[^:\n;]+ return 'LABEL'; <*>"_blank" return 'LINK_TARGET';
":"{3} return 'STYLE_SEPARATOR'; <*>"_parent" return 'LINK_TARGET';
\- return 'MINUS'; <*>"_top" return 'LINK_TARGET';
"." return 'DOT';
\+ return 'PLUS'; <*>\s*\<\| return 'EXTENSION';
\% return 'PCT'; <*>\s*\|\> return 'EXTENSION';
"=" return 'EQUALS'; <*>\s*\> return 'DEPENDENCY';
\= return 'EQUALS'; <*>\s*\< return 'DEPENDENCY';
\w+ return 'ALPHA'; <*>\s*\* return 'COMPOSITION';
"[" return 'SQS'; <*>\s*o return 'AGGREGATION';
"]" return 'SQE'; <*>\s*\(\) return 'LOLLIPOP';
[!"#$%&'*+,-.`?\\/] return 'PUNCTUATION'; <*>\-\- return 'LINE';
[0-9]+ return 'NUM'; <*>\.\. return 'DOTTED_LINE';
[\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]| <*>":"{1}[^:\n;]+ return 'LABEL';
<*>":"{3} return 'STYLE_SEPARATOR';
<*>\- return 'MINUS';
<*>"." return 'DOT';
<*>\+ return 'PLUS';
<*>\% return 'PCT';
<*>"=" return 'EQUALS';
<*>\= return 'EQUALS';
<*>\w+ return 'ALPHA';
<*>"[" return 'SQS';
<*>"]" return 'SQE';
<*>[!"#$%&'*+,-.`?\\/] return 'PUNCTUATION';
<*>[0-9]+ return 'NUM';
<*>[\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]|
[\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377]| [\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377]|
[\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5]| [\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5]|
[\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA]| [\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA]|
@ -185,8 +207,8 @@ Function arguments are optional: 'call <callback_name>()' simply executes 'callb
[\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF]| [\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF]|
[\uFFD2-\uFFD7\uFFDA-\uFFDC] [\uFFD2-\uFFD7\uFFDA-\uFFDC]
return 'UNICODE_TEXT'; return 'UNICODE_TEXT';
\s return 'SPACE'; <*>\s return 'SPACE';
<<EOF>> return 'EOF'; <*><<EOF>> return 'EOF';
/lex /lex
@ -254,6 +276,11 @@ classLabel
: SQS STR SQE { $$=$2; } : SQS STR SQE { $$=$2; }
; ;
namespaceName
: alphaNumToken { $$=$1; }
| alphaNumToken namespaceName { $$=$1+$2; }
;
className className
: alphaNumToken { $$=$1; } : alphaNumToken { $$=$1; }
| classLiteralName { $$=$1; } | classLiteralName { $$=$1; }
@ -265,6 +292,7 @@ className
statement statement
: relationStatement { yy.addRelation($1); } : relationStatement { yy.addRelation($1); }
| relationStatement LABEL { $1.title = yy.cleanupLabel($2); yy.addRelation($1); } | relationStatement LABEL { $1.title = yy.cleanupLabel($2); yy.addRelation($1); }
| namespaceStatement
| classStatement | classStatement
| methodStatement | methodStatement
| annotationStatement | annotationStatement
@ -277,6 +305,21 @@ statement
| acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); } | acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); }
; ;
namespaceStatement
: namespaceIdentifier STRUCT_START classStatements STRUCT_STOP {yy.addClassesToNamespace($1, $3);}
| namespaceIdentifier STRUCT_START NEWLINE classStatements STRUCT_STOP {yy.addClassesToNamespace($1, $4);}
;
namespaceIdentifier
: NAMESPACE namespaceName {$$=$2; yy.addNamespace($2);}
;
classStatements
: classStatement {$$=[$1]}
| classStatement NEWLINE {$$=[$1]}
| classStatement NEWLINE classStatements {$3.unshift($1); $$=$3}
;
classStatement classStatement
: classIdentifier : classIdentifier
| classIdentifier STYLE_SEPARATOR alphaNumToken {yy.setCssClass($1, $3);} | classIdentifier STYLE_SEPARATOR alphaNumToken {yy.setCssClass($1, $3);}

View File

@ -486,6 +486,7 @@ const buildLegacyDisplay = function (text) {
cssStyle, cssStyle,
}; };
}; };
/** /**
* Adds a <tspan> for a member in a diagram * Adds a <tspan> for a member in a diagram
* *

View File

@ -138,6 +138,32 @@ const getUrl = (useAbsolute: boolean): string => {
export const evaluate = (val?: string | boolean): boolean => export const evaluate = (val?: string | boolean): boolean =>
val === false || ['false', 'null', '0'].includes(String(val).trim().toLowerCase()) ? false : true; val === false || ['false', 'null', '0'].includes(String(val).trim().toLowerCase()) ? false : true;
/**
* Wrapper around Math.max which removes non-numeric values
* Returns the larger of a set of supplied numeric expressions.
* @param values - Numeric expressions to be evaluated
* @returns The smaller value
*/
export const getMax = function (...values: number[]): number {
const newValues: number[] = values.filter((value) => {
return !isNaN(value);
});
return Math.max(...newValues);
};
/**
* Wrapper around Math.min which removes non-numeric values
* Returns the smaller of a set of supplied numeric expressions.
* @param values - Numeric expressions to be evaluated
* @returns The smaller value
*/
export const getMin = function (...values: number[]): number {
const newValues: number[] = values.filter((value) => {
return !isNaN(value);
});
return Math.min(...newValues);
};
/** /**
* Makes generics in typescript syntax * Makes generics in typescript syntax
* *
@ -180,4 +206,6 @@ export default {
removeScript, removeScript,
getUrl, getUrl,
evaluate, evaluate,
getMax,
getMin,
}; };

View File

@ -0,0 +1,114 @@
import { sanitizeUrl } from '@braintree/sanitize-url';
export const drawRect = function (elem, rectData) {
const rectElem = elem.append('rect');
rectElem.attr('x', rectData.x);
rectElem.attr('y', rectData.y);
rectElem.attr('fill', rectData.fill);
rectElem.attr('stroke', rectData.stroke);
rectElem.attr('width', rectData.width);
rectElem.attr('height', rectData.height);
rectElem.attr('rx', rectData.rx);
rectElem.attr('ry', rectData.ry);
if (rectData.attrs !== 'undefined' && rectData.attrs !== null) {
for (let attrKey in rectData.attrs) {
rectElem.attr(attrKey, rectData.attrs[attrKey]);
}
}
if (rectData.class !== 'undefined') {
rectElem.attr('class', rectData.class);
}
return rectElem;
};
/**
* Draws a background rectangle
*
* @param {any} elem Diagram (reference for bounds)
* @param {any} bounds Shape of the rectangle
*/
export const drawBackgroundRect = function (elem, bounds) {
const rectElem = drawRect(elem, {
x: bounds.startx,
y: bounds.starty,
width: bounds.stopx - bounds.startx,
height: bounds.stopy - bounds.starty,
fill: bounds.fill,
stroke: bounds.stroke,
class: 'rect',
});
rectElem.lower();
};
export const drawText = function (elem, textData) {
// Remove and ignore br:s
const nText = textData.text.replace(/<br\s*\/?>/gi, ' ');
const textElem = elem.append('text');
textElem.attr('x', textData.x);
textElem.attr('y', textData.y);
textElem.attr('class', 'legend');
textElem.style('text-anchor', textData.anchor);
if (textData.class !== undefined) {
textElem.attr('class', textData.class);
}
const span = textElem.append('tspan');
span.attr('x', textData.x + textData.textMargin * 2);
span.text(nText);
return textElem;
};
export const drawImage = function (elem, x, y, link) {
const imageElem = elem.append('image');
imageElem.attr('x', x);
imageElem.attr('y', y);
var sanitizedLink = sanitizeUrl(link);
imageElem.attr('xlink:href', sanitizedLink);
};
export const drawEmbeddedImage = function (elem, x, y, link) {
const imageElem = elem.append('use');
imageElem.attr('x', x);
imageElem.attr('y', y);
const sanitizedLink = sanitizeUrl(link);
imageElem.attr('xlink:href', '#' + sanitizedLink);
};
export const getNoteRect = function () {
return {
x: 0,
y: 0,
width: 100,
height: 100,
fill: '#EDF2AE',
stroke: '#666',
anchor: 'start',
rx: 0,
ry: 0,
};
};
export const getTextObj = function () {
return {
x: 0,
y: 0,
width: 100,
height: 100,
fill: undefined,
anchor: undefined,
'text-anchor': 'start',
style: '#666',
textMargin: 0,
rx: 0,
ry: 0,
tspan: true,
valign: undefined,
};
};

View File

@ -20,6 +20,7 @@ const Cardinality = {
ZERO_OR_MORE: 'ZERO_OR_MORE', ZERO_OR_MORE: 'ZERO_OR_MORE',
ONE_OR_MORE: 'ONE_OR_MORE', ONE_OR_MORE: 'ONE_OR_MORE',
ONLY_ONE: 'ONLY_ONE', ONLY_ONE: 'ONLY_ONE',
MD_PARENT: 'MD_PARENT',
}; };
const Identification = { const Identification = {

View File

@ -7,6 +7,8 @@ const ERMarkers = {
ONE_OR_MORE_END: 'ONE_OR_MORE_END', ONE_OR_MORE_END: 'ONE_OR_MORE_END',
ZERO_OR_MORE_START: 'ZERO_OR_MORE_START', ZERO_OR_MORE_START: 'ZERO_OR_MORE_START',
ZERO_OR_MORE_END: 'ZERO_OR_MORE_END', ZERO_OR_MORE_END: 'ZERO_OR_MORE_END',
MD_PARENT_END: 'MD_PARENT_END',
MD_PARENT_START: 'MD_PARENT_START',
}; };
/** /**
@ -18,6 +20,30 @@ const ERMarkers = {
const insertMarkers = function (elem, conf) { const insertMarkers = function (elem, conf) {
let marker; let marker;
elem
.append('defs')
.append('marker')
.attr('id', ERMarkers.MD_PARENT_START)
.attr('refX', 0)
.attr('refY', 7)
.attr('markerWidth', 190)
.attr('markerHeight', 240)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
elem
.append('defs')
.append('marker')
.attr('id', ERMarkers.MD_PARENT_END)
.attr('refX', 19)
.attr('refY', 7)
.attr('markerWidth', 20)
.attr('markerHeight', 28)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
elem elem
.append('defs') .append('defs')
.append('marker') .append('marker')

View File

@ -478,6 +478,9 @@ const drawRelationshipFromLayout = function (svg, rel, g, insert, diagObj) {
case diagObj.db.Cardinality.ONLY_ONE: case diagObj.db.Cardinality.ONLY_ONE:
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_END + ')'); svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_END + ')');
break; break;
case diagObj.db.Cardinality.MD_PARENT:
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.MD_PARENT_END + ')');
break;
} }
switch (rel.relSpec.cardB) { switch (rel.relSpec.cardB) {
@ -502,6 +505,9 @@ const drawRelationshipFromLayout = function (svg, rel, g, insert, diagObj) {
case diagObj.db.Cardinality.ONLY_ONE: case diagObj.db.Cardinality.ONLY_ONE:
svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_START + ')'); svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_START + ')');
break; break;
case diagObj.db.Cardinality.MD_PARENT:
svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.MD_PARENT_START + ')');
break;
} }
// Now label the relationship // Now label the relationship

View File

@ -57,6 +57,7 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
o\| return 'ZERO_OR_ONE'; o\| return 'ZERO_OR_ONE';
o\{ return 'ZERO_OR_MORE'; o\{ return 'ZERO_OR_MORE';
\|\{ return 'ONE_OR_MORE'; \|\{ return 'ONE_OR_MORE';
\s*u return 'MD_PARENT';
\.\. return 'NON_IDENTIFYING'; \.\. return 'NON_IDENTIFYING';
\-\- return 'IDENTIFYING'; \-\- return 'IDENTIFYING';
"to" return 'IDENTIFYING'; "to" return 'IDENTIFYING';
@ -170,6 +171,7 @@ cardinality
| 'ZERO_OR_MORE' { $$ = yy.Cardinality.ZERO_OR_MORE; } | 'ZERO_OR_MORE' { $$ = yy.Cardinality.ZERO_OR_MORE; }
| 'ONE_OR_MORE' { $$ = yy.Cardinality.ONE_OR_MORE; } | 'ONE_OR_MORE' { $$ = yy.Cardinality.ONE_OR_MORE; }
| 'ONLY_ONE' { $$ = yy.Cardinality.ONLY_ONE; } | 'ONLY_ONE' { $$ = yy.Cardinality.ONLY_ONE; }
| 'MD_PARENT' { $$ = yy.Cardinality.MD_PARENT; }
; ;
relType relType

View File

@ -718,5 +718,14 @@ describe('when parsing ER diagram it...', function () {
const rels = erDb.getRelationships(); const rels = erDb.getRelationships();
expect(rels[0].roleA).toBe('places'); expect(rels[0].roleA).toBe('places');
}); });
it('should represent parent-child relationship correctly', function () {
erDiagram.parser.parse('erDiagram\nPROJECT u--o{ TEAM_MEMBER : "parent"');
const rels = erDb.getRelationships();
expect(Object.keys(erDb.getEntities()).length).toBe(2);
expect(rels.length).toBe(1);
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.MD_PARENT);
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE);
});
}); });
}); });

View File

@ -33,6 +33,17 @@ const getStyles = (options) =>
font-size: 18px; font-size: 18px;
fill: ${options.textColor}; fill: ${options.textColor};
} }
#MD_PARENT_START {
fill: #f5f5f5 !important;
stroke: ${options.lineColor} !important;
stroke-width: 1;
}
#MD_PARENT_END {
fill: #f5f5f5 !important;
stroke: ${options.lineColor} !important;
stroke-width: 1;
}
`; `;
export default getStyles; export default getStyles;

View File

@ -414,7 +414,7 @@ export const addEdges = function (edges, diagObj, graph, svg) {
edges.forEach(function (edge) { edges.forEach(function (edge) {
// Identify Link // Identify Link
var linkIdBase = 'L-' + edge.start + '-' + edge.end; const linkIdBase = 'L-' + edge.start + '-' + edge.end;
// count the links from+to the same node to give unique id // count the links from+to the same node to give unique id
if (linkIdCnt[linkIdBase] === undefined) { if (linkIdCnt[linkIdBase] === undefined) {
linkIdCnt[linkIdBase] = 0; linkIdCnt[linkIdBase] = 0;
@ -425,8 +425,8 @@ export const addEdges = function (edges, diagObj, graph, svg) {
} }
let linkId = linkIdBase + '-' + linkIdCnt[linkIdBase]; let linkId = linkIdBase + '-' + linkIdCnt[linkIdBase];
log.info('abc78 new link id to be used is', linkIdBase, linkId, linkIdCnt[linkIdBase]); log.info('abc78 new link id to be used is', linkIdBase, linkId, linkIdCnt[linkIdBase]);
var linkNameStart = 'LS-' + edge.start; const linkNameStart = 'LS-' + edge.start;
var linkNameEnd = 'LE-' + edge.end; const linkNameEnd = 'LE-' + edge.end;
const edgeData = { style: '', labelStyle: '' }; const edgeData = { style: '', labelStyle: '' };
edgeData.minlen = edge.length || 1; edgeData.minlen = edge.length || 1;

View File

@ -214,7 +214,7 @@ export const addEdges = function (edges, g, diagObj) {
cnt++; cnt++;
// Identify Link // Identify Link
var linkIdBase = 'L-' + edge.start + '-' + edge.end; const linkIdBase = 'L-' + edge.start + '-' + edge.end;
// count the links from+to the same node to give unique id // count the links from+to the same node to give unique id
if (linkIdCnt[linkIdBase] === undefined) { if (linkIdCnt[linkIdBase] === undefined) {
linkIdCnt[linkIdBase] = 0; linkIdCnt[linkIdBase] = 0;
@ -225,8 +225,8 @@ export const addEdges = function (edges, g, diagObj) {
} }
let linkId = linkIdBase + '-' + linkIdCnt[linkIdBase]; let linkId = linkIdBase + '-' + linkIdCnt[linkIdBase];
log.info('abc78 new link id to be used is', linkIdBase, linkId, linkIdCnt[linkIdBase]); log.info('abc78 new link id to be used is', linkIdBase, linkId, linkIdCnt[linkIdBase]);
var linkNameStart = 'LS-' + edge.start; const linkNameStart = 'LS-' + edge.start;
var linkNameEnd = 'LE-' + edge.end; const linkNameEnd = 'LE-' + edge.end;
const edgeData = { style: '', labelStyle: '' }; const edgeData = { style: '', labelStyle: '' };
edgeData.minlen = edge.length || 1; edgeData.minlen = edge.length || 1;

View File

@ -176,9 +176,9 @@ export const addEdges = function (edges, g, diagObj) {
cnt++; cnt++;
// Identify Link // Identify Link
var linkId = 'L-' + edge.start + '-' + edge.end; const linkId = 'L-' + edge.start + '-' + edge.end;
var linkNameStart = 'LS-' + edge.start; const linkNameStart = 'LS-' + edge.start;
var linkNameEnd = 'LE-' + edge.end; const linkNameEnd = 'LE-' + edge.end;
const edgeData = {}; const edgeData = {};

View File

@ -287,7 +287,17 @@ const getStartDate = function (prevTime, dateFormat, str) {
log.debug('Invalid date:' + str); log.debug('Invalid date:' + str);
log.debug('With date format:' + dateFormat.trim()); log.debug('With date format:' + dateFormat.trim());
const d = new Date(str); const d = new Date(str);
if (d === undefined || isNaN(d.getTime())) { if (
d === undefined ||
isNaN(d.getTime()) ||
// WebKit browsers can mis-parse invalid dates to be ridiculously
// huge numbers, e.g. new Date('202304') gets parsed as January 1, 202304.
// This can cause virtually infinite loops while rendering, so for the
// purposes of Gantt charts we'll just treat any date beyond 10,000 AD/BC as
// invalid.
d.getFullYear() < -10000 ||
d.getFullYear() > 10000
) {
throw new Error('Invalid date:' + str); throw new Error('Invalid date:' + str);
} }
return d; return d;

View File

@ -432,4 +432,10 @@ describe('when using the ganttDb', function () {
ganttDb.setTodayMarker(expected); ganttDb.setTodayMarker(expected);
expect(ganttDb.getTodayMarker()).toEqual(expected); expect(ganttDb.getTodayMarker()).toEqual(expected);
}); });
it('should reject dates with ridiculous years', function () {
ganttDb.setDateFormat('YYYYMMDD');
ganttDb.addTask('test1', 'id1,202304,1d');
expect(() => ganttDb.getTasks()).toThrowError('Invalid date:202304');
});
}); });

View File

@ -70,6 +70,7 @@ const defaultBkg = function (elem, node, section) {
.attr('x2', node.width) .attr('x2', node.width)
.attr('y2', node.height); .attr('y2', node.height);
}; };
const rectBkg = function (elem, node) { const rectBkg = function (elem, node) {
elem elem
.append('rect') .append('rect')
@ -78,6 +79,7 @@ const rectBkg = function (elem, node) {
.attr('height', node.height) .attr('height', node.height)
.attr('width', node.width); .attr('width', node.width);
}; };
const cloudBkg = function (elem, node) { const cloudBkg = function (elem, node) {
const w = node.width; const w = node.width;
const h = node.height; const h = node.height;
@ -108,6 +110,7 @@ const cloudBkg = function (elem, node) {
H0 V0 Z` H0 V0 Z`
); );
}; };
const bangBkg = function (elem, node) { const bangBkg = function (elem, node) {
const w = node.width; const w = node.width;
const h = node.height; const h = node.height;
@ -139,6 +142,7 @@ const bangBkg = function (elem, node) {
H0 V0 Z` H0 V0 Z`
); );
}; };
const circleBkg = function (elem, node) { const circleBkg = function (elem, node) {
elem elem
.append('circle') .append('circle')

View File

@ -47,6 +47,7 @@
"alt" { this.begin('LINE'); return 'alt'; } "alt" { this.begin('LINE'); return 'alt'; }
"else" { this.begin('LINE'); return 'else'; } "else" { this.begin('LINE'); return 'else'; }
"par" { this.begin('LINE'); return 'par'; } "par" { this.begin('LINE'); return 'par'; }
"par_over" { this.begin('LINE'); return 'par_over'; }
"and" { this.begin('LINE'); return 'and'; } "and" { this.begin('LINE'); return 'and'; }
"critical" { this.begin('LINE'); return 'critical'; } "critical" { this.begin('LINE'); return 'critical'; }
"option" { this.begin('LINE'); return 'option'; } "option" { this.begin('LINE'); return 'option'; }
@ -190,6 +191,14 @@ statement
// End // End
$3.push({type: 'parEnd', signalType: yy.LINETYPE.PAR_END}); $3.push({type: 'parEnd', signalType: yy.LINETYPE.PAR_END});
$$=$3;} $$=$3;}
| par_over restOfLine par_sections end
{
// Parallel (overlapped) start
$3.unshift({type: 'parStart', parText:yy.parseMessage($2), signalType: yy.LINETYPE.PAR_OVER_START});
// Content in par is already in $3
// End
$3.push({type: 'parEnd', signalType: yy.LINETYPE.PAR_END});
$$=$3;}
| critical restOfLine option_sections end | critical restOfLine option_sections end
{ {
// critical start // critical start

View File

@ -286,6 +286,7 @@ export const LINETYPE = {
CRITICAL_END: 29, CRITICAL_END: 29,
BREAK_START: 30, BREAK_START: 30,
BREAK_END: 31, BREAK_END: 31,
PAR_OVER_START: 32,
}; };
export const ARROWTYPE = { export const ARROWTYPE = {

View File

@ -1126,6 +1126,29 @@ end`;
expect(messages[1].from).toBe('Alice'); expect(messages[1].from).toBe('Alice');
expect(messages[2].from).toBe('Bob'); expect(messages[2].from).toBe('Bob');
}); });
it('it should handle par_over statements', async () => {
const str = `
sequenceDiagram
par_over Parallel overlap
Alice ->> Bob: Message
Note left of Alice: Alice note
Note right of Bob: Bob note
end`;
await mermaidAPI.parse(str);
const actors = diagram.db.getActors();
expect(actors.Alice.description).toBe('Alice');
expect(actors.Bob.description).toBe('Bob');
const messages = diagram.db.getMessages();
expect(messages.length).toBe(5);
expect(messages[0].message).toBe('Parallel overlap');
expect(messages[1].from).toBe('Alice');
expect(messages[2].from).toBe('Alice');
expect(messages[3].from).toBe('Bob');
});
it('should handle special characters in signals', async () => { it('should handle special characters in signals', async () => {
const str = 'sequenceDiagram\n' + 'Alice->Bob: -:<>,;# comment'; const str = 'sequenceDiagram\n' + 'Alice->Bob: -:<>,;# comment';

View File

@ -3,6 +3,7 @@ import { select, selectAll } from 'd3';
import svgDraw, { drawText, fixLifeLineHeights } from './svgDraw.js'; import svgDraw, { drawText, fixLifeLineHeights } from './svgDraw.js';
import { log } from '../../logger.js'; import { log } from '../../logger.js';
import common from '../common/common.js'; import common from '../common/common.js';
import * as svgDrawCommon from '../common/svgDrawCommon';
import * as configApi from '../../config.js'; import * as configApi from '../../config.js';
import assignWithDepth from '../../assignWithDepth.js'; import assignWithDepth from '../../assignWithDepth.js';
import utils from '../../utils.js'; import utils from '../../utils.js';
@ -130,10 +131,10 @@ export const bounds = {
this.activations.forEach(updateFn('activation')); this.activations.forEach(updateFn('activation'));
}, },
insert: function (startx, starty, stopx, stopy) { insert: function (startx, starty, stopx, stopy) {
const _startx = Math.min(startx, stopx); const _startx = common.getMin(startx, stopx);
const _stopx = Math.max(startx, stopx); const _stopx = common.getMax(startx, stopx);
const _starty = Math.min(starty, stopy); const _starty = common.getMin(starty, stopy);
const _stopy = Math.max(starty, stopy); const _stopy = common.getMax(starty, stopy);
this.updateVal(bounds.data, 'startx', _startx, Math.min); this.updateVal(bounds.data, 'startx', _startx, Math.min);
this.updateVal(bounds.data, 'starty', _starty, Math.min); this.updateVal(bounds.data, 'starty', _starty, Math.min);
@ -183,6 +184,11 @@ export const bounds = {
endLoop: function () { endLoop: function () {
return this.sequenceItems.pop(); return this.sequenceItems.pop();
}, },
isLoopOverlap: function () {
return this.sequenceItems.length
? this.sequenceItems[this.sequenceItems.length - 1].overlap
: false;
},
addSectionToLoop: function (message) { addSectionToLoop: function (message) {
const loop = this.sequenceItems.pop(); const loop = this.sequenceItems.pop();
loop.sections = loop.sections || []; loop.sections = loop.sections || [];
@ -191,9 +197,19 @@ export const bounds = {
loop.sectionTitles.push(message); loop.sectionTitles.push(message);
this.sequenceItems.push(loop); this.sequenceItems.push(loop);
}, },
saveVerticalPos: function () {
if (this.isLoopOverlap()) {
this.savedVerticalPos = this.verticalPos;
}
},
resetVerticalPos: function () {
if (this.isLoopOverlap()) {
this.verticalPos = this.savedVerticalPos;
}
},
bumpVerticalPos: function (bump) { bumpVerticalPos: function (bump) {
this.verticalPos = this.verticalPos + bump; this.verticalPos = this.verticalPos + bump;
this.data.stopy = this.verticalPos; this.data.stopy = common.getMax(this.data.stopy, this.verticalPos);
}, },
getVerticalPos: function () { getVerticalPos: function () {
return this.verticalPos; return this.verticalPos;
@ -225,7 +241,7 @@ const drawNote = function (elem: any, noteModel: NoteModel) {
bounds.bumpVerticalPos(conf.boxMargin); bounds.bumpVerticalPos(conf.boxMargin);
noteModel.height = conf.boxMargin; noteModel.height = conf.boxMargin;
noteModel.starty = bounds.getVerticalPos(); noteModel.starty = bounds.getVerticalPos();
const rect = svgDraw.getNoteRect(); const rect = svgDrawCommon.getNoteRect();
rect.x = noteModel.startx; rect.x = noteModel.startx;
rect.y = noteModel.starty; rect.y = noteModel.starty;
rect.width = noteModel.width || conf.width; rect.width = noteModel.width || conf.width;
@ -233,7 +249,7 @@ const drawNote = function (elem: any, noteModel: NoteModel) {
const g = elem.append('g'); const g = elem.append('g');
const rectElem = svgDraw.drawRect(g, rect); const rectElem = svgDraw.drawRect(g, rect);
const textObj = svgDraw.getTextObj(); const textObj = svgDrawCommon.getTextObj();
textObj.x = noteModel.startx; textObj.x = noteModel.startx;
textObj.y = noteModel.starty; textObj.y = noteModel.starty;
textObj.width = rect.width; textObj.width = rect.width;
@ -316,7 +332,7 @@ function boundMessage(_diagram, msgModel): number {
lineStartY = bounds.getVerticalPos() + totalOffset; lineStartY = bounds.getVerticalPos() + totalOffset;
} }
totalOffset += 30; totalOffset += 30;
const dx = Math.max(textWidth / 2, conf.width / 2); const dx = common.getMax(textWidth / 2, conf.width / 2);
bounds.insert( bounds.insert(
startx - dx, startx - dx,
bounds.getVerticalPos() - 10 + totalOffset, bounds.getVerticalPos() - 10 + totalOffset,
@ -347,7 +363,7 @@ function boundMessage(_diagram, msgModel): number {
const drawMessage = function (diagram, msgModel, lineStartY: number, diagObj: Diagram) { const drawMessage = function (diagram, msgModel, lineStartY: number, diagObj: Diagram) {
const { startx, stopx, starty, message, type, sequenceIndex, sequenceVisible } = msgModel; const { startx, stopx, starty, message, type, sequenceIndex, sequenceVisible } = msgModel;
const textDims = utils.calculateTextDimensions(message, messageFont(conf)); const textDims = utils.calculateTextDimensions(message, messageFont(conf));
const textObj = svgDraw.getTextObj(); const textObj = svgDrawCommon.getTextObj();
textObj.x = startx; textObj.x = startx;
textObj.y = starty + 10; textObj.y = starty + 10;
textObj.width = stopx - startx; textObj.width = stopx - startx;
@ -373,9 +389,9 @@ const drawMessage = function (diagram, msgModel, lineStartY: number, diagObj: Di
.append('path') .append('path')
.attr( .attr(
'd', 'd',
`M ${startx},${lineStartY} H ${startx + Math.max(conf.width / 2, textWidth / 2)} V ${ `M ${startx},${lineStartY} H ${
lineStartY + 25 startx + common.getMax(conf.width / 2, textWidth / 2)
} H ${startx}` } V ${lineStartY + 25} H ${startx}`
); );
} else { } else {
line = diagram line = diagram
@ -509,7 +525,7 @@ export const drawActors = function (
// Add some rendering data to the object // Add some rendering data to the object
actor.width = actor.width || conf.width; actor.width = actor.width || conf.width;
actor.height = Math.max(actor.height || conf.height, conf.height); actor.height = common.getMax(actor.height || conf.height, conf.height);
actor.margin = actor.margin || conf.actorMargin; actor.margin = actor.margin || conf.actorMargin;
actor.x = prevWidth + prevMargin; actor.x = prevWidth + prevMargin;
@ -517,7 +533,7 @@ export const drawActors = function (
// Draw the box with the attached line // Draw the box with the attached line
const height = svgDraw.drawActor(diagram, actor, conf, isFooter); const height = svgDraw.drawActor(diagram, actor, conf, isFooter);
maxHeight = Math.max(maxHeight, height); maxHeight = common.getMax(maxHeight, height);
bounds.insert(actor.x, verticalPos, actor.x + actor.width, actor.height); bounds.insert(actor.x, verticalPos, actor.x + actor.width, actor.height);
prevWidth += actor.width + prevMargin; prevWidth += actor.width + prevMargin;
@ -589,10 +605,10 @@ const activationBounds = function (actor, actors) {
const activations = actorActivations(actor); const activations = actorActivations(actor);
const left = activations.reduce(function (acc, activation) { const left = activations.reduce(function (acc, activation) {
return Math.min(acc, activation.startx); return common.getMin(acc, activation.startx);
}, actorObj.x + actorObj.width / 2); }, actorObj.x + actorObj.width / 2);
const right = activations.reduce(function (acc, activation) { const right = activations.reduce(function (acc, activation) {
return Math.max(acc, activation.stopx); return common.getMax(acc, activation.stopx);
}, actorObj.x + actorObj.width / 2); }, actorObj.x + actorObj.width / 2);
return [left, right]; return [left, right];
}; };
@ -609,7 +625,7 @@ function adjustLoopHeightForWrap(loopWidths, msg, preMargin, postMargin, addLoop
// const lines = common.splitBreaks(msg.message).length; // const lines = common.splitBreaks(msg.message).length;
const textDims = utils.calculateTextDimensions(msg.message, textConf); const textDims = utils.calculateTextDimensions(msg.message, textConf);
const totalOffset = Math.max(textDims.height, conf.labelBoxHeight); const totalOffset = common.getMax(textDims.height, conf.labelBoxHeight);
heightAdjust = postMargin + totalOffset; heightAdjust = postMargin + totalOffset;
log.debug(`${totalOffset} - ${msg.message}`); log.debug(`${totalOffset} - ${msg.message}`);
} }
@ -709,6 +725,7 @@ export const draw = function (_text: string, id: string, _version: string, diagO
switch (msg.type) { switch (msg.type) {
case diagObj.db.LINETYPE.NOTE: case diagObj.db.LINETYPE.NOTE:
bounds.resetVerticalPos();
noteModel = msg.noteModel; noteModel = msg.noteModel;
drawNote(diagram, noteModel); drawNote(diagram, noteModel);
break; break;
@ -784,6 +801,7 @@ export const draw = function (_text: string, id: string, _version: string, diagO
bounds.models.addLoop(loopModel); bounds.models.addLoop(loopModel);
break; break;
case diagObj.db.LINETYPE.PAR_START: case diagObj.db.LINETYPE.PAR_START:
case diagObj.db.LINETYPE.PAR_OVER_START:
adjustLoopHeightForWrap( adjustLoopHeightForWrap(
loopWidths, loopWidths,
msg, msg,
@ -791,6 +809,7 @@ export const draw = function (_text: string, id: string, _version: string, diagO
conf.boxMargin + conf.boxTextMargin, conf.boxMargin + conf.boxTextMargin,
(message) => bounds.newLoop(message) (message) => bounds.newLoop(message)
); );
bounds.saveVerticalPos();
break; break;
case diagObj.db.LINETYPE.PAR_AND: case diagObj.db.LINETYPE.PAR_AND:
adjustLoopHeightForWrap( adjustLoopHeightForWrap(
@ -858,6 +877,7 @@ export const draw = function (_text: string, id: string, _version: string, diagO
default: default:
try { try {
// lastMsg = msg // lastMsg = msg
bounds.resetVerticalPos();
msgModel = msg.msgModel; msgModel = msg.msgModel;
msgModel.starty = bounds.getVerticalPos(); msgModel.starty = bounds.getVerticalPos();
msgModel.sequenceIndex = sequenceIndex; msgModel.sequenceIndex = sequenceIndex;
@ -1025,45 +1045,45 @@ function getMaxMessageWidthPerActor(
* margin * margin
*/ */
if (isMessage && msg.from === actor.nextActor) { if (isMessage && msg.from === actor.nextActor) {
maxMessageWidthPerActor[msg.to] = Math.max( maxMessageWidthPerActor[msg.to] = common.getMax(
maxMessageWidthPerActor[msg.to] || 0, maxMessageWidthPerActor[msg.to] || 0,
messageWidth messageWidth
); );
} else if (isMessage && msg.from === actor.prevActor) { } else if (isMessage && msg.from === actor.prevActor) {
maxMessageWidthPerActor[msg.from] = Math.max( maxMessageWidthPerActor[msg.from] = common.getMax(
maxMessageWidthPerActor[msg.from] || 0, maxMessageWidthPerActor[msg.from] || 0,
messageWidth messageWidth
); );
} else if (isMessage && msg.from === msg.to) { } else if (isMessage && msg.from === msg.to) {
maxMessageWidthPerActor[msg.from] = Math.max( maxMessageWidthPerActor[msg.from] = common.getMax(
maxMessageWidthPerActor[msg.from] || 0, maxMessageWidthPerActor[msg.from] || 0,
messageWidth / 2 messageWidth / 2
); );
maxMessageWidthPerActor[msg.to] = Math.max( maxMessageWidthPerActor[msg.to] = common.getMax(
maxMessageWidthPerActor[msg.to] || 0, maxMessageWidthPerActor[msg.to] || 0,
messageWidth / 2 messageWidth / 2
); );
} else if (msg.placement === diagObj.db.PLACEMENT.RIGHTOF) { } else if (msg.placement === diagObj.db.PLACEMENT.RIGHTOF) {
maxMessageWidthPerActor[msg.from] = Math.max( maxMessageWidthPerActor[msg.from] = common.getMax(
maxMessageWidthPerActor[msg.from] || 0, maxMessageWidthPerActor[msg.from] || 0,
messageWidth messageWidth
); );
} else if (msg.placement === diagObj.db.PLACEMENT.LEFTOF) { } else if (msg.placement === diagObj.db.PLACEMENT.LEFTOF) {
maxMessageWidthPerActor[actor.prevActor] = Math.max( maxMessageWidthPerActor[actor.prevActor] = common.getMax(
maxMessageWidthPerActor[actor.prevActor] || 0, maxMessageWidthPerActor[actor.prevActor] || 0,
messageWidth messageWidth
); );
} else if (msg.placement === diagObj.db.PLACEMENT.OVER) { } else if (msg.placement === diagObj.db.PLACEMENT.OVER) {
if (actor.prevActor) { if (actor.prevActor) {
maxMessageWidthPerActor[actor.prevActor] = Math.max( maxMessageWidthPerActor[actor.prevActor] = common.getMax(
maxMessageWidthPerActor[actor.prevActor] || 0, maxMessageWidthPerActor[actor.prevActor] || 0,
messageWidth / 2 messageWidth / 2
); );
} }
if (actor.nextActor) { if (actor.nextActor) {
maxMessageWidthPerActor[msg.from] = Math.max( maxMessageWidthPerActor[msg.from] = common.getMax(
maxMessageWidthPerActor[msg.from] || 0, maxMessageWidthPerActor[msg.from] || 0,
messageWidth / 2 messageWidth / 2
); );
@ -1119,10 +1139,10 @@ function calculateActorMargins(
const actDims = utils.calculateTextDimensions(actor.description, actorFont(conf)); const actDims = utils.calculateTextDimensions(actor.description, actorFont(conf));
actor.width = actor.wrap actor.width = actor.wrap
? conf.width ? conf.width
: Math.max(conf.width, actDims.width + 2 * conf.wrapPadding); : common.getMax(conf.width, actDims.width + 2 * conf.wrapPadding);
actor.height = actor.wrap ? Math.max(actDims.height, conf.height) : conf.height; actor.height = actor.wrap ? common.getMax(actDims.height, conf.height) : conf.height;
maxHeight = Math.max(maxHeight, actor.height); maxHeight = common.getMax(maxHeight, actor.height);
}); });
for (const actorKey in actorToMessageWidth) { for (const actorKey in actorToMessageWidth) {
@ -1138,14 +1158,14 @@ function calculateActorMargins(
if (!nextActor) { if (!nextActor) {
const messageWidth = actorToMessageWidth[actorKey]; const messageWidth = actorToMessageWidth[actorKey];
const actorWidth = messageWidth + conf.actorMargin - actor.width / 2; const actorWidth = messageWidth + conf.actorMargin - actor.width / 2;
actor.margin = Math.max(actorWidth, conf.actorMargin); actor.margin = common.getMax(actorWidth, conf.actorMargin);
continue; continue;
} }
const messageWidth = actorToMessageWidth[actorKey]; const messageWidth = actorToMessageWidth[actorKey];
const actorWidth = messageWidth + conf.actorMargin - actor.width / 2 - nextActor.width / 2; const actorWidth = messageWidth + conf.actorMargin - actor.width / 2 - nextActor.width / 2;
actor.margin = Math.max(actorWidth, conf.actorMargin); actor.margin = common.getMax(actorWidth, conf.actorMargin);
} }
let maxBoxHeight = 0; let maxBoxHeight = 0;
@ -1161,8 +1181,8 @@ function calculateActorMargins(
} }
const boxMsgDimensions = utils.calculateTextDimensions(box.name, textFont); const boxMsgDimensions = utils.calculateTextDimensions(box.name, textFont);
maxBoxHeight = Math.max(boxMsgDimensions.height, maxBoxHeight); maxBoxHeight = common.getMax(boxMsgDimensions.height, maxBoxHeight);
const minWidth = Math.max(totalWidth, boxMsgDimensions.width + 2 * conf.wrapPadding); const minWidth = common.getMax(totalWidth, boxMsgDimensions.width + 2 * conf.wrapPadding);
box.margin = conf.boxTextMargin; box.margin = conf.boxTextMargin;
if (totalWidth < minWidth) { if (totalWidth < minWidth) {
const missing = (minWidth - totalWidth) / 2; const missing = (minWidth - totalWidth) / 2;
@ -1171,7 +1191,7 @@ function calculateActorMargins(
}); });
boxes.forEach((box) => (box.textMaxHeight = maxBoxHeight)); boxes.forEach((box) => (box.textMaxHeight = maxBoxHeight));
return Math.max(maxHeight, conf.height); return common.getMax(maxHeight, conf.height);
} }
const buildNoteModel = function (msg, actors, diagObj) { const buildNoteModel = function (msg, actors, diagObj) {
@ -1186,7 +1206,7 @@ const buildNoteModel = function (msg, actors, diagObj) {
const noteModel = { const noteModel = {
width: shouldWrap width: shouldWrap
? conf.width ? conf.width
: Math.max(conf.width, textDimensions.width + 2 * conf.noteMargin), : common.getMax(conf.width, textDimensions.width + 2 * conf.noteMargin),
height: 0, height: 0,
startx: actors[msg.from].x, startx: actors[msg.from].x,
stopx: 0, stopx: 0,
@ -1196,16 +1216,16 @@ const buildNoteModel = function (msg, actors, diagObj) {
}; };
if (msg.placement === diagObj.db.PLACEMENT.RIGHTOF) { if (msg.placement === diagObj.db.PLACEMENT.RIGHTOF) {
noteModel.width = shouldWrap noteModel.width = shouldWrap
? Math.max(conf.width, textDimensions.width) ? common.getMax(conf.width, textDimensions.width)
: Math.max( : common.getMax(
actors[msg.from].width / 2 + actors[msg.to].width / 2, actors[msg.from].width / 2 + actors[msg.to].width / 2,
textDimensions.width + 2 * conf.noteMargin textDimensions.width + 2 * conf.noteMargin
); );
noteModel.startx = startx + (actors[msg.from].width + conf.actorMargin) / 2; noteModel.startx = startx + (actors[msg.from].width + conf.actorMargin) / 2;
} else if (msg.placement === diagObj.db.PLACEMENT.LEFTOF) { } else if (msg.placement === diagObj.db.PLACEMENT.LEFTOF) {
noteModel.width = shouldWrap noteModel.width = shouldWrap
? Math.max(conf.width, textDimensions.width + 2 * conf.noteMargin) ? common.getMax(conf.width, textDimensions.width + 2 * conf.noteMargin)
: Math.max( : common.getMax(
actors[msg.from].width / 2 + actors[msg.to].width / 2, actors[msg.from].width / 2 + actors[msg.to].width / 2,
textDimensions.width + 2 * conf.noteMargin textDimensions.width + 2 * conf.noteMargin
); );
@ -1213,13 +1233,21 @@ const buildNoteModel = function (msg, actors, diagObj) {
} else if (msg.to === msg.from) { } else if (msg.to === msg.from) {
textDimensions = utils.calculateTextDimensions( textDimensions = utils.calculateTextDimensions(
shouldWrap shouldWrap
? utils.wrapLabel(msg.message, Math.max(conf.width, actors[msg.from].width), noteFont(conf)) ? utils.wrapLabel(
msg.message,
common.getMax(conf.width, actors[msg.from].width),
noteFont(conf)
)
: msg.message, : msg.message,
noteFont(conf) noteFont(conf)
); );
noteModel.width = shouldWrap noteModel.width = shouldWrap
? Math.max(conf.width, actors[msg.from].width) ? common.getMax(conf.width, actors[msg.from].width)
: Math.max(actors[msg.from].width, conf.width, textDimensions.width + 2 * conf.noteMargin); : common.getMax(
actors[msg.from].width,
conf.width,
textDimensions.width + 2 * conf.noteMargin
);
noteModel.startx = startx + (actors[msg.from].width - noteModel.width) / 2; noteModel.startx = startx + (actors[msg.from].width - noteModel.width) / 2;
} else { } else {
noteModel.width = noteModel.width =
@ -1271,14 +1299,14 @@ const buildMessageModel = function (msg, actors, diagObj) {
if (msg.wrap && msg.message) { if (msg.wrap && msg.message) {
msg.message = utils.wrapLabel( msg.message = utils.wrapLabel(
msg.message, msg.message,
Math.max(boundedWidth + 2 * conf.wrapPadding, conf.width), common.getMax(boundedWidth + 2 * conf.wrapPadding, conf.width),
messageFont(conf) messageFont(conf)
); );
} }
const msgDims = utils.calculateTextDimensions(msg.message, messageFont(conf)); const msgDims = utils.calculateTextDimensions(msg.message, messageFont(conf));
return { return {
width: Math.max( width: common.getMax(
msg.wrap ? 0 : msgDims.width + 2 * conf.wrapPadding, msg.wrap ? 0 : msgDims.width + 2 * conf.wrapPadding,
boundedWidth + 2 * conf.wrapPadding, boundedWidth + 2 * conf.wrapPadding,
conf.width conf.width
@ -1308,6 +1336,7 @@ const calculateLoopBounds = function (messages, actors, _maxWidthPerActor, diagO
case diagObj.db.LINETYPE.ALT_START: case diagObj.db.LINETYPE.ALT_START:
case diagObj.db.LINETYPE.OPT_START: case diagObj.db.LINETYPE.OPT_START:
case diagObj.db.LINETYPE.PAR_START: case diagObj.db.LINETYPE.PAR_START:
case diagObj.db.LINETYPE.PAR_OVER_START:
case diagObj.db.LINETYPE.CRITICAL_START: case diagObj.db.LINETYPE.CRITICAL_START:
case diagObj.db.LINETYPE.BREAK_START: case diagObj.db.LINETYPE.BREAK_START:
stack.push({ stack.push({
@ -1367,10 +1396,10 @@ const calculateLoopBounds = function (messages, actors, _maxWidthPerActor, diagO
msg.noteModel = noteModel; msg.noteModel = noteModel;
stack.forEach((stk) => { stack.forEach((stk) => {
current = stk; current = stk;
current.from = Math.min(current.from, noteModel.startx); current.from = common.getMin(current.from, noteModel.startx);
current.to = Math.max(current.to, noteModel.startx + noteModel.width); current.to = common.getMax(current.to, noteModel.startx + noteModel.width);
current.width = current.width =
Math.max(current.width, Math.abs(current.from - current.to)) - conf.labelBoxWidth; common.getMax(current.width, Math.abs(current.from - current.to)) - conf.labelBoxWidth;
}); });
} else { } else {
msgModel = buildMessageModel(msg, actors, diagObj); msgModel = buildMessageModel(msg, actors, diagObj);
@ -1381,18 +1410,23 @@ const calculateLoopBounds = function (messages, actors, _maxWidthPerActor, diagO
if (msgModel.startx === msgModel.stopx) { if (msgModel.startx === msgModel.stopx) {
const from = actors[msg.from]; const from = actors[msg.from];
const to = actors[msg.to]; const to = actors[msg.to];
current.from = Math.min( current.from = common.getMin(
from.x - msgModel.width / 2, from.x - msgModel.width / 2,
from.x - from.width / 2, from.x - from.width / 2,
current.from current.from
); );
current.to = Math.max(to.x + msgModel.width / 2, to.x + from.width / 2, current.to); current.to = common.getMax(
to.x + msgModel.width / 2,
to.x + from.width / 2,
current.to
);
current.width = current.width =
Math.max(current.width, Math.abs(current.to - current.from)) - conf.labelBoxWidth; common.getMax(current.width, Math.abs(current.to - current.from)) -
conf.labelBoxWidth;
} else { } else {
current.from = Math.min(msgModel.startx, current.from); current.from = common.getMin(msgModel.startx, current.from);
current.to = Math.max(msgModel.stopx, current.to); current.to = common.getMax(msgModel.stopx, current.to);
current.width = Math.max(current.width, msgModel.width) - conf.labelBoxWidth; current.width = common.getMax(current.width, msgModel.width) - conf.labelBoxWidth;
} }
}); });
} }

View File

@ -1,33 +1,13 @@
import common from '../common/common.js'; import common from '../common/common.js';
import * as svgDrawCommon from '../common/svgDrawCommon';
import { addFunction } from '../../interactionDb.js'; import { addFunction } from '../../interactionDb.js';
import { parseFontSize } from '../../utils.js'; import { parseFontSize } from '../../utils.js';
import { sanitizeUrl } from '@braintree/sanitize-url'; import { sanitizeUrl } from '@braintree/sanitize-url';
export const drawRect = function (elem, rectData) { export const drawRect = function (elem, rectData) {
const rectElem = elem.append('rect'); return svgDrawCommon.drawRect(elem, rectData);
rectElem.attr('x', rectData.x);
rectElem.attr('y', rectData.y);
rectElem.attr('fill', rectData.fill);
rectElem.attr('stroke', rectData.stroke);
rectElem.attr('width', rectData.width);
rectElem.attr('height', rectData.height);
rectElem.attr('rx', rectData.rx);
rectElem.attr('ry', rectData.ry);
if (rectData.class !== undefined) {
rectElem.attr('class', rectData.class);
}
return rectElem;
}; };
// const sanitizeUrl = function (s) {
// return s
// .replace(/&/g, '&amp;')
// .replace(/</g, '&lt;')
// .replace(/javascript:/g, '');
// };
const addPopupInteraction = (id, actorCnt) => { const addPopupInteraction = (id, actorCnt) => {
addFunction(() => { addFunction(() => {
const arr = document.querySelectorAll(id); const arr = document.querySelectorAll(id);
@ -43,6 +23,7 @@ const addPopupInteraction = (id, actorCnt) => {
}); });
}); });
}; };
export const drawPopup = function (elem, actor, minMenuWidth, textAttrs, forceMenus) { export const drawPopup = function (elem, actor, minMenuWidth, textAttrs, forceMenus) {
if (actor.links === undefined || actor.links === null || Object.keys(actor.links).length === 0) { if (actor.links === undefined || actor.links === null || Object.keys(actor.links).length === 0) {
return { height: 0, width: 0 }; return { height: 0, width: 0 };
@ -107,22 +88,6 @@ export const drawPopup = function (elem, actor, minMenuWidth, textAttrs, forceMe
return { height: rectData.height + linkY, width: menuWidth }; return { height: rectData.height + linkY, width: menuWidth };
}; };
export const drawImage = function (elem, x, y, link) {
const imageElem = elem.append('image');
imageElem.attr('x', x);
imageElem.attr('y', y);
var sanitizedLink = sanitizeUrl(link);
imageElem.attr('xlink:href', sanitizedLink);
};
export const drawEmbeddedImage = function (elem, x, y, link) {
const imageElem = elem.append('use');
imageElem.attr('x', x);
imageElem.attr('y', y);
var sanitizedLink = sanitizeUrl(link);
imageElem.attr('xlink:href', '#' + sanitizedLink);
};
export const popupMenu = function (popid) { export const popupMenu = function (popid) {
return ( return (
"var pu = document.getElementById('" + "var pu = document.getElementById('" +
@ -152,9 +117,10 @@ const popupMenuDownFunc = function (popupId) {
pu.style.display = 'none'; pu.style.display = 'none';
} }
}; };
export const drawText = function (elem, textData) { export const drawText = function (elem, textData) {
let prevTextHeight = 0, let prevTextHeight = 0;
textHeight = 0; let textHeight = 0;
const lines = textData.text.split(common.lineBreakRegex); const lines = textData.text.split(common.lineBreakRegex);
const [_textFontSize, _textFontSizePx] = parseFontSize(textData.fontSize); const [_textFontSize, _textFontSizePx] = parseFontSize(textData.fontSize);
@ -188,6 +154,7 @@ export const drawText = function (elem, textData) {
break; break;
} }
} }
if ( if (
textData.anchor !== undefined && textData.anchor !== undefined &&
textData.textMargin !== undefined && textData.textMargin !== undefined &&
@ -217,6 +184,7 @@ export const drawText = function (elem, textData) {
break; break;
} }
} }
for (let [i, line] of lines.entries()) { for (let [i, line] of lines.entries()) {
if ( if (
textData.textMargin !== undefined && textData.textMargin !== undefined &&
@ -371,7 +339,7 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) {
} }
} }
const rect = getNoteRect(); const rect = svgDrawCommon.getNoteRect();
var cssclass = 'actor'; var cssclass = 'actor';
if (actor.properties != null && actor.properties['class']) { if (actor.properties != null && actor.properties['class']) {
cssclass = actor.properties['class']; cssclass = actor.properties['class'];
@ -391,9 +359,9 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) {
if (actor.properties != null && actor.properties['icon']) { if (actor.properties != null && actor.properties['icon']) {
const iconSrc = actor.properties['icon'].trim(); const iconSrc = actor.properties['icon'].trim();
if (iconSrc.charAt(0) === '@') { if (iconSrc.charAt(0) === '@') {
drawEmbeddedImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc.substr(1)); svgDrawCommon.drawEmbeddedImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc.substr(1));
} else { } else {
drawImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc); svgDrawCommon.drawImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc);
} }
} }
@ -438,7 +406,7 @@ const drawActorTypeActor = function (elem, actor, conf, isFooter) {
const actElem = elem.append('g'); const actElem = elem.append('g');
actElem.attr('class', 'actor-man'); actElem.attr('class', 'actor-man');
const rect = getNoteRect(); const rect = svgDrawCommon.getNoteRect();
rect.x = actor.x; rect.x = actor.x;
rect.y = actor.y; rect.y = actor.y;
rect.fill = '#eaeaea'; rect.fill = '#eaeaea';
@ -447,7 +415,6 @@ const drawActorTypeActor = function (elem, actor, conf, isFooter) {
rect.class = 'actor'; rect.class = 'actor';
rect.rx = 3; rect.rx = 3;
rect.ry = 3; rect.ry = 3;
// drawRect(actElem, rect);
actElem actElem
.append('line') .append('line')
@ -532,6 +499,7 @@ export const drawBox = function (elem, box, conf) {
export const anchorElement = function (elem) { export const anchorElement = function (elem) {
return elem.append('g'); return elem.append('g');
}; };
/** /**
* Draws an activation in the diagram * Draws an activation in the diagram
* *
@ -542,7 +510,7 @@ export const anchorElement = function (elem) {
* @param {any} actorActivations - Number of activations on the actor. * @param {any} actorActivations - Number of activations on the actor.
*/ */
export const drawActivation = function (elem, bounds, verticalPos, conf, actorActivations) { export const drawActivation = function (elem, bounds, verticalPos, conf, actorActivations) {
const rect = getNoteRect(); const rect = svgDrawCommon.getNoteRect();
const g = bounds.anchored; const g = bounds.anchored;
rect.x = bounds.startx; rect.x = bounds.startx;
rect.y = bounds.starty; rect.y = bounds.starty;
@ -594,7 +562,7 @@ export const drawLoop = function (elem, loopModel, labelText, conf) {
}); });
} }
let txt = getTextObj(); let txt = svgDrawCommon.getTextObj();
txt.text = labelText; txt.text = labelText;
txt.x = loopModel.startx; txt.x = loopModel.startx;
txt.y = loopModel.starty; txt.y = loopModel.starty;
@ -610,7 +578,7 @@ export const drawLoop = function (elem, loopModel, labelText, conf) {
txt.class = 'labelText'; txt.class = 'labelText';
drawLabel(g, txt); drawLabel(g, txt);
txt = getTextObj(); txt = svgDrawCommon.getTextObj();
txt.text = loopModel.title; txt.text = loopModel.title;
txt.x = loopModel.startx + labelBoxWidth / 2 + (loopModel.stopx - loopModel.startx) / 2; txt.x = loopModel.startx + labelBoxWidth / 2 + (loopModel.stopx - loopModel.startx) / 2;
txt.y = loopModel.starty + boxMargin + boxTextMargin; txt.y = loopModel.starty + boxMargin + boxTextMargin;
@ -661,16 +629,7 @@ export const drawLoop = function (elem, loopModel, labelText, conf) {
* @param {any} bounds Shape of the rectangle * @param {any} bounds Shape of the rectangle
*/ */
export const drawBackgroundRect = function (elem, bounds) { export const drawBackgroundRect = function (elem, bounds) {
const rectElem = drawRect(elem, { svgDrawCommon.drawBackgroundRect(elem, bounds);
x: bounds.startx,
y: bounds.starty,
width: bounds.stopx - bounds.startx,
height: bounds.stopy - bounds.starty,
fill: bounds.fill,
stroke: bounds.stroke,
class: 'rect',
});
rectElem.lower();
}; };
export const insertDatabaseIcon = function (elem) { export const insertDatabaseIcon = function (elem) {
@ -737,6 +696,7 @@ export const insertArrowHead = function (elem) {
.append('path') .append('path')
.attr('d', 'M 0 0 L 10 5 L 0 10 z'); // this is actual shape for arrowhead .attr('d', 'M 0 0 L 10 5 L 0 10 z'); // this is actual shape for arrowhead
}; };
/** /**
* Setup arrow head and define the marker. The result is appended to the svg. * Setup arrow head and define the marker. The result is appended to the svg.
* *
@ -755,6 +715,7 @@ export const insertArrowFilledHead = function (elem) {
.append('path') .append('path')
.attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z'); .attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z');
}; };
/** /**
* Setup node number. The result is appended to the svg. * Setup node number. The result is appended to the svg.
* *
@ -776,6 +737,7 @@ export const insertSequenceNumber = function (elem) {
.attr('r', 6); .attr('r', 6);
// .style("fill", '#f00'); // .style("fill", '#f00');
}; };
/** /**
* Setup cross head and define the marker. The result is appended to the svg. * Setup cross head and define the marker. The result is appended to the svg.
* *
@ -802,37 +764,6 @@ export const insertArrowCrossHead = function (elem) {
// this is actual shape for arrowhead // this is actual shape for arrowhead
}; };
export const getTextObj = function () {
return {
x: 0,
y: 0,
fill: undefined,
anchor: undefined,
style: '#666',
width: undefined,
height: undefined,
textMargin: 0,
rx: 0,
ry: 0,
tspan: true,
valign: undefined,
};
};
export const getNoteRect = function () {
return {
x: 0,
y: 0,
fill: '#EDF2AE',
stroke: '#666',
width: 100,
anchor: 'start',
height: 100,
rx: 0,
ry: 0,
};
};
const _drawTextCandidateFunc = (function () { const _drawTextCandidateFunc = (function () {
/** /**
* @param {any} content * @param {any} content
@ -1062,8 +993,6 @@ export default {
drawActor, drawActor,
drawBox, drawBox,
drawPopup, drawPopup,
drawImage,
drawEmbeddedImage,
anchorElement, anchorElement,
drawActivation, drawActivation,
drawLoop, drawLoop,
@ -1075,8 +1004,6 @@ export default {
insertDatabaseIcon, insertDatabaseIcon,
insertComputerIcon, insertComputerIcon,
insertClockIcon, insertClockIcon,
getTextObj,
getNoteRect,
popupMenu, popupMenu,
popdownMenu, popdownMenu,
fixLifeLineHeights, fixLifeLineHeights,

View File

@ -174,16 +174,4 @@ describe('svgDraw', function () {
expect(rect.lower).toHaveBeenCalled(); expect(rect.lower).toHaveBeenCalled();
}); });
}); });
describe('sanitizeUrl', function () {
it('should sanitize malicious urls', function () {
const maliciousStr = 'javascript:script:alert(1)';
const result = svgDraw.sanitizeUrl(maliciousStr);
expect(result).not.toContain('javascript:alert(1)');
});
it('should not sanitize non dangerous urls', function () {
const maliciousStr = 'javajavascript:script:alert(1)';
const result = svgDraw.sanitizeUrl(maliciousStr);
expect(result).not.toContain('javascript:alert(1)');
});
});
}); });

View File

@ -1,21 +1,8 @@
import { arc as d3arc } from 'd3'; import { arc as d3arc } from 'd3';
import * as svgDrawCommon from '../common/svgDrawCommon';
export const drawRect = function (elem, rectData) { export const drawRect = function (elem, rectData) {
const rectElem = elem.append('rect'); return svgDrawCommon.drawRect(elem, rectData);
rectElem.attr('x', rectData.x);
rectElem.attr('y', rectData.y);
rectElem.attr('fill', rectData.fill);
rectElem.attr('stroke', rectData.stroke);
rectElem.attr('width', rectData.width);
rectElem.attr('height', rectData.height);
rectElem.attr('rx', rectData.rx);
rectElem.attr('ry', rectData.ry);
if (rectData.class !== undefined) {
rectElem.attr('class', rectData.class);
}
return rectElem;
}; };
export const drawFace = function (element, faceData) { export const drawFace = function (element, faceData) {
@ -128,25 +115,7 @@ export const drawCircle = function (element, circleData) {
}; };
export const drawText = function (elem, textData) { export const drawText = function (elem, textData) {
// Remove and ignore br:s return svgDrawCommon.drawText(elem, textData);
const nText = textData.text.replace(/<br\s*\/?>/gi, ' ');
const textElem = elem.append('text');
textElem.attr('x', textData.x);
textElem.attr('y', textData.y);
textElem.attr('class', 'legend');
textElem.style('text-anchor', textData.anchor);
if (textData.class !== undefined) {
textElem.attr('class', textData.class);
}
const span = textElem.append('tspan');
span.attr('x', textData.x + textData.textMargin * 2);
span.text(nText);
return textElem;
}; };
export const drawLabel = function (elem, txtObject) { export const drawLabel = function (elem, txtObject) {
@ -192,7 +161,7 @@ export const drawLabel = function (elem, txtObject) {
export const drawSection = function (elem, section, conf) { export const drawSection = function (elem, section, conf) {
const g = elem.append('g'); const g = elem.append('g');
const rect = getNoteRect(); const rect = svgDrawCommon.getNoteRect();
rect.x = section.x; rect.x = section.x;
rect.y = section.y; rect.y = section.y;
rect.fill = section.fill; rect.fill = section.fill;
@ -249,7 +218,7 @@ export const drawTask = function (elem, task, conf) {
score: task.score, score: task.score,
}); });
const rect = getNoteRect(); const rect = svgDrawCommon.getNoteRect();
rect.x = task.x; rect.x = task.x;
rect.y = task.y; rect.y = task.y;
rect.fill = task.fill; rect.fill = task.fill;
@ -298,41 +267,7 @@ export const drawTask = function (elem, task, conf) {
* @param {any} bounds The bounds of the drawing * @param {any} bounds The bounds of the drawing
*/ */
export const drawBackgroundRect = function (elem, bounds) { export const drawBackgroundRect = function (elem, bounds) {
const rectElem = drawRect(elem, { svgDrawCommon.drawBackgroundRect(elem, bounds);
x: bounds.startx,
y: bounds.starty,
width: bounds.stopx - bounds.startx,
height: bounds.stopy - bounds.starty,
fill: bounds.fill,
class: 'rect',
});
rectElem.lower();
};
export const getTextObj = function () {
return {
x: 0,
y: 0,
fill: undefined,
'text-anchor': 'start',
width: 100,
height: 100,
textMargin: 0,
rx: 0,
ry: 0,
};
};
export const getNoteRect = function () {
return {
x: 0,
y: 0,
width: 100,
anchor: 'start',
height: 100,
rx: 0,
ry: 0,
};
}; };
const _drawTextCandidateFunc = (function () { const _drawTextCandidateFunc = (function () {
@ -475,7 +410,5 @@ export default {
drawLabel, drawLabel,
drawTask, drawTask,
drawBackgroundRect, drawBackgroundRect,
getTextObj,
getNoteRect,
initGraphics, initGraphics,
}; };

View File

@ -345,10 +345,10 @@ mermaid.parseError = function (err, hash) {
displayErrorInGui(err); displayErrorInGui(err);
}; };
const textFieldUpdated = function () { const textFieldUpdated = async function () {
const textStr = getTextFromFormField('code'); const textStr = getTextFromFormField('code');
if (mermaid.parse(textStr)) { if (await mermaid.parse(textStr)) {
reRender(textStr); reRender(textStr);
} }
}; };

View File

@ -141,8 +141,12 @@ They also serve as proof of concept, for the variety of things that can be built
- [Sphinx](https://www.sphinx-doc.org/en/master/) - [Sphinx](https://www.sphinx-doc.org/en/master/)
- [sphinxcontrib-mermaid](https://github.com/mgaitan/sphinxcontrib-mermaid) - [sphinxcontrib-mermaid](https://github.com/mgaitan/sphinxcontrib-mermaid)
- [remark.js](https://remark.js.org/) - [remark](https://remark.js.org/)
- [remark-mermaid](https://github.com/temando/remark-mermaid) - [remark-mermaidjs](https://github.com/remcohaszing/remark-mermaidjs)
- [rehype](https://github.com/rehypejs/rehype)
- [rehype-mermaidjs](https://github.com/remcohaszing/rehype-mermaidjs)
- [Gatsby](https://www.gatsbyjs.com/)
- [gatsby-remark-mermaid](https://github.com/remcohaszing/gatsby-remark-mermaid)
- [jSDoc](https://jsdoc.app/) - [jSDoc](https://jsdoc.app/)
- [jsdoc-mermaid](https://github.com/Jellyvision/jsdoc-mermaid) - [jsdoc-mermaid](https://github.com/Jellyvision/jsdoc-mermaid)
- [MkDocs](https://www.mkdocs.org) - [MkDocs](https://www.mkdocs.org)
@ -183,6 +187,7 @@ They also serve as proof of concept, for the variety of things that can be built
- [bisheng-plugin-mermaid](https://github.com/yct21/bisheng-plugin-mermaid) - [bisheng-plugin-mermaid](https://github.com/yct21/bisheng-plugin-mermaid)
- [Reveal CK](https://github.com/jedcn/reveal-ck) - [Reveal CK](https://github.com/jedcn/reveal-ck)
- [reveal-ck-mermaid-plugin](https://github.com/tmtm/reveal-ck-mermaid-plugin) - [reveal-ck-mermaid-plugin](https://github.com/tmtm/reveal-ck-mermaid-plugin)
- [mermaid-isomorphic](https://github.com/remcohaszing/mermaid-isomorphic)
- [mermaid-server: Generate diagrams using a HTTP request](https://github.com/TomWright/mermaid-server) - [mermaid-server: Generate diagrams using a HTTP request](https://github.com/TomWright/mermaid-server)
- [ExDoc](https://github.com/elixir-lang/ex_doc) - [ExDoc](https://github.com/elixir-lang/ex_doc)
- [Rendering Mermaid graphs](https://github.com/elixir-lang/ex_doc#rendering-mermaid-graphs) - [Rendering Mermaid graphs](https://github.com/elixir-lang/ex_doc#rendering-mermaid-graphs)

View File

@ -277,6 +277,23 @@ And `Link` can be one of:
| -- | Solid | | -- | Solid |
| .. | Dashed | | .. | Dashed |
## Define Namespace
A namespace groups classes.
Code:
```mermaid-example
classDiagram
namespace BaseShapes {
class Triangle
class Rectangle {
double width
double height
}
}
```
## Cardinality / Multiplicity on relations ## Cardinality / Multiplicity on relations
Multiplicity or cardinality in class diagrams indicates the number of instances of one class that can be linked to an instance of the other class. For example, each company will have one or more employees (not zero), and each employee currently works for zero or one companies. Multiplicity or cardinality in class diagrams indicates the number of instances of one class that can be linked to an instance of the other class. For example, each company will have one or more employees (not zero), and each employee currently works for zero or one companies.