Merge pull request #11 from mermaid-js/develop

sync
This commit is contained in:
Justin Greywolf
2020-01-06 10:08:43 -08:00
committed by GitHub
9 changed files with 221 additions and 73 deletions

View File

@@ -14,6 +14,8 @@ describe('Class diagram', () => {
Class09 --> C2 : Where am i?
Class09 --* C3
Class09 --|> Class07
Class12 <|.. Class08
Class11 ..>Class12
Class07 : equals()
Class07 : Object[] elementData
Class01 : size()
@@ -29,7 +31,7 @@ describe('Class diagram', () => {
test()
}
`,
{}
{logLevel : 1}
);
cy.get('svg');
});

View File

@@ -169,22 +169,26 @@ A relationship is a general term covering the specific types of logical connecti
There are different types of relations defined for classes under UML which are currently supported:
Type | Description
--- | ---
<\|--| Inheritance
*-- | Composition
o-- | Aggregation
--> | Association
-- | Link
--- | ---
<\|-- | Inheritance
*-- | Composition
o-- | Aggregation
--> | Association
-- | Link (Solid)
..> | Dependency
..\|> | Realization
.. | Link (Dashed)
<!--- TODO ..> Dependency--->
```
classDiagram
classA <|-- classB
classC *-- classD
classE o-- classF
classG <-- classH
classI <.. classJ
classK .. classL
classI -- classJ
classK <.. classL
classM <|.. classN
classO .. classP
```
@@ -194,30 +198,36 @@ classA <|-- classB
classC *-- classD
classE o-- classF
classG <-- classH
classI <.. classJ
classK .. classL
classI -- classJ
classK <.. classL
classM <|.. classN
classO .. classP
```
We can use the arrowheads in opposite directions as well :
We can use the labels to describe nature of relation between two classes. Also, arrowheads can be used in opposite directions as well :
```
classDiagram
classA --|> classB
classC --* classD
classE --o classF
classG <--> classH
classI ..> classJ
classK .. classL
classA --|> classB : Inheritance
classC --* classD : Composition
classE --o classF : Aggregation
classG --> classH : Association
classI -- classJ : Link(Solid)
classK ..> classL : Dependency
classM ..|> classN : Realization
classO .. classP : Link(Dashed)
```
```mermaid
classDiagram
classA --|> classB
classC --* classD
classE --o classF
classG <--> classH
classI ..> classJ
classK .. classL
classA --|> classB : Inheritance
classC --* classD : Composition
classE --o classF : Aggregation
classG --> classH : Association
classI -- classJ : Link(Solid)
classK ..> classL : Dependency
classM ..|> classN : Realization
classO .. classP : Link(Dashed)
```
@@ -345,6 +355,101 @@ class Shape{
```
## Interaction
It is possible to bind a click event to a node, the click can lead to either a javascript callback or to a link which will be opened in a new browser tab. **Note**: This functionality is disabled when using `securityLevel='strict'` and enabled when using `securityLevel='loose'`.
You would define these actions on a separate line after all classes have been declared.
```
action className "reference" "tooltip"
```
* _action_ is either `link` or `callback`, depending on which type of interaction you want to have called
* _className_ is the id of the node that the action will be associated with
* _reference_ is either the url link, or the function name for callback. (note: callback function will be called with the nodeId as parameter).
* (_optional_) tooltip is a string to be displayed when hovering over element (note: The styles of the tooltip are set by the class .mermaidTooltip.)
### Examples:
*URL Link:*
```
classDiagram
class Shape
link Shape "http://www.github.com" "This is a tooltip for a link"
```
*Callback:*
```
classDiagram
class Shape
callback Shape "callbackFunction" "This is a tooltip for a callback"
```
```
<script>
var callbackFunction = function(){
alert('A callback was triggered');
}
<script>
```
```mermaid
classDiagram
class Class01
class Class02
callback Class01 "callbackFunction" "Callback tooltip"
link Class02 "http://www.github.com" "This is a link"
```
> **Success** The tooltip functionality and the ability to link to urls are available from version 0.5.2.
Beginners tip, a full example using interactive links in an html context:
```
<body>
<div class="mermaid">
classDiagram
Animal <|-- Duck
Animal <|-- Fish
Animal <|-- Zebra
Animal : +int age
Animal : +String gender
Animal: +isMammal()
Animal: +mate()
class Duck{
+String beakColor
+swim()
+quack()
}
class Fish{
-int sizeInFeet
-canEat()
}
class Zebra{
+bool is_wild
+run()
}
callback Duck callback "Tooltip"
click Zebra "http://www.github.com" "This is a link"
</div>
<script>
var callback = function(){
alert('A callback was triggered');
}
var config = {
startOnLoad:true,
securityLevel:'loose',
};
mermaid.initialize(config);
</script>
</body>
```
## Styling
Styling of the class diagram is done by defining a number of css classes. During rendering these classes are extracted from the file located at src/themes/class.scss

View File

@@ -1,9 +1,9 @@
import * as d3 from 'd3';
import { sanitizeUrl } from '@braintree/sanitize-url';
import { logger } from '../../logger';
import { getConfig } from '../../config';
import utils from '../../utils';
const MERMAID_DOM_ID_PREFIX = '';
const MERMAID_DOM_ID_PREFIX = 'classid-';
const config = getConfig();
@@ -155,14 +155,10 @@ export const setLink = function(ids, linkStr, tooltip) {
let id = _id;
if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id;
if (typeof classes[id] !== 'undefined') {
if (config.securityLevel !== 'loose') {
classes[id].link = sanitizeUrl(linkStr);
} else {
classes[id].link = linkStr;
}
classes[id].link = utils.formatUrl(linkStr, config);
if (tooltip) {
classes[id].tooltip = tooltip;
classes[id].tooltip = utils.sanitize(tooltip, config);
}
}
});
@@ -182,9 +178,10 @@ export const setClickEvent = function(ids, functionName, tooltip) {
setCssClass(ids, 'clickable');
};
const setClickFunc = function(_id, functionName) {
const setClickFunc = function(_id, functionName, tooltip) {
let id = _id;
if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id;
let elemId = MERMAID_DOM_ID_PREFIX + id;
if (config.securityLevel !== 'loose') {
return;
}
@@ -192,14 +189,17 @@ const setClickFunc = function(_id, functionName) {
return;
}
if (typeof classes[id] !== 'undefined') {
if (tooltip) {
classes[id].tooltip = utils.sanitize(tooltip, config);
}
funs.push(function() {
const elem = document.querySelector(`[id="${id}"]`);
const elem = document.querySelector(`[id="${elemId}"]`);
if (elem !== null) {
elem.setAttribute('title', classes[id].tooltip);
elem.addEventListener(
'click',
function() {
window[functionName](id);
window[functionName](elemId);
},
false
);

View File

@@ -310,6 +310,16 @@ describe('class diagram, ', function () {
parser.parse(str);
});
it('should handle dashed relation definition of different types and directions', function () {
const str =
'classDiagram\n' +
'Class11 <|.. Class12\n' +
'Class13 <.. Class14\n' +
'Class15 ..|> Class16\n' +
'Class17 ..> Class18\n' +
'Class19 .. Class20';
parser.parse(str);
});
});
describe('when fetching data from a classDiagram graph it', function () {

View File

@@ -8,9 +8,9 @@ import { parser } from './parser/classDiagram';
parser.yy = classDb;
const MERMAID_DOM_ID_PREFIX = 'classid-';
let idCache = {};
let classCnt = 0;
const conf = {
dividerMargin: 10,
padding: 5,
@@ -183,6 +183,9 @@ const drawEdge = function(elem, path, relation) {
url = url.replace(/\)/g, '\\)');
}
if (relation.relation.lineType == 1) {
svgPath.attr('class', 'relation dashed-line');
}
if (relation.relation.type1 !== 'none') {
svgPath.attr(
'marker-start',
@@ -319,7 +322,7 @@ const drawClass = function(elem, classDef) {
}
};
const id = 'classId' + classCnt;
const id = MERMAID_DOM_ID_PREFIX + classDef.id;
const classInfo = {
id: id,
label: classDef.id,
@@ -339,8 +342,7 @@ const drawClass = function(elem, classDef) {
title = g
.append('svg:a')
.attr('xlink:href', classDef.link)
.attr('xlink:target', '_blank')
.attr('xlink:title', classDef.tooltip)
.attr('target', '_blank')
.append('text')
.attr('y', conf.textHeight + conf.padding)
.attr('x', 0);
@@ -432,6 +434,10 @@ const drawClass = function(elem, classDef) {
x.setAttribute('x', (rectWidth - x.getBBox().width) / 2);
});
if (classDef.tooltip) {
title.insert('title').text(classDef.tooltip);
}
membersLine.attr('x2', rectWidth);
methodsLine.attr('x2', rectWidth);
@@ -439,7 +445,6 @@ const drawClass = function(elem, classDef) {
classInfo.height = classBox.height + conf.padding + 0.5 * conf.dividerMargin;
idCache[id] = classInfo;
classCnt++;
return classInfo;
};

View File

@@ -1,5 +1,4 @@
import * as d3 from 'd3';
import { sanitizeUrl } from '@braintree/sanitize-url';
import { logger } from '../../logger';
import utils from '../../utils';
import { getConfig } from '../../config';
@@ -20,25 +19,6 @@ let direction;
// Functions to be run after graph rendering
let funs = [];
const sanitize = text => {
let txt = text;
let htmlLabels = true;
if (
config.flowchart &&
(config.flowchart.htmlLabels === false || config.flowchart.htmlLabels === 'false')
)
htmlLabels = false;
if (config.securityLevel !== 'loose' && htmlLabels) { // eslint-disable-line
txt = txt.replace(/<br>/g, '#br#');
txt = txt.replace(/<br\S*?\/>/g, '#br#');
txt = txt.replace(/</g, '&lt;').replace(/>/g, '&gt;');
txt = txt.replace(/=/g, '&equals;');
txt = txt.replace(/#br#/g, '<br/>');
}
return txt;
};
/**
* Function called by parser when a node definition has been found
* @param id
@@ -63,7 +43,7 @@ export const addVertex = function(_id, text, type, style, classes) {
vertices[id] = { id: id, styles: [], classes: [] };
}
if (typeof text !== 'undefined') {
txt = sanitize(text.trim());
txt = utils.sanitize(text.trim(), config);
// strip quotes if string starts and ends with a quote
if (txt[0] === '"' && txt[txt.length - 1] === '"') {
@@ -113,7 +93,7 @@ export const addSingleLink = function(_start, _end, type, linktext) {
linktext = type.text;
if (typeof linktext !== 'undefined') {
edge.text = sanitize(linktext.trim());
edge.text = utils.sanitize(linktext.trim(), config);
// strip quotes if string starts and exnds with a quote
if (edge.text[0] === '"' && edge.text[edge.text.length - 1] === '"') {
@@ -225,7 +205,7 @@ export const setClass = function(ids, className) {
const setTooltip = function(ids, tooltip) {
ids.split(',').forEach(function(id) {
if (typeof tooltip !== 'undefined') {
tooltips[id] = sanitize(tooltip);
tooltips[id] = utils.sanitize(tooltip, config);
}
});
};
@@ -266,11 +246,7 @@ export const setLink = function(ids, linkStr, tooltip) {
let id = _id;
if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id;
if (typeof vertices[id] !== 'undefined') {
if (config.securityLevel !== 'loose') {
vertices[id].link = sanitizeUrl(linkStr); // .replace(/javascript:.*/g, '')
} else {
vertices[id].link = linkStr;
}
vertices[id].link = utils.formatUrl(linkStr, config);
}
});
setTooltip(ids, tooltip);
@@ -429,7 +405,7 @@ export const addSubGraph = function(_id, list, _title) {
id = id || 'subGraph' + subCount;
if (id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id;
title = title || '';
title = sanitize(title);
title = utils.sanitize(title, config);
subCount = subCount + 1;
const subGraph = { id: id, nodes: nodeList, title: title.trim(), classes: [] };
subGraphs.push(subGraph);

View File

@@ -613,6 +613,9 @@ const render = function(id, txt, cb, container) {
case 'gantt':
cb(svgCode, ganttDb.bindFunctions);
break;
case 'class':
cb(svgCode, classDb.bindFunctions);
break;
default:
cb(svgCode);
}

View File

@@ -10,6 +10,10 @@ g.classGroup text {
}
}
g.clickable {
cursor: pointer;
}
g.classGroup rect {
fill: $nodeBkg;
stroke: $nodeBorder;
@@ -38,6 +42,10 @@ g.classGroup line {
fill: none;
}
.dashed-line{
stroke-dasharray: 3;
}
@mixin composition {
fill: $nodeBorder;
stroke: $nodeBorder;

View File

@@ -1,5 +1,6 @@
import * as d3 from 'd3';
import { logger } from './logger';
import { sanitizeUrl } from '@braintree/sanitize-url';
/**
* @function detectType
@@ -73,6 +74,42 @@ export const interpolateToCurve = (interpolate, defaultCurve) => {
return d3[curveName] || defaultCurve;
};
export const sanitize = (text, config) => {
let txt = text;
let htmlLabels = true;
if (
config.flowchart &&
(config.flowchart.htmlLabels === false || config.flowchart.htmlLabels === 'false')
)
htmlLabels = false;
if (config.securityLevel !== 'loose' && htmlLabels) { // eslint-disable-line
txt = txt.replace(/<br>/g, '#br#');
txt = txt.replace(/<br\S*?\/>/g, '#br#');
txt = txt.replace(/</g, '&lt;').replace(/>/g, '&gt;');
txt = txt.replace(/=/g, '&equals;');
txt = txt.replace(/#br#/g, '<br/>');
}
return txt;
};
export const formatUrl = (linkStr, config) => {
let url = linkStr.trim();
if (url) {
if (config.securityLevel !== 'loose') {
return sanitizeUrl(url);
} else {
if (!/^(https?:)?\/\//i.test(url)) {
url = 'http://' + url;
}
}
return url;
}
};
const distance = (p1, p2) =>
p1 && p2 ? Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)) : 0;
@@ -174,5 +211,7 @@ export default {
isSubstringInArray,
interpolateToCurve,
calcLabelPosition,
calcCardinalityPosition
calcCardinalityPosition,
sanitize,
formatUrl
};