Merge pull request #973 from chris579/feature/963_class_annotations

Class diagram: annotations support
This commit is contained in:
ashishjain0512
2019-10-08 12:56:49 +02:00
committed by GitHub
11 changed files with 205 additions and 61 deletions

1
.gitignore vendored
View File

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

View File

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

View File

@@ -1,12 +1,13 @@
/* eslint-env jest */ /* eslint-env jest */
import { imgSnapshotTest } from '../../helpers/util'; import { imgSnapshotTest } from '../../helpers/util';
describe('Sequencediagram', () => { describe('Class diagram', () => {
it('should render a simple class diagrams', () => { it('should render a simple class diagram', () => {
imgSnapshotTest( imgSnapshotTest(
` `
classDiagram classDiagram
Class01 <|-- AveryLongClass : Cool Class01 <|-- AveryLongClass : Cool
&lt;&lt;interface&gt;&gt; Class01
Class03 *-- Class04 Class03 *-- Class04
Class05 o-- Class06 Class05 o-- Class06
Class07 .. Class08 Class07 .. Class08
@@ -19,6 +20,11 @@ describe('Sequencediagram', () => {
Class01 : int chimp Class01 : int chimp
Class01 : int gorilla Class01 : int gorilla
Class08 <--> C2: Cool label 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 { Base64 } from 'js-base64';
import mermaid2 from '../../src/mermaid' import mermaid2 from '../../src/mermaid';
/** /**
* ##contentLoaded * ##contentLoaded
* Callback function that is called when page is loaded. This functions fetches configuration for mermaid rendering and * 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. * calls init for rendering the mermaid diagrams on the page.
*/ */
const contentLoaded = function () { const contentLoaded = function() {
let pos = document.location.href.indexOf('?graph=') let pos = document.location.href.indexOf('?graph=');
if (pos > 0) { if (pos > 0) {
pos = pos + 7 pos = pos + 7;
const graphBase64 = document.location.href.substr(pos) const graphBase64 = document.location.href.substr(pos);
const graphObj = JSON.parse(Base64.decode(graphBase64)) const graphObj = JSON.parse(Base64.decode(graphBase64));
// const graph = 'hello' // const graph = 'hello'
console.log(graphObj) console.log(graphObj);
const div = document.createElement('div') const div = document.createElement('div');
div.id = 'block' div.id = 'block';
div.className = 'mermaid' div.className = 'mermaid';
div.innerHTML = graphObj.code div.innerHTML = graphObj.code;
document.getElementsByTagName('body')[0].appendChild(div) document.getElementsByTagName('body')[0].appendChild(div);
global.mermaid.initialize(graphObj.mermaid) global.mermaid.initialize(graphObj.mermaid);
// console.log('graphObj.mermaid', graphObj.mermaid) // console.log('graphObj.mermaid', graphObj.mermaid)
global.mermaid.init() global.mermaid.init();
} }
} };
const contentLoadedApi = function () { const contentLoadedApi = function() {
let pos = document.location.href.indexOf('?graph=') let pos = document.location.href.indexOf('?graph=');
if (pos > 0) { if (pos > 0) {
pos = pos + 7 pos = pos + 7;
const graphBase64 = document.location.href.substr(pos) const graphBase64 = document.location.href.substr(pos);
const graphObj = JSON.parse(Base64.decode(graphBase64)) const graphObj = JSON.parse(Base64.decode(graphBase64));
// const graph = 'hello' // const graph = 'hello'
const div = document.createElement('div') const div = document.createElement('div');
div.id = 'block' div.id = 'block';
div.className = 'mermaid' div.className = 'mermaid';
// div.innerHTML = graphObj.code // div.innerHTML = graphObj.code
document.getElementsByTagName('body')[0].appendChild(div) document.getElementsByTagName('body')[0].appendChild(div);
global.mermaid.initialize(graphObj.mermaid) global.mermaid.initialize(graphObj.mermaid);
mermaid2.render('newid', graphObj.code, (svgCode, bindFunctions) => { mermaid2.render(
div.innerHTML = svgCode 'newid',
graphObj.code,
(svgCode, bindFunctions) => {
div.innerHTML = svgCode;
bindFunctions(div) bindFunctions(div);
}, div) },
div
);
} }
} };
if (typeof document !== 'undefined') { if (typeof document !== 'undefined') {
/*! /*!
@@ -52,15 +57,15 @@ if (typeof document !== 'undefined') {
*/ */
window.addEventListener( window.addEventListener(
'load', 'load',
function () { function() {
if (this.location.href.match('xss.html')) { if (this.location.href.match('xss.html')) {
this.console.log('Using api') this.console.log('Using api');
contentLoadedApi() contentLoadedApi();
} else { } else {
this.console.log('Not using api') this.console.log('Not using api');
contentLoaded() contentLoaded();
} }
}, },
false false
) );
} }

6
dist/index.html vendored
View File

@@ -399,6 +399,7 @@ merge newbranch
<div class="mermaid"> <div class="mermaid">
classDiagram classDiagram
Class01 <|-- AveryLongClass : Cool Class01 <|-- AveryLongClass : Cool
&lt;&lt;interface&gt;&gt; Class01
Class03 "0" *-- "0..n" Class04 Class03 "0" *-- "0..n" Class04
Class05 "1" o-- "many" Class06 Class05 "1" o-- "many" Class06
Class07 .. Class08 Class07 .. Class08
@@ -411,6 +412,11 @@ Class01 : size()
Class01 : int chimp Class01 : int chimp
Class01 : int gorilla Class01 : int gorilla
Class08 <--> C2: Cool label Class08 <--> C2: Cool label
class Class10 {
&lt;&lt;service&gt;&gt;
int id
size()
}
</div> </div>
<script src="./mermaid.js"></script> <script src="./mermaid.js"></script>
<script> <script>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 141 KiB

View File

@@ -6,18 +6,18 @@ let classes = {};
/** /**
* Function called by parser when a node definition has been found. * Function called by parser when a node definition has been found.
* @param id * @param id
* @param text * @public
* @param type
* @param style
*/ */
export const addClass = function(id) { export const addClass = function(id) {
if (typeof classes[id] === 'undefined') { // Only add class if not exists
classes[id] = { if (typeof classes[id] !== 'undefined') return;
id: id,
methods: [], classes[id] = {
members: [] id: id,
}; methods: [],
} members: [],
annotations: []
};
}; };
export const clear = function() { export const clear = function() {
@@ -43,13 +43,39 @@ export const addRelation = function(relation) {
relations.push(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) { export const addMember = function(className, member) {
const theClass = classes[className]; const theClass = classes[className];
if (typeof member === 'string') { if (typeof member === 'string') {
if (member.substr(-1) === ')') { // Member can contain white spaces, we trim them out
theClass.methods.push(member); 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 { } else {
theClass.members.push(member); theClass.members.push(memberString);
} }
} }
}; };
@@ -85,6 +111,7 @@ export default {
clear, clear,
getClass, getClass,
getClasses, getClasses,
addAnnotation,
getRelations, getRelations,
addRelation, addRelation,
addMember, addMember,

View File

@@ -207,5 +207,60 @@ describe('class diagram, ', 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 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 height: 0
}; };
// add class group
const g = elem const g = elem
.append('g') .append('g')
.attr('id', id) .attr('id', id)
.attr('class', 'classGroup'); .attr('class', 'classGroup');
// add title
const title = g const title = g
.append('text') .append('text')
.attr('x', conf.padding)
.attr('y', conf.textHeight + 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; const titleHeight = title.node().getBBox().height;
@@ -280,7 +299,7 @@ const drawClass = function(elem, classDef) {
.attr('fill', 'white') .attr('fill', 'white')
.attr('class', 'classText'); .attr('class', 'classText');
let isFirst = true; isFirst = true;
classDef.members.forEach(function(member) { classDef.members.forEach(function(member) {
addTspan(members, member, isFirst); addTspan(members, member, isFirst);
isFirst = false; isFirst = false;
@@ -309,16 +328,25 @@ const drawClass = function(elem, classDef) {
}); });
const classBox = g.node().getBBox(); const classBox = g.node().getBBox();
g.insert('rect', ':first-child') const rect = g
.insert('rect', ':first-child')
.attr('x', 0) .attr('x', 0)
.attr('y', 0) .attr('y', 0)
.attr('width', classBox.width + 2 * conf.padding) .attr('width', classBox.width + 2 * conf.padding)
.attr('height', classBox.height + conf.padding + 0.5 * conf.dividerMargin); .attr('height', classBox.height + conf.padding + 0.5 * conf.dividerMargin);
membersLine.attr('x2', classBox.width + 2 * conf.padding); const rectWidth = rect.node().getBBox().width;
methodsLine.attr('x2', classBox.width + 2 * conf.padding);
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; classInfo.height = classBox.height + conf.padding + 0.5 * conf.dividerMargin;
idCache[id] = classInfo; idCache[id] = classInfo;

View File

@@ -21,6 +21,8 @@
"class" return 'CLASS'; "class" return 'CLASS';
"<<" return 'ANNOTATION_START';
">>" return 'ANNOTATION_END';
["] this.begin("string"); ["] this.begin("string");
<string>["] this.popState(); <string>["] this.popState();
<string>[^"]* return "STR"; <string>[^"]* return "STR";
@@ -131,7 +133,6 @@ statements
| statement NEWLINE statements | statement NEWLINE statements
; ;
className className
: alphaNumToken className { $$=$1+$2; } : alphaNumToken className { $$=$1+$2; }
| alphaNumToken { $$=$1; } | alphaNumToken { $$=$1; }
@@ -142,6 +143,7 @@ statement
| relationStatement LABEL { $1.title = yy.cleanupLabel($2); yy.addRelation($1); } | relationStatement LABEL { $1.title = yy.cleanupLabel($2); yy.addRelation($1); }
| classStatement | classStatement
| methodStatement | methodStatement
| annotationStatement
; ;
classStatement 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);} | 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 members
: MEMBER { $$ = [$1]; } : MEMBER { $$ = [$1]; }
| MEMBER members { $2.push($1);$$=$2;} | MEMBER members { $2.push($1);$$=$2;}

View File

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