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
commit 4ce523f33b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 205 additions and 61 deletions

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
)
);
}

8
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>
@ -443,7 +449,7 @@ Class08 <--> C2: Cool label
const testLineEndings = (test, input) => {
try {
mermaid.render(test, input, () => {});
} catch (err) {
} catch (err) {
console.error("Error in %s:\n\n%s", test, err);
}
};

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.
* @param id
* @param text
* @param type
* @param style
* @public
*/
export const addClass = function(id) {
if (typeof classes[id] === 'undefined') {
classes[id] = {
id: id,
methods: [],
members: []
};
}
// Only add class if not exists
if (typeof classes[id] !== 'undefined') return;
classes[id] = {
id: id,
methods: [],
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

@ -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 {