mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-10-23 16:04:12 +02:00
7
.gitignore
vendored
7
.gitignore
vendored
@@ -12,3 +12,10 @@ yarn-error.log
|
||||
token
|
||||
|
||||
package-lock.json
|
||||
|
||||
dist/classTest.html
|
||||
|
||||
dist/sequenceTest.html
|
||||
|
||||
.vscode/
|
||||
cypress/platform/current.html
|
@@ -289,4 +289,20 @@ describe('Class diagram', () => {
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
|
||||
it('12: should render a simple class diagram with generic types', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
classDiagram
|
||||
class Class10~T~ {
|
||||
int[] id
|
||||
List~int~ ids
|
||||
test(List~int~ ids) List~bool~
|
||||
testArray() bool[]
|
||||
}
|
||||
`,
|
||||
{}
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
});
|
||||
|
@@ -512,7 +512,7 @@ describe('Flowchart', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('24: Keep node label text (if already defined) when a style is applied', () => {
|
||||
it('24.1: Keep node label text (if already defined) when a style is applied', () => {
|
||||
imgSnapshotTest(
|
||||
`graph LR
|
||||
A(( )) -->|step 1| B(( ))
|
||||
@@ -524,4 +524,55 @@ describe('Flowchart', () => {
|
||||
{ flowchart: { htmlLabels: false } }
|
||||
);
|
||||
});
|
||||
it('24.2: Handle link click events (link, anchor, mailto, other protocol, script)', () => {
|
||||
imgSnapshotTest(
|
||||
`graph TB
|
||||
TITLE["Link Click Events<br>(click the nodes below)"]
|
||||
A[link test]
|
||||
B[anchor test]
|
||||
C[mailto test]
|
||||
D[other protocol test]
|
||||
E[script test]
|
||||
TITLE --> A & B & C & D & E
|
||||
click A "https://mermaid-js.github.io/mermaid/#/" "link test"
|
||||
click B "#link-clicked" "anchor test"
|
||||
click C "mailto:user@user.user" "mailto test"
|
||||
click D "notes://do-your-thing/id" "other protocol test"
|
||||
click E "javascript:alert('test')" "script test"
|
||||
`,
|
||||
{ securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
|
||||
it('25: Set node text color according to style when html labels are enabled', () => {
|
||||
imgSnapshotTest(
|
||||
`graph LR
|
||||
A[red<br>text] --> B(blue<br>text)
|
||||
C[/red<br/>text/] --> D{blue<br/>text}
|
||||
style A color:red;
|
||||
style B color:blue;
|
||||
style C stroke:#ff0000,fill:#ffcccc,color:#ff0000
|
||||
style D stroke:#0000ff,fill:#ccccff,color:#0000ff
|
||||
click B "index.html#link-clicked" "link test"
|
||||
click D testClick "click test"
|
||||
`,
|
||||
{ flowchart: { htmlLabels: true } }
|
||||
);
|
||||
});
|
||||
|
||||
it('26: Set node text color according to style when html labels are disabled', () => {
|
||||
imgSnapshotTest(
|
||||
`graph LR
|
||||
A[red<br>text] --> B(blue<br>text)
|
||||
C[/red<br/>text/] --> D{blue<br/>text}
|
||||
style A color:red;
|
||||
style B color:blue;
|
||||
style C stroke:#ff0000,fill:#ffcccc,color:#ff0000
|
||||
style D stroke:#0000ff,fill:#ccccff,color:#0000ff
|
||||
click B "index.html#link-clicked" "link test"
|
||||
click D testClick "click test"
|
||||
`,
|
||||
{ flowchart: { htmlLabels: false } }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@@ -319,7 +319,7 @@ describe('State diagram', () => {
|
||||
}
|
||||
);
|
||||
});
|
||||
it('Simplest compone state', () => {
|
||||
it('Simplest composit state', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
stateDiagram
|
||||
@@ -332,5 +332,17 @@ describe('State diagram', () => {
|
||||
}
|
||||
);
|
||||
});
|
||||
it('should handle multiple arrows from one node to another', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
stateDiagram
|
||||
a --> b: Start
|
||||
a --> b: Stop
|
||||
`,
|
||||
{
|
||||
logLevel: 0,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -5,33 +5,28 @@
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
body {background: black}
|
||||
body {background: white}
|
||||
h1 { color: white;}
|
||||
.arrowheadPath {fill: red;}
|
||||
|
||||
.edgePath .path {stroke: red;}
|
||||
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>info below</h1>
|
||||
<div style="display: flex;width: 100%; height: 100%">
|
||||
<div class="mermaid" style="width: 100%; height: 100%">
|
||||
graph TB
|
||||
A --> B
|
||||
A ==> C
|
||||
A .-> D
|
||||
A === E
|
||||
A -.- F
|
||||
D -- Hello --> a
|
||||
D-- text including R TD space --xb
|
||||
stateDiagram
|
||||
|
||||
NotFound --> NotFound: Status
|
||||
NotFound --> NotFound: Stop
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script src="./mermaid.js"></script>
|
||||
<script>
|
||||
mermaid.initialize({
|
||||
theme: 'dark',
|
||||
// theme: 'dark',
|
||||
// arrowMarkerAbsolute: true,
|
||||
// themeCSS: '.edgePath .path {stroke: red;} .arrowheadPath {fill: red;}',
|
||||
logLevel: 0,
|
||||
|
39
cypress/platform/huge.html
Normal file
39
cypress/platform/huge.html
Normal file
File diff suppressed because one or more lines are too long
30
dist/index.html
vendored
30
dist/index.html
vendored
@@ -353,6 +353,33 @@ graph TB
|
||||
linkStyle 1 stroke:greenyellow,stroke-width:2px
|
||||
style C fill:greenyellow,stroke:green,stroke-width:4px
|
||||
</div>
|
||||
<div class="mermaid">
|
||||
graph TB
|
||||
TITLE["Link Click Events<br>(click the nodes below)"]
|
||||
A[link test]
|
||||
B[anchor test]
|
||||
C[mailto test]
|
||||
D[other protocol test]
|
||||
E[script test]
|
||||
TITLE --> A & B & C & D & E
|
||||
click A "https://mermaid-js.github.io/mermaid/#/" "link test"
|
||||
click B "#link-clicked" "anchor test"
|
||||
click C "mailto:user@user.user" "mailto test"
|
||||
click D "notes://do-your-thing/id" "other protocol test"
|
||||
click E "javascript:alert('test')" "script test"
|
||||
</div>
|
||||
<hr/>
|
||||
<div class="mermaid">
|
||||
graph LR
|
||||
A[red<br>text] --> B(blue<br>text)
|
||||
C[/red<br/>text/] --> D{blue<br/>text}
|
||||
style A color:red;
|
||||
style B color:blue;
|
||||
style C stroke:#ff0000,fill:#ffcccc,color:#ff0000
|
||||
style D stroke:#0000ff,fill:#ccccff,color:#0000ff
|
||||
click B "index.html#link-clicked" "link test"
|
||||
click D testClick "click test"
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
|
||||
@@ -587,12 +614,15 @@ class Class10 {
|
||||
end note
|
||||
</div>
|
||||
|
||||
<h1 id="link-clicked">Anchor for "link-clicked" test</h1>
|
||||
|
||||
<script src="./mermaid.js"></script>
|
||||
<script>
|
||||
mermaid.initialize({
|
||||
theme: 'forest',
|
||||
// themeCSS: '.node rect { fill: red; }',
|
||||
logLevel: 3,
|
||||
securityLevel: 'loose',
|
||||
flowchart: { curve: 'basis' },
|
||||
gantt: { axisFormat: '%m/%d/%Y' },
|
||||
sequence: { actorMargin: 50 },
|
||||
|
@@ -105,7 +105,7 @@ Naming convention: a class name should be composed of alphanumeric (unicode allo
|
||||
|
||||
UML provides mechanisms to represent class members, such as attributes and methods, and additional information about them.
|
||||
|
||||
Mermaid distinguishes between attributes and functions/methods based on if the **parenthesis** `()` are present or not. The ones with `()` are treated as functions/methods, and others as attributes. To indicate a return type for a method, enclose the type within **square brackets** `[]`
|
||||
Mermaid distinguishes between attributes and functions/methods based on if the **parenthesis** `()` are present or not. The ones with `()` are treated as functions/methods, and others as attributes.
|
||||
|
||||
|
||||
There are two ways to define the members of a class, and regardless of whichever syntax is used to define the members, the output will still be same. The two different ways are :
|
||||
@@ -115,7 +115,7 @@ There are two ways to define the members of a class, and regardless of whichever
|
||||
class BankAccount
|
||||
BankAccount : +String owner
|
||||
BankAccount : +BigDecimal balance
|
||||
BankAccount : +deposit(amount) bool
|
||||
BankAccount : +deposit(amount)
|
||||
BankAccount : +withdrawal(amount)
|
||||
```
|
||||
|
||||
@@ -124,7 +124,7 @@ There are two ways to define the members of a class, and regardless of whichever
|
||||
class BankAccount
|
||||
BankAccount : +String owner
|
||||
BankAccount : +BigDecimal balance
|
||||
BankAccount : +deposit(amount) : bool
|
||||
BankAccount : +deposit(amount)
|
||||
BankAccount : +withdrawl(amount)
|
||||
```
|
||||
|
||||
@@ -142,11 +142,64 @@ class BankAccount{
|
||||
class BankAccount{
|
||||
+String owner
|
||||
+BigDecimal balance
|
||||
+deposit(amount) : bool
|
||||
+withdrawl(amount)
|
||||
+deposit(amount) bool
|
||||
+withdrawl(amount) int
|
||||
}
|
||||
```
|
||||
|
||||
#### Return Type
|
||||
Optionally you can end the method/function definition with the data type that will be returned (note: there must be a space between the final `)` of the method definition and return type
|
||||
example:
|
||||
```
|
||||
class BankAccount{
|
||||
+String owner
|
||||
+BigDecimal balance
|
||||
+deposit(amount) bool
|
||||
+withdrawl(amount) int
|
||||
}
|
||||
```
|
||||
```mermaid
|
||||
classDiagram
|
||||
class BankAccount{
|
||||
+String owner
|
||||
+BigDecimal balance
|
||||
+deposit(amount) bool
|
||||
+withdrawl(amount) int
|
||||
}
|
||||
```
|
||||
|
||||
#### Generic Types
|
||||
Members can be defined using generic types, such as `List<int>`, for fields, parameters and return types by enclosing the type within `~` (**tilde**). Note: **nested** type declarations (such as `List<List<int>>`) are not currently supported
|
||||
|
||||
This can be done as part of either class definition method:
|
||||
|
||||
```
|
||||
classDiagram
|
||||
class Square~Shape~{
|
||||
int id
|
||||
List~int~ position
|
||||
setPoints(List~int~ points)
|
||||
getPoints() List~int~
|
||||
}
|
||||
|
||||
Square : -List~string~ messages
|
||||
Square : +setMessages(List~string~ messages)
|
||||
Square : +getMessages() List~string~
|
||||
```
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Square~Shape~{
|
||||
int id
|
||||
List~int~ position
|
||||
setPoints(List~int~ points)
|
||||
getPoints() List~int~
|
||||
}
|
||||
|
||||
Square : -List~string~ messages
|
||||
Square : +setMessages(List~string~ messages)
|
||||
Square : +getMessages() List~string~
|
||||
```
|
||||
|
||||
#### Return Type
|
||||
Optionally you can end the method/function definition with the data type that will be returned
|
||||
@@ -157,7 +210,7 @@ To specify the visibility of a class member (i.e. any attribute or method), thes
|
||||
- `+` Public
|
||||
- `-` Private
|
||||
- `#` Protected
|
||||
- `~` Package
|
||||
- `~` Package/Internal
|
||||
|
||||
>_note_ you can also include additional _classifers_ to a method definition by adding the following notations to the end of the method, i.e.: after the `()`:
|
||||
> - `*` Abstract e.g.: `someAbstractMethod()*`
|
||||
|
@@ -509,13 +509,13 @@ It is possible to apply specific styles such as a thicker border or a different
|
||||
graph LR
|
||||
id1(Start)-->id2(Stop)
|
||||
style id1 fill:#f9f,stroke:#333,stroke-width:4px
|
||||
style id2 fill:#ccf,stroke:#f66,stroke-width:2px,stroke-dasharray: 5, 5
|
||||
style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5, 5
|
||||
```
|
||||
```mermaid
|
||||
graph LR
|
||||
id1(Start)-->id2(Stop)
|
||||
style id1 fill:#f9f,stroke:#333,stroke-width:4px
|
||||
style id2 fill:#ccf,stroke:#f66,stroke-width:2px,stroke-dasharray: 5, 5
|
||||
style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5, 5
|
||||
```
|
||||
|
||||
|
||||
|
@@ -275,11 +275,12 @@ mermaidAPI.initialize({
|
||||
### Parameters
|
||||
|
||||
- `id` the id of the element to be rendered
|
||||
- `txt` the graph definition
|
||||
- `_txt`
|
||||
- `cb` callback which is called after rendering is finished with the svg code as inparam.
|
||||
- `container` selector to element in which a div with the graph temporarily will be inserted. In one is
|
||||
provided a hidden div will be inserted in the body of the page instead. The element will be removed when rendering is
|
||||
completed.
|
||||
- `txt` the graph definition
|
||||
|
||||
##
|
||||
|
||||
|
@@ -380,6 +380,7 @@ describe('class diagram, ', function () {
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle dashed relation definition of different types and directions', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
@@ -390,6 +391,29 @@ describe('class diagram, ', function () {
|
||||
'Class19 .. Class20';
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle generic types in members', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Car~T~\n' +
|
||||
'Car : -List~Wheel~ wheels\n' +
|
||||
'Car : +setWheels(List~Wheel~ wheels)\n' +
|
||||
'Car : +getWheels() List~Wheel~';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle generic types in members in class with brackets', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Car {\n' +
|
||||
'List~Wheel~ wheels\n' +
|
||||
'setWheels(List~Wheel~ wheels)\n' +
|
||||
'+getWheels() List~Wheel~\n' +
|
||||
'}';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when fetching data from a classDiagram graph it', function () {
|
||||
@@ -614,6 +638,7 @@ describe('class diagram, ', function () {
|
||||
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);
|
||||
|
137
src/diagrams/class/classMemberRenderer.js
Normal file
137
src/diagrams/class/classMemberRenderer.js
Normal file
@@ -0,0 +1,137 @@
|
||||
export const addTspan = function(textEl, txt, isFirst, conf) {
|
||||
let member = parseMember(txt);
|
||||
|
||||
const tSpan = textEl
|
||||
.append('tspan')
|
||||
.attr('x', conf.padding)
|
||||
.text(member.displayText);
|
||||
|
||||
if (member.cssStyle !== '') {
|
||||
tSpan.attr('style', member.cssStyle);
|
||||
}
|
||||
|
||||
if (!isFirst) {
|
||||
tSpan.attr('dy', conf.textHeight);
|
||||
}
|
||||
};
|
||||
|
||||
export const buildFieldDisplay = function(parsedText) {
|
||||
let visibility = parsedText[1] ? parsedText[1].trim() : '';
|
||||
let fieldType = parsedText[2] ? parsedText[2].trim() : '';
|
||||
let genericType = parsedText[3] ? parseGenericTypes(parsedText[3]) : '';
|
||||
let fieldName = parsedText[4] ? parsedText[4].trim() : '';
|
||||
|
||||
return {
|
||||
displayText: visibility + fieldType + genericType + ' ' + fieldName,
|
||||
cssStyle: ''
|
||||
};
|
||||
};
|
||||
|
||||
export const buildMethodDisplay = function(parsedText) {
|
||||
let cssStyle = '';
|
||||
let displayText = parsedText;
|
||||
|
||||
let visibility = parsedText[1] ? parsedText[1].trim() : '';
|
||||
let methodName = parsedText[2] ? parsedText[2].trim() : '';
|
||||
let parameters = parsedText[3] ? parseGenericTypes(parsedText[3]) : '';
|
||||
let classifier = parsedText[6] ? parsedText[6].trim() : '';
|
||||
let returnType = parsedText[7] ? ' : ' + parseGenericTypes(parsedText[7]).trim() : '';
|
||||
|
||||
displayText = visibility + methodName + '(' + parameters + ')' + returnType;
|
||||
|
||||
cssStyle = parseClassifier(classifier);
|
||||
|
||||
let member = {
|
||||
displayText: displayText,
|
||||
cssStyle: cssStyle
|
||||
};
|
||||
|
||||
return member;
|
||||
};
|
||||
|
||||
export const buildLegacyDisplay = function(text) {
|
||||
// if for some reason we dont have any match, use old format to parse text
|
||||
let memberText = '';
|
||||
let cssStyle = '';
|
||||
let returnType = '';
|
||||
let methodStart = text.indexOf('(');
|
||||
let methodEnd = text.indexOf(')');
|
||||
|
||||
if (methodStart > 1 && methodEnd > methodStart && methodEnd <= text.length) {
|
||||
let parsedText = text.match(/(\+|-|~|#)?(\w+)/);
|
||||
let visibility = parsedText[1] ? parsedText[1].trim() : '';
|
||||
let methodName = parsedText[2];
|
||||
let parameters = text.substring(methodStart + 1, methodEnd);
|
||||
let classifier = text.substring(methodEnd, methodEnd + 1);
|
||||
cssStyle = parseClassifier(classifier);
|
||||
|
||||
memberText = visibility + methodName + '(' + parseGenericTypes(parameters.trim()) + ')';
|
||||
|
||||
if (methodEnd < memberText.length) {
|
||||
returnType = text.substring(methodEnd + 2).trim();
|
||||
if (returnType !== '') {
|
||||
returnType = ' : ' + parseGenericTypes(returnType);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// finally - if all else fails, just send the text back as written (other than parsing for generic types)
|
||||
memberText = parseGenericTypes(text);
|
||||
}
|
||||
|
||||
let member = {
|
||||
displayText: memberText + returnType,
|
||||
cssStyle: cssStyle
|
||||
};
|
||||
|
||||
return member;
|
||||
};
|
||||
|
||||
export const parseGenericTypes = function(text) {
|
||||
let cleanedText = text;
|
||||
|
||||
if (text.indexOf('~') != -1) {
|
||||
cleanedText = cleanedText.replace('~', '<');
|
||||
cleanedText = cleanedText.replace('~', '>');
|
||||
|
||||
return parseGenericTypes(cleanedText);
|
||||
} else {
|
||||
return cleanedText;
|
||||
}
|
||||
};
|
||||
|
||||
export const parseMember = function(text) {
|
||||
const fieldRegEx = /^(\+|-|~|#)?(\w+)(~\w+~|\[\])?\s+(\w+)$/;
|
||||
const methodRegEx = /^(\+|-|~|#)?(\w+)\s?\(\s*(\w+(~\w+~|\[\])?\s*(\w+)?)?\s*\)\s?([*|$])?\s?(\w+(~\w+~|\[\])?)?\s*$/;
|
||||
//const methodRegEx = /(\+|-|~|#)?(\w+)\s?\(\s*(\w+(~\w+~|\[\])?\s*(\w+)?)?\s*\)\s?([*|$])?\s?(\w+(~\w+~|\[\])?)?/;
|
||||
|
||||
let fieldMatch = text.match(fieldRegEx);
|
||||
let methodMatch = text.match(methodRegEx);
|
||||
|
||||
if (fieldMatch) {
|
||||
return buildFieldDisplay(fieldMatch);
|
||||
} else if (methodMatch) {
|
||||
return buildMethodDisplay(methodMatch);
|
||||
} else {
|
||||
return buildLegacyDisplay(text);
|
||||
}
|
||||
};
|
||||
|
||||
const parseClassifier = function(classifier) {
|
||||
switch (classifier) {
|
||||
case '*':
|
||||
return 'font-style:italic;';
|
||||
case '$':
|
||||
return 'text-decoration:underline;';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
addTspan,
|
||||
buildFieldDisplay,
|
||||
buildLegacyDisplay,
|
||||
buildMethodDisplay,
|
||||
parseGenericTypes,
|
||||
parseMember
|
||||
};
|
168
src/diagrams/class/classMemberRenderer.spec.js
Normal file
168
src/diagrams/class/classMemberRenderer.spec.js
Normal file
@@ -0,0 +1,168 @@
|
||||
/* eslint-env jasmine */
|
||||
import memberRenderer from './classMemberRenderer';
|
||||
|
||||
describe('class member Renderer, ', function () {
|
||||
describe('when parsing text to build method display string', function () {
|
||||
it('should handle simple method declaration', function () {
|
||||
const str = 'foo()';
|
||||
let actual = memberRenderer.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo()');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle public visibility', function () {
|
||||
const str = '+foo()';
|
||||
let actual = memberRenderer.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('+foo()');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle private visibility', function () {
|
||||
const str = '-foo()';
|
||||
let actual = memberRenderer.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('-foo()');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle protected visibility', function () {
|
||||
const str = '#foo()';
|
||||
let actual = memberRenderer.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('#foo()');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle package/internal visibility', function () {
|
||||
const str = '~foo()';
|
||||
let actual = memberRenderer.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('~foo()');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should ignore unknown character for visibility', function () {
|
||||
const str = '!foo()';
|
||||
let actual = memberRenderer.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo()');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle abstract classifier', function () {
|
||||
const str = 'foo()*';
|
||||
let actual = memberRenderer.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo()');
|
||||
expect(actual.cssStyle).toBe('font-style:italic;');
|
||||
});
|
||||
|
||||
it('should handle static classifier', function () {
|
||||
const str = 'foo()$';
|
||||
let actual = memberRenderer.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo()');
|
||||
expect(actual.cssStyle).toBe('text-decoration:underline;');
|
||||
});
|
||||
|
||||
it('should ignore unknown character for classifier', function () {
|
||||
const str = 'foo()!';
|
||||
let actual = memberRenderer.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo()');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle simple method declaration with parameters', function () {
|
||||
const str = 'foo(int id)';
|
||||
let actual = memberRenderer.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo(int id)');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle simple method declaration with single item in parameters', function () {
|
||||
const str = 'foo(id)';
|
||||
let actual = memberRenderer.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo(id)');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle simple method declaration with single item in parameters with extra spaces', function () {
|
||||
const str = ' foo ( id) ';
|
||||
let actual = memberRenderer.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo(id)');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle method declaration with return value', function () {
|
||||
const str = 'foo(id) int';
|
||||
let actual = memberRenderer.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo(id) : int');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle method declaration with generic return value', function () {
|
||||
const str = 'foo(id) List~int~';
|
||||
let actual = memberRenderer.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo(id) : List<int>');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle method declaration with generic parameter', function () {
|
||||
const str = 'foo(List~int~)';
|
||||
let actual = memberRenderer.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo(List<int>)');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle method declaration with all possible markup', function () {
|
||||
const str = '+foo ( List~int~ ids )* List~Item~';
|
||||
let actual = memberRenderer.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('+foo(List<int> ids) : List<Item>');
|
||||
expect(actual.cssStyle).toBe('font-style:italic;');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when parsing text for generic types', function () {
|
||||
it('should handle open and close brackets in correct order', function () {
|
||||
const str = 'foo(List~Item~)';
|
||||
let actual = memberRenderer.parseGenericTypes(str);
|
||||
|
||||
expect(actual).toBe('foo(List<Item>)');
|
||||
});
|
||||
|
||||
it('should handle open and close brackets in correct order with multiple usages', function () {
|
||||
const str = 'foo(List~Item~) List~Item~';
|
||||
let actual = memberRenderer.parseGenericTypes(str);
|
||||
|
||||
expect(actual).toBe('foo(List<Item>) List<Item>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when parsing text to build field display string', function () {
|
||||
it('should handle simple field declaration', function () {
|
||||
const str = 'int[] ids';
|
||||
let actual = memberRenderer.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('int[] ids');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle field declaration with generic type', function () {
|
||||
const str = 'List~int~ ids';
|
||||
let actual = memberRenderer.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('List<int> ids');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
@@ -5,6 +5,7 @@ import { logger } from '../../logger';
|
||||
import classDb, { lookUpDomId } from './classDb';
|
||||
import utils from '../../utils';
|
||||
import { parser } from './parser/classDiagram';
|
||||
import memberRenderer from './classMemberRenderer';
|
||||
|
||||
parser.yy = classDb;
|
||||
|
||||
@@ -287,75 +288,6 @@ const drawClass = function(elem, classDef) {
|
||||
cssClassStr = cssClassStr + classDef.cssClasses.join(' ');
|
||||
}
|
||||
|
||||
const addTspan = function(textEl, txt, isFirst) {
|
||||
let isMethod = txt.indexOf(')') > 1;
|
||||
let displayText = txt;
|
||||
let cssStyle = '';
|
||||
|
||||
if (isMethod) {
|
||||
let method = buildDisplayTextForMethod(txt);
|
||||
displayText = method.displayText;
|
||||
cssStyle = method.cssStyle;
|
||||
}
|
||||
|
||||
const tSpan = textEl
|
||||
.append('tspan')
|
||||
.attr('x', conf.padding)
|
||||
.text(displayText);
|
||||
|
||||
if (cssStyle !== '') {
|
||||
tSpan.attr('style', cssStyle);
|
||||
}
|
||||
|
||||
if (!isFirst) {
|
||||
tSpan.attr('dy', conf.textHeight);
|
||||
}
|
||||
};
|
||||
|
||||
const buildDisplayTextForMethod = function(txt) {
|
||||
let regEx = /(\+|-|~|#)?(\w+)\s?\((\w+(<\w+>|\[\])?\s?(\w+)?)?\)\s?([*|$])?\s?(\w+(<\w+>|\[\])?)?/;
|
||||
|
||||
let cssStyle = '';
|
||||
let displayText = txt;
|
||||
let methodName = txt;
|
||||
let classifier = '';
|
||||
|
||||
let parsedText = txt.match(regEx);
|
||||
|
||||
if (parsedText) {
|
||||
let visibility = parsedText[1] ? parsedText[1].trim() : '';
|
||||
methodName = parsedText[2] ? parsedText[2].trim() : '';
|
||||
let parameters = parsedText[3] ? parsedText[3].trim() : '';
|
||||
classifier = parsedText[6] ? parsedText[6].trim() : '';
|
||||
let returnType = parsedText[7] ? ' : ' + parsedText[7].trim() : '';
|
||||
|
||||
displayText = visibility + methodName + '(' + parameters + ')' + returnType;
|
||||
} else {
|
||||
let methodEnd = displayText.indexOf(')') + 1;
|
||||
classifier = displayText.substring(methodEnd, methodEnd + 1);
|
||||
if (classifier !== '' && classifier !== ' ') {
|
||||
displayText = displayText.replace(classifier, '');
|
||||
}
|
||||
}
|
||||
|
||||
switch (classifier) {
|
||||
case '*':
|
||||
cssStyle = 'font-style:italic;';
|
||||
break;
|
||||
case '$':
|
||||
cssStyle = 'text-decoration:underline;';
|
||||
break;
|
||||
}
|
||||
|
||||
let method = {
|
||||
methodname: methodName,
|
||||
displayText: displayText,
|
||||
cssStyle: cssStyle
|
||||
};
|
||||
|
||||
return method;
|
||||
};
|
||||
|
||||
const id = classDef.id;
|
||||
const classInfo = {
|
||||
id: id,
|
||||
@@ -426,7 +358,7 @@ const drawClass = function(elem, classDef) {
|
||||
|
||||
isFirst = true;
|
||||
classDef.members.forEach(function(member) {
|
||||
addTspan(members, member, isFirst);
|
||||
memberRenderer.addTspan(members, member, isFirst, conf);
|
||||
isFirst = false;
|
||||
});
|
||||
|
||||
@@ -448,7 +380,7 @@ const drawClass = function(elem, classDef) {
|
||||
isFirst = true;
|
||||
|
||||
classDef.methods.forEach(function(method) {
|
||||
addTspan(methods, method, isFirst);
|
||||
memberRenderer.addTspan(methods, method, isFirst, conf);
|
||||
isFirst = false;
|
||||
});
|
||||
|
||||
@@ -570,8 +502,8 @@ export const draw = function(text, id) {
|
||||
}
|
||||
});
|
||||
|
||||
diagram.attr('height', '100%');
|
||||
diagram.attr('width', `${g.graph().width * 1.5 + 20}`);
|
||||
diagram.attr('height', g.graph().height + 40);
|
||||
diagram.attr('width', g.graph().width * 1.5 + 20);
|
||||
diagram.attr('viewBox', '-10 -10 ' + (g.graph().width + 20) + ' ' + (g.graph().height + 20));
|
||||
};
|
||||
|
||||
|
@@ -87,6 +87,7 @@ export const addVertices = function(vert, g, svgId) {
|
||||
vertexNode.parentNode.removeChild(vertexNode);
|
||||
} else {
|
||||
const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
svgLabel.setAttribute('style', labelStyle.replace('color:', 'fill:'));
|
||||
|
||||
const rows = vertexText.split(/<br\s*\/?>/gi);
|
||||
|
||||
|
29
src/diagrams/flowchart/parser/flow-huge.spec.js
Normal file
29
src/diagrams/flowchart/parser/flow-huge.spec.js
Normal file
File diff suppressed because one or more lines are too long
@@ -48,7 +48,7 @@
|
||||
<STATE>.*"[[fork]]" {this.popState();yytext=yytext.slice(0,-8).trim();/*console.warn('Fork Fork: ',yytext);*/return 'FORK';}
|
||||
<STATE>.*"[[join]]" {this.popState();yytext=yytext.slice(0,-8).trim();/*console.warn('Fork Join: ',yytext);*/return 'JOIN';}
|
||||
<STATE>["] this.begin("STATE_STRING");
|
||||
<STATE>"as"\s* {this.popState();this.pushState('STATE_ID');return "AS";}
|
||||
<STATE>\s*"as"\s+ {this.popState();this.pushState('STATE_ID');return "AS";}
|
||||
<STATE_ID>[^\n\{]* {this.popState();/* console.log('STATE_ID', yytext);*/return "ID";}
|
||||
<STATE_STRING>["] this.popState();
|
||||
<STATE_STRING>[^"]* { /*console.log('Long description:', yytext);*/return "STATE_DESCR";}
|
||||
|
@@ -53,6 +53,39 @@ describe('state diagram, ', function() {
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('handle "as" in state names', function() {
|
||||
const str = `stateDiagram
|
||||
assemble
|
||||
state assemble
|
||||
`;
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
it('handle "as" in state names 1', function() {
|
||||
const str = `stateDiagram
|
||||
assemble
|
||||
state assemble
|
||||
`;
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
it('handle "as" in state names 2', function() {
|
||||
const str = `stateDiagram
|
||||
assembleas
|
||||
state assembleas
|
||||
`;
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
it('handle "as" in state names 3', function() {
|
||||
const str = `stateDiagram
|
||||
state "as" as as
|
||||
`;
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('scale', function() {
|
||||
const str = `stateDiagram\n
|
||||
scale 350 width
|
||||
|
@@ -53,7 +53,7 @@ export const draw = function(text, id) {
|
||||
|
||||
// Layout graph, Create a new directed graph
|
||||
const graph = new graphlib.Graph({
|
||||
multigraph: false,
|
||||
multigraph: true,
|
||||
compound: true,
|
||||
// acyclicer: 'greedy',
|
||||
rankdir: 'RL'
|
||||
@@ -110,7 +110,8 @@ const getRows = s => {
|
||||
const renderDoc = (doc, diagram, parentId, altBkg) => {
|
||||
// // Layout graph, Create a new directed graph
|
||||
const graph = new graphlib.Graph({
|
||||
compound: true
|
||||
compound: true,
|
||||
multigraph: true
|
||||
});
|
||||
|
||||
let i;
|
||||
@@ -126,28 +127,29 @@ const renderDoc = (doc, diagram, parentId, altBkg) => {
|
||||
if (parentId)
|
||||
graph.setGraph({
|
||||
rankdir: 'LR',
|
||||
// multigraph: false,
|
||||
multigraph: true,
|
||||
compound: true,
|
||||
// acyclicer: 'greedy',
|
||||
ranker: 'tight-tree',
|
||||
ranksep: edgeFreeDoc ? 1 : conf.edgeLengthFactor,
|
||||
nodeSep: edgeFreeDoc ? 1 : 50
|
||||
// isMultiGraph: false
|
||||
nodeSep: edgeFreeDoc ? 1 : 50,
|
||||
isMultiGraph: true
|
||||
// ranksep: 5,
|
||||
// nodesep: 1
|
||||
});
|
||||
else {
|
||||
graph.setGraph({
|
||||
rankdir: 'TB',
|
||||
multigraph: true,
|
||||
compound: true,
|
||||
// isCompound: true,
|
||||
// acyclicer: 'greedy',
|
||||
// ranker: 'longest-path'
|
||||
ranksep: edgeFreeDoc ? 1 : conf.edgeLengthFactor,
|
||||
nodeSep: edgeFreeDoc ? 1 : 50,
|
||||
ranker: 'tight-tree'
|
||||
ranker: 'tight-tree',
|
||||
// ranker: 'network-simplex'
|
||||
// isMultiGraph: false
|
||||
isMultiGraph: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -226,14 +228,22 @@ const renderDoc = (doc, diagram, parentId, altBkg) => {
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Count=', graph.nodeCount());
|
||||
logger.debug('Count=', graph.nodeCount(), graph);
|
||||
let cnt = 0;
|
||||
relations.forEach(function(relation) {
|
||||
graph.setEdge(relation.id1, relation.id2, {
|
||||
relation: relation,
|
||||
width: getLabelWidth(relation.title),
|
||||
height: conf.labelHeight * getRows(relation.title).length,
|
||||
labelpos: 'c'
|
||||
});
|
||||
cnt++;
|
||||
logger.debug('Setting edge', relation);
|
||||
graph.setEdge(
|
||||
relation.id1,
|
||||
relation.id2,
|
||||
{
|
||||
relation: relation,
|
||||
width: getLabelWidth(relation.title),
|
||||
height: conf.labelHeight * getRows(relation.title).length,
|
||||
labelpos: 'c'
|
||||
},
|
||||
'id' + cnt
|
||||
);
|
||||
});
|
||||
|
||||
dagre.layout(graph);
|
||||
@@ -299,7 +309,7 @@ const renderDoc = (doc, diagram, parentId, altBkg) => {
|
||||
stateInfo.width = stateBox.width + 2 * conf.padding;
|
||||
stateInfo.height = stateBox.height + 2 * conf.padding;
|
||||
|
||||
logger.info('Doc rendered', stateInfo, graph);
|
||||
logger.debug('Doc rendered', stateInfo, graph);
|
||||
return stateInfo;
|
||||
};
|
||||
|
||||
|
@@ -95,6 +95,8 @@ const config = {
|
||||
*/
|
||||
theme: 'default',
|
||||
themeCSS: undefined,
|
||||
/* **maxTextSize** - The maximum allowed size of the users text diamgram */
|
||||
maxTextSize: 50000,
|
||||
|
||||
/**
|
||||
* **fontFamily** The font to be used for the rendered diagrams. Default value is \"trebuchet ms\", verdana, arial;
|
||||
@@ -460,7 +462,13 @@ export const decodeEntities = function(text) {
|
||||
* provided a hidden div will be inserted in the body of the page instead. The element will be removed when rendering is
|
||||
* completed.
|
||||
*/
|
||||
const render = function(id, txt, cb, container) {
|
||||
const render = function(id, _txt, cb, container) {
|
||||
// Check the maximum allowed text size
|
||||
let txt = _txt;
|
||||
if (_txt.length > config.maxTextSize) {
|
||||
txt = 'graph TB;a[Maximum text size in diagram exceeded];style a fill:#faa';
|
||||
}
|
||||
|
||||
if (typeof container !== 'undefined') {
|
||||
container.innerHTML = '';
|
||||
|
||||
|
@@ -99,10 +99,6 @@ export const formatUrl = (linkStr, config) => {
|
||||
if (url) {
|
||||
if (config.securityLevel !== 'loose') {
|
||||
return sanitizeUrl(url);
|
||||
} else {
|
||||
if (!/^(https?:)?\/\//i.test(url)) {
|
||||
url = 'http://' + url;
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
|
@@ -37,3 +37,61 @@ describe('when finding substring in array ', function() {
|
||||
expect(result).toEqual(-1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when formatting urls', function() {
|
||||
it('should handle links', function() {
|
||||
const url = 'https://mermaid-js.github.io/mermaid/#/';
|
||||
|
||||
let config = { securityLevel: 'loose' };
|
||||
let result = utils.formatUrl(url, config);
|
||||
expect(result).toEqual(url);
|
||||
|
||||
config.securityLevel = 'strict';
|
||||
result = utils.formatUrl(url, config);
|
||||
expect(result).toEqual(url);
|
||||
});
|
||||
it('should handle anchors', function() {
|
||||
const url = '#interaction';
|
||||
|
||||
let config = { securityLevel: 'loose' };
|
||||
let result = utils.formatUrl(url, config);
|
||||
expect(result).toEqual(url);
|
||||
|
||||
config.securityLevel = 'strict';
|
||||
result = utils.formatUrl(url, config);
|
||||
expect(result).toEqual('about:blank');
|
||||
});
|
||||
it('should handle mailto', function() {
|
||||
const url = 'mailto:user@user.user';
|
||||
|
||||
let config = { securityLevel: 'loose' };
|
||||
let result = utils.formatUrl(url, config);
|
||||
expect(result).toEqual(url);
|
||||
|
||||
config.securityLevel = 'strict';
|
||||
result = utils.formatUrl(url, config);
|
||||
expect(result).toEqual(url);
|
||||
});
|
||||
it('should handle other protocols', function() {
|
||||
const url = 'notes://do-your-thing/id';
|
||||
|
||||
let config = { securityLevel: 'loose' };
|
||||
let result = utils.formatUrl(url, config);
|
||||
expect(result).toEqual(url);
|
||||
|
||||
config.securityLevel = 'strict';
|
||||
result = utils.formatUrl(url, config);
|
||||
expect(result).toEqual(url);
|
||||
});
|
||||
it('should handle scripts', function() {
|
||||
const url = 'javascript:alert("test")';
|
||||
|
||||
let config = { securityLevel: 'loose' };
|
||||
let result = utils.formatUrl(url, config);
|
||||
expect(result).toEqual(url);
|
||||
|
||||
config.securityLevel = 'strict';
|
||||
result = utils.formatUrl(url, config);
|
||||
expect(result).toEqual('about:blank');
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user