mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-20 07:49:43 +02:00
Merge branch 'feature/963_class_annotations' of https://github.com/chris579/mermaid into develop
This commit is contained in:
@@ -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,
|
||||
|
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -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;
|
||||
|
@@ -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;}
|
||||
|
@@ -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';
|
||||
|
||||
|
@@ -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) {
|
||||
|
@@ -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');
|
||||
});
|
||||
});
|
||||
|
@@ -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 {
|
||||
|
Reference in New Issue
Block a user