Merge branch 'feature/963_class_annotations' of https://github.com/chris579/mermaid into develop

This commit is contained in:
Ashish Jain 2019-10-08 12:49:52 +02:00
commit 6b4a8325ae
24 changed files with 19169 additions and 85 deletions

15
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@ -0,0 +1,15 @@
---
name: Question
about: Get some help from the community.
title: ''
labels: 'Help wanted!, Type: Other'
assignees: ''
---
## Help us help you!
You want an answer. Here are some ways to get it quicker:
* Use a clear and concise title.
* Try to pose a clear and concise question.
* Include as much, or as little, code as necessary.
* Don't be shy to give us some screenshots, if it helps!

13
.github/workflows/lock-closed-issue.yml vendored Normal file
View File

@ -0,0 +1,13 @@
name: Lock closed issue
on:
issues:
types: [closed]
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: Dunning-Kruger/lock-issues@v1
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"

View File

@ -0,0 +1,13 @@
name: Unlock reopened issue
on:
issues:
types: [reopened]
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: Dunning-Kruger/unlock-issues@v1
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"

1
.gitignore vendored
View File

@ -2,6 +2,7 @@
node_modules/
coverage/
.idea/
dist/*.js
dist/*.map

View File

@ -86,6 +86,7 @@ Future task2 : des4, after des3, 5d
```
classDiagram
Class01 <|-- AveryLongClass : Cool
<<interface>> Class01
Class03 *-- Class04
Class05 o-- Class06
Class07 .. Class08
@ -98,6 +99,11 @@ Class01 : size()
Class01 : int chimp
Class01 : int gorilla
Class08 <--> C2: Cool label
class Class10 {
<<service>>
int id
size()
}
```
![Class diagram](./img/class.png)

View File

@ -1,12 +1,13 @@
/* eslint-env jest */
import { imgSnapshotTest } from '../../helpers/util';
describe('Sequencediagram', () => {
it('should render a simple class diagrams', () => {
describe('Class diagram', () => {
it('should render a simple class diagram', () => {
imgSnapshotTest(
`
classDiagram
Class01 <|-- AveryLongClass : Cool
&lt;&lt;interface&gt;&gt; Class01
Class03 *-- Class04
Class05 o-- Class06
Class07 .. Class08
@ -19,6 +20,11 @@ describe('Sequencediagram', () => {
Class01 : int chimp
Class01 : int gorilla
Class08 <--> C2: Cool label
class Class10 {
&lt;&lt;service&gt;&gt;
int id
test()
}
`,
{}
);

View File

@ -1,50 +1,55 @@
import { Base64 } from 'js-base64'
import mermaid2 from '../../src/mermaid'
import { Base64 } from 'js-base64';
import mermaid2 from '../../src/mermaid';
/**
* ##contentLoaded
* Callback function that is called when page is loaded. This functions fetches configuration for mermaid rendering and
* calls init for rendering the mermaid diagrams on the page.
*/
const contentLoaded = function () {
let pos = document.location.href.indexOf('?graph=')
const contentLoaded = function() {
let pos = document.location.href.indexOf('?graph=');
if (pos > 0) {
pos = pos + 7
const graphBase64 = document.location.href.substr(pos)
const graphObj = JSON.parse(Base64.decode(graphBase64))
pos = pos + 7;
const graphBase64 = document.location.href.substr(pos);
const graphObj = JSON.parse(Base64.decode(graphBase64));
// const graph = 'hello'
console.log(graphObj)
const div = document.createElement('div')
div.id = 'block'
div.className = 'mermaid'
div.innerHTML = graphObj.code
document.getElementsByTagName('body')[0].appendChild(div)
global.mermaid.initialize(graphObj.mermaid)
console.log(graphObj);
const div = document.createElement('div');
div.id = 'block';
div.className = 'mermaid';
div.innerHTML = graphObj.code;
document.getElementsByTagName('body')[0].appendChild(div);
global.mermaid.initialize(graphObj.mermaid);
// console.log('graphObj.mermaid', graphObj.mermaid)
global.mermaid.init()
global.mermaid.init();
}
}
const contentLoadedApi = function () {
let pos = document.location.href.indexOf('?graph=')
};
const contentLoadedApi = function() {
let pos = document.location.href.indexOf('?graph=');
if (pos > 0) {
pos = pos + 7
const graphBase64 = document.location.href.substr(pos)
const graphObj = JSON.parse(Base64.decode(graphBase64))
pos = pos + 7;
const graphBase64 = document.location.href.substr(pos);
const graphObj = JSON.parse(Base64.decode(graphBase64));
// const graph = 'hello'
const div = document.createElement('div')
div.id = 'block'
div.className = 'mermaid'
const div = document.createElement('div');
div.id = 'block';
div.className = 'mermaid';
// div.innerHTML = graphObj.code
document.getElementsByTagName('body')[0].appendChild(div)
global.mermaid.initialize(graphObj.mermaid)
document.getElementsByTagName('body')[0].appendChild(div);
global.mermaid.initialize(graphObj.mermaid);
mermaid2.render('newid', graphObj.code, (svgCode, bindFunctions) => {
div.innerHTML = svgCode
mermaid2.render(
'newid',
graphObj.code,
(svgCode, bindFunctions) => {
div.innerHTML = svgCode;
bindFunctions(div)
}, div)
bindFunctions(div);
},
div
);
}
}
};
if (typeof document !== 'undefined') {
/*!
@ -52,15 +57,15 @@ if (typeof document !== 'undefined') {
*/
window.addEventListener(
'load',
function () {
function() {
if (this.location.href.match('xss.html')) {
this.console.log('Using api')
contentLoadedApi()
this.console.log('Using api');
contentLoadedApi();
} else {
this.console.log('Not using api')
contentLoaded()
this.console.log('Not using api');
contentLoaded();
}
},
false
)
);
}

19
dist/index.html vendored
View File

@ -399,6 +399,7 @@ merge newbranch
<div class="mermaid">
classDiagram
Class01 <|-- AveryLongClass : Cool
&lt;&lt;interface&gt;&gt; Class01
Class03 "0" *-- "0..n" Class04
Class05 "1" o-- "many" Class06
Class07 .. Class08
@ -411,6 +412,11 @@ Class01 : size()
Class01 : int chimp
Class01 : int gorilla
Class08 <--> C2: Cool label
class Class10 {
&lt;&lt;service&gt;&gt;
int id
size()
}
</div>
<script src="./mermaid.js"></script>
<script>
@ -439,5 +445,18 @@ Class08 <--> C2: Cool label
}, 100)
}
</script>
<script>
const testLineEndings = (test, input) => {
try {
mermaid.render(test, input, () => {});
} catch (err) {
console.error("Error in %s:\n\n%s", test, err);
}
};
testLineEndings("CR", "graph LR\rsubgraph CR\rA --> B\rend");
testLineEndings("LF", "graph LR\nsubgraph LF\nA --> B\nend");
testLineEndings("CRLF", "graph LR\r\nsubgraph CRLF\r\nA --> B\r\nend");
</script>
</body>
</html>

View File

@ -4,7 +4,7 @@
This statement declares a new graph and the direction of the graph layout.
This declares a graph oriented from top to bottom.
This declares a graph oriented from top to bottom (`TD` or `TB`).
```
graph TD
@ -15,7 +15,16 @@ graph TD
Start --> Stop
```
This declares a graph oriented from left to right.
This declares a graph oriented from left to right (`LR`).
```
graph LR
Start --> Stop
```
```mermaid
graph LR
Start --> Stop
```
Possible directions are:
@ -26,14 +35,6 @@ Possible directions are:
* TD - same as TB
```
graph LR
Start --> Stop
```
```mermaid
graph LR
Start --> Stop
```
## Nodes & shapes

View File

@ -26,8 +26,6 @@ gantt
Task in sec :2014-01-12 , 12d
another task : 24d
```
## Syntax
```
@ -106,7 +104,11 @@ Tbd
### Date format
Tbd
The default date format is YYYY-MM-DD. You can define your ``dateFormat``. For example:
```
dateFormat YYYY MM DD
```
### Diagram definition

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -79,7 +79,6 @@ If your application is taking resposibility for the diagram source security you
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="mermaid.min.css">
</head>
<body>
<div class="mermaid">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 68 KiB

18828
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -47,6 +47,7 @@
},
"dependencies": {
"@braintree/sanitize-url": "^3.1.0",
"crypto-random-string": "^3.0.1",
"d3": "^5.7.0",
"dagre-d3-renderer": "^0.5.8",
"dagre-layout": "^0.8.8",

View File

@ -6,18 +6,18 @@ let classes = {};
/**
* Function called by parser when a node definition has been found.
* @param id
* @param text
* @param type
* @param style
* @public
*/
export const addClass = function(id) {
if (typeof classes[id] === 'undefined') {
// Only add class if not exists
if (typeof classes[id] !== 'undefined') return;
classes[id] = {
id: id,
methods: [],
members: []
members: [],
annotations: []
};
}
};
export const clear = function() {
@ -43,13 +43,39 @@ export const addRelation = function(relation) {
relations.push(relation);
};
/**
* Adds an annotation to the specified class
* Annotations mark special properties of the given type (like 'interface' or 'service')
* @param className The class name
* @param annotation The name of the annotation without any brackets
* @public
*/
export const addAnnotation = function(className, annotation) {
classes[className].annotations.push(annotation);
};
/**
* Adds a member to the specified class
* @param className The class name
* @param member The full name of the member.
* If the member is enclosed in <<brackets>> it is treated as an annotation
* If the member is ending with a closing bracket ) it is treated as a method
* Otherwise the member will be treated as a normal property
* @public
*/
export const addMember = function(className, member) {
const theClass = classes[className];
if (typeof member === 'string') {
if (member.substr(-1) === ')') {
theClass.methods.push(member);
// Member can contain white spaces, we trim them out
const memberString = member.trim();
if (memberString.startsWith('<<') && memberString.endsWith('>>')) {
// Remove leading and trailing brackets
theClass.annotations.push(memberString.substring(2, memberString.length - 2));
} else if (memberString.endsWith(')')) {
theClass.methods.push(memberString);
} else {
theClass.members.push(member);
theClass.members.push(memberString);
}
}
};
@ -85,6 +111,7 @@ export default {
clear,
getClass,
getClasses,
addAnnotation,
getRelations,
addRelation,
addMember,

View File

@ -207,5 +207,60 @@ describe('class diagram, ', function() {
expect(relations[3].relation.type2).toBe('none');
expect(relations[3].relation.lineType).toBe(classDb.lineType.DOTTED_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');
});
});
});

View File

@ -255,15 +255,34 @@ const drawClass = function(elem, classDef) {
height: 0
};
// add class group
const g = elem
.append('g')
.attr('id', id)
.attr('class', 'classGroup');
// add title
const title = g
.append('text')
.attr('x', conf.padding)
.attr('y', conf.textHeight + conf.padding)
.text(classDef.id);
.attr('x', 0);
// add annotations
let isFirst = true;
classDef.annotations.forEach(function(member) {
const titleText2 = title.append('tspan').text('«' + member + '»');
if (!isFirst) titleText2.attr('dy', conf.textHeight);
isFirst = false;
});
// add class title
const classTitle = title
.append('tspan')
.text(classDef.id)
.attr('class', 'title');
// If class has annotations the title needs to have an offset of the text height
if (!isFirst) classTitle.attr('dy', conf.textHeight);
const titleHeight = title.node().getBBox().height;
@ -280,7 +299,7 @@ const drawClass = function(elem, classDef) {
.attr('fill', 'white')
.attr('class', 'classText');
let isFirst = true;
isFirst = true;
classDef.members.forEach(function(member) {
addTspan(members, member, isFirst);
isFirst = false;
@ -309,16 +328,25 @@ const drawClass = function(elem, classDef) {
});
const classBox = g.node().getBBox();
g.insert('rect', ':first-child')
const rect = g
.insert('rect', ':first-child')
.attr('x', 0)
.attr('y', 0)
.attr('width', classBox.width + 2 * conf.padding)
.attr('height', classBox.height + conf.padding + 0.5 * conf.dividerMargin);
membersLine.attr('x2', classBox.width + 2 * conf.padding);
methodsLine.attr('x2', classBox.width + 2 * conf.padding);
const rectWidth = rect.node().getBBox().width;
classInfo.width = classBox.width + 2 * conf.padding;
// Center title
// We subtract the width of each text element from the class box width and divide it by 2
title.node().childNodes.forEach(function(x) {
x.setAttribute('x', (rectWidth - x.getBBox().width) / 2);
});
membersLine.attr('x2', rectWidth);
methodsLine.attr('x2', rectWidth);
classInfo.width = rectWidth;
classInfo.height = classBox.height + conf.padding + 0.5 * conf.dividerMargin;
idCache[id] = classInfo;

View File

@ -21,6 +21,8 @@
"class" return 'CLASS';
"<<" return 'ANNOTATION_START';
">>" return 'ANNOTATION_END';
["] this.begin("string");
<string>["] this.popState();
<string>[^"]* return "STR";
@ -131,7 +133,6 @@ statements
| statement NEWLINE statements
;
className
: alphaNumToken className { $$=$1+$2; }
| alphaNumToken { $$=$1; }
@ -142,6 +143,7 @@ statement
| relationStatement LABEL { $1.title = yy.cleanupLabel($2); yy.addRelation($1); }
| classStatement
| methodStatement
| annotationStatement
;
classStatement
@ -149,6 +151,10 @@ classStatement
| CLASS className STRUCT_START members STRUCT_STOP {/*console.log($2,JSON.stringify($4));*/yy.addClass($2);yy.addMembers($2,$4);}
;
annotationStatement
: ANNOTATION_START alphaNumToken ANNOTATION_END className { yy.addAnnotation($4,$2); }
;
members
: MEMBER { $$ = [$1]; }
| MEMBER members { $2.push($1);$$=$2;}

View File

@ -169,7 +169,7 @@
"{" return 'DIAMOND_START'
"}" return 'DIAMOND_STOP'
"\"" return 'QUOTE';
\n+ return 'NEWLINE';
(\r|\n|\r\n)+ return 'NEWLINE';
\s return 'SPACE';
<<EOF>> return 'EOF';

View File

@ -1,4 +1,5 @@
import _ from 'lodash';
import randomString from 'crypto-random-string';
import { logger } from '../../logger';
@ -9,17 +10,11 @@ let curBranch = 'master';
let direction = 'LR';
let seq = 0;
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
function getId() {
const pool = '0123456789abcdef';
let id = '';
for (let i = 0; i < 7; i++) {
id += pool[getRandomInt(0, 16)];
}
return id;
return randomString({
length: 7,
characters: '0123456789abcdef'
});
}
function isfastforwardable(currentCommit, otherCommit) {

View File

@ -1,11 +1,24 @@
/* eslint-env jasmine */
import gitGraphAst from './gitGraphAst';
import { parser } from './parser/gitGraph';
import randomString from 'crypto-random-string';
import cryptoRandomString from 'crypto-random-string';
jest.mock('crypto-random-string');
describe('when parsing a gitGraph', function() {
let randomNumber;
beforeEach(function() {
parser.yy = gitGraphAst;
parser.yy.clear();
randomNumber = 0;
cryptoRandomString.mockImplementation(() => {
randomNumber = randomNumber + 1;
return String(randomNumber);
});
});
afterEach(function() {
cryptoRandomString.mockReset();
});
it('should handle a gitGraph defintion', function() {
const str = 'gitGraph:\n' + 'commit\n';
@ -224,4 +237,51 @@ describe('when parsing a gitGraph', function() {
parser.yy.prettyPrint();
});
it('it should generate a secure random ID for commits', function() {
const str = 'gitGraph:\n' + 'commit\n' + 'commit\n';
const EXPECTED_LENGTH = 7;
const EXPECTED_CHARACTERS = '0123456789abcdef';
let idCount = 0;
randomString.mockImplementation(options => {
if (
options.length === EXPECTED_LENGTH &&
options.characters === EXPECTED_CHARACTERS &&
Object.keys(options).length === 2
) {
const id = `abcdef${idCount}`;
idCount += 1;
return id;
}
return 'unexpected-ID';
});
parser.parse(str);
const commits = parser.yy.getCommits();
expect(Object.keys(commits)).toEqual(['abcdef0', 'abcdef1']);
Object.keys(commits).forEach(key => {
expect(commits[key].id).toEqual(key);
});
});
it('it should generate an array of known branches', function() {
const str =
'gitGraph:\n' +
'commit\n' +
'branch b1\n' +
'checkout b1\n' +
'commit\n' +
'commit\n' +
'branch b2\n';
parser.parse(str);
const branches = gitGraphAst.getBranchesAsObjArray();
expect(branches).toHaveLength(3);
expect(branches[0]).toHaveProperty('name', 'master');
expect(branches[1]).toHaveProperty('name', 'b1');
expect(branches[2]).toHaveProperty('name', 'b2');
});
});

View File

@ -3,6 +3,10 @@ g.classGroup text {
stroke: none;
font-family: 'trebuchet ms', verdana, arial;
font-size: 10px;
.title {
font-weight: bolder;
}
}
g.classGroup rect {