1179 Add ability to use generics for members

Created new class to handle parsing of members with regex to handle determining type of member and the different elements within.  Also moved addTSpan in drawClass method to this new file.  Finally, I added a "catch all" section in case something fails in the regex to make sure everything gets formatted correctly.

Added more tests and documentation

updating gitignore

Tired of constantly having to ignore files and stash/pop when switching between branches
This commit is contained in:
Justin Greywolf
2020-01-13 16:04:26 -08:00
parent 425b071a50
commit 587592449a
7 changed files with 414 additions and 77 deletions

6
.gitignore vendored
View File

@@ -12,3 +12,9 @@ yarn-error.log
token
package-lock.json
dist/classTest.html
dist/sequenceTest.html
.vscode/

View File

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

View File

@@ -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()*`

View File

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

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

View 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('');
});
});
});

View File

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