Merge pull request #932 from knsv/i931_code_standard

I931 code standard
This commit is contained in:
Knut Sveidqvist 2019-09-12 22:18:46 +02:00 committed by GitHub
commit 4d1a34661e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 6561 additions and 5868 deletions

18
.eslintrc.json Normal file
View File

@ -0,0 +1,18 @@
{
"env": {
"browser": true,
"es6": true
},
"parserOptions": {
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
},
"sourceType": "module"
},
"extends": ["prettier"],
"plugins": ["prettier"],
"rules": {
"prettier/prettier": ["error"]
}
}

4
.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"printWidth": 100,
"singleQuote": true
}

View File

@ -18,7 +18,7 @@
"build:watch": "yarn build --watch", "build:watch": "yarn build --watch",
"minify": "minify ./dist/mermaid.js > ./dist/mermaid.min.js", "minify": "minify ./dist/mermaid.js > ./dist/mermaid.min.js",
"release": "yarn build -p --config webpack.config.prod.babel.js", "release": "yarn build -p --config webpack.config.prod.babel.js",
"lint": "standard", "lint": "eslint src",
"e2e:depr": "yarn lint && jest e2e --config e2e/jest.config.js", "e2e:depr": "yarn lint && jest e2e --config e2e/jest.config.js",
"cypress": "percy exec -- cypress run", "cypress": "percy exec -- cypress run",
"e2e": "start-server-and-test dev http://localhost:9000/ cypress", "e2e": "start-server-and-test dev http://localhost:9000/ cypress",
@ -51,11 +51,15 @@
"dagre-d3-renderer": "^0.5.8", "dagre-d3-renderer": "^0.5.8",
"dagre-layout": "^0.8.8", "dagre-layout": "^0.8.8",
"documentation": "^12.0.1", "documentation": "^12.0.1",
"eslint": "^6.3.0",
"eslint-config-prettier": "^6.3.0",
"eslint-plugin-prettier": "^3.1.0",
"graphlibrary": "^2.2.0", "graphlibrary": "^2.2.0",
"he": "^1.2.0", "he": "^1.2.0",
"lodash": "^4.17.11", "lodash": "^4.17.11",
"minify": "^4.1.1", "minify": "^4.1.1",
"moment-mini": "^2.22.1", "moment-mini": "^2.22.1",
"prettier": "^1.18.2",
"scope-css": "^1.2.1" "scope-css": "^1.2.1"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,28 +1,27 @@
let config = { let config = {};
}
const setConf = function(cnf) { const setConf = function(cnf) {
// Top level initially mermaid, gflow, sequenceDiagram and gantt // Top level initially mermaid, gflow, sequenceDiagram and gantt
const lvl1Keys = Object.keys(cnf) const lvl1Keys = Object.keys(cnf);
for (let i = 0; i < lvl1Keys.length; i++) { for (let i = 0; i < lvl1Keys.length; i++) {
if (typeof cnf[lvl1Keys[i]] === 'object' && cnf[lvl1Keys[i]] != null) { if (typeof cnf[lvl1Keys[i]] === 'object' && cnf[lvl1Keys[i]] != null) {
const lvl2Keys = Object.keys(cnf[lvl1Keys[i]]) const lvl2Keys = Object.keys(cnf[lvl1Keys[i]]);
for (let j = 0; j < lvl2Keys.length; j++) { for (let j = 0; j < lvl2Keys.length; j++) {
// logger.debug('Setting conf ', lvl1Keys[i], '-', lvl2Keys[j]) // logger.debug('Setting conf ', lvl1Keys[i], '-', lvl2Keys[j])
if (typeof config[lvl1Keys[i]] === 'undefined') { if (typeof config[lvl1Keys[i]] === 'undefined') {
config[lvl1Keys[i]] = {} config[lvl1Keys[i]] = {};
} }
// logger.debug('Setting config: ' + lvl1Keys[i] + ' ' + lvl2Keys[j] + ' to ' + cnf[lvl1Keys[i]][lvl2Keys[j]]) // logger.debug('Setting config: ' + lvl1Keys[i] + ' ' + lvl2Keys[j] + ' to ' + cnf[lvl1Keys[i]][lvl2Keys[j]])
config[lvl1Keys[i]][lvl2Keys[j]] = cnf[lvl1Keys[i]][lvl2Keys[j]] config[lvl1Keys[i]][lvl2Keys[j]] = cnf[lvl1Keys[i]][lvl2Keys[j]];
} }
} else { } else {
config[lvl1Keys[i]] = cnf[lvl1Keys[i]] config[lvl1Keys[i]] = cnf[lvl1Keys[i]];
}
} }
} }
};
export const setConfig = conf => { export const setConfig = conf => {
setConf(conf) setConf(conf);
} };
export const getConfig = () => config export const getConfig = () => config;

View File

@ -1,8 +1,7 @@
import { logger } from '../../logger';
import { logger } from '../../logger' let relations = [];
let classes = {};
let relations = []
let classes = {}
/** /**
* Function called by parser when a node definition has been found. * Function called by parser when a node definition has been found.
@ -17,69 +16,69 @@ export const addClass = function (id) {
id: id, id: id,
methods: [], methods: [],
members: [] members: []
};
} }
} };
}
export const clear = function() { export const clear = function() {
relations = [] relations = [];
classes = {} classes = {};
} };
export const getClass = function(id) { export const getClass = function(id) {
return classes[id] return classes[id];
} };
export const getClasses = function() { export const getClasses = function() {
return classes return classes;
} };
export const getRelations = function() { export const getRelations = function() {
return relations return relations;
} };
export const addRelation = function(relation) { export const addRelation = function(relation) {
logger.debug('Adding relation: ' + JSON.stringify(relation)) logger.debug('Adding relation: ' + JSON.stringify(relation));
addClass(relation.id1) addClass(relation.id1);
addClass(relation.id2) addClass(relation.id2);
relations.push(relation) relations.push(relation);
} };
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) === ')') { if (member.substr(-1) === ')') {
theClass.methods.push(member) theClass.methods.push(member);
} else { } else {
theClass.members.push(member) theClass.members.push(member);
}
} }
} }
};
export const addMembers = function(className, MembersArr) { export const addMembers = function(className, MembersArr) {
if (Array.isArray(MembersArr)) { if (Array.isArray(MembersArr)) {
MembersArr.forEach(member => addMember(className, member)) MembersArr.forEach(member => addMember(className, member));
}
} }
};
export const cleanupLabel = function(label) { export const cleanupLabel = function(label) {
if (label.substring(0, 1) === ':') { if (label.substring(0, 1) === ':') {
return label.substr(2).trim() return label.substr(2).trim();
} else { } else {
return label.trim() return label.trim();
}
} }
};
export const lineType = { export const lineType = {
LINE: 0, LINE: 0,
DOTTED_LINE: 1 DOTTED_LINE: 1
} };
export const relationType = { export const relationType = {
AGGREGATION: 0, AGGREGATION: 0,
EXTENSION: 1, EXTENSION: 1,
COMPOSITION: 2, COMPOSITION: 2,
DEPENDENCY: 3 DEPENDENCY: 3
} };
export default { export default {
addClass, addClass,
@ -93,4 +92,4 @@ export default {
cleanupLabel, cleanupLabel,
lineType, lineType,
relationType relationType
} };

View File

@ -1,63 +1,69 @@
/* eslint-env jasmine */ /* eslint-env jasmine */
import { parser } from './parser/classDiagram' import { parser } from './parser/classDiagram';
import classDb from './classDb' import classDb from './classDb';
describe('class diagram, ', function() { describe('class diagram, ', function() {
describe('when parsing an info graph it', function() { describe('when parsing an info graph it', function() {
beforeEach(function() { beforeEach(function() {
parser.yy = classDb parser.yy = classDb;
}) });
it('should handle relation definitions', function() { it('should handle relation definitions', function() {
const str = 'classDiagram\n' + const str =
'classDiagram\n' +
'Class01 <|-- Class02\n' + 'Class01 <|-- Class02\n' +
'Class03 *-- Class04\n' + 'Class03 *-- Class04\n' +
'Class05 o-- Class06\n' + 'Class05 o-- Class06\n' +
'Class07 .. Class08\n' + 'Class07 .. Class08\n' +
'Class09 -- Class1' 'Class09 -- Class1';
parser.parse(str) parser.parse(str);
}) });
it('should handle relation definition of different types and directions', function() { it('should handle relation definition of different types and directions', function() {
const str = 'classDiagram\n' + const str =
'classDiagram\n' +
'Class11 <|.. Class12\n' + 'Class11 <|.. Class12\n' +
'Class13 --> Class14\n' + 'Class13 --> Class14\n' +
'Class15 ..> Class16\n' + 'Class15 ..> Class16\n' +
'Class17 ..|> Class18\n' + 'Class17 ..|> Class18\n' +
'Class19 <--* Class20' 'Class19 <--* Class20';
parser.parse(str) parser.parse(str);
}) });
it('should handle cardinality and labels', function() { it('should handle cardinality and labels', function() {
const str = 'classDiagram\n' + const str =
'classDiagram\n' +
'Class01 "1" *-- "many" Class02 : contains\n' + 'Class01 "1" *-- "many" Class02 : contains\n' +
'Class03 o-- Class04 : aggregation\n' + 'Class03 o-- Class04 : aggregation\n' +
'Class05 --> "1" Class06' 'Class05 --> "1" Class06';
parser.parse(str) parser.parse(str);
}) });
it('should handle class definitions', function() { it('should handle class definitions', function() {
const str = 'classDiagram\n' + const str =
'classDiagram\n' +
'class Car\n' + 'class Car\n' +
'Driver -- Car : drives >\n' + 'Driver -- Car : drives >\n' +
'Car *-- Wheel : have 4 >\n' + 'Car *-- Wheel : have 4 >\n' +
'Car -- Person : < owns' 'Car -- Person : < owns';
parser.parse(str) parser.parse(str);
}) });
it('should handle method statements', function() { it('should handle method statements', function() {
const str = 'classDiagram\n' + const str =
'classDiagram\n' +
'Object <|-- ArrayList\n' + 'Object <|-- ArrayList\n' +
'Object : equals()\n' + 'Object : equals()\n' +
'ArrayList : Object[] elementData\n' + 'ArrayList : Object[] elementData\n' +
'ArrayList : size()' 'ArrayList : size()';
parser.parse(str) parser.parse(str);
}) });
it('should handle parsing of method statements grouped by brackets', function() { it('should handle parsing of method statements grouped by brackets', function() {
const str = 'classDiagram\n' + const str =
'classDiagram\n' +
'class Dummy {\n' + 'class Dummy {\n' +
'String data\n' + 'String data\n' +
' void methods()\n' + ' void methods()\n' +
@ -66,13 +72,14 @@ describe('class diagram, ', function () {
'class Flight {\n' + 'class Flight {\n' +
' flightNumber : Integer\n' + ' flightNumber : Integer\n' +
' departureTime : Date\n' + ' departureTime : Date\n' +
'}' '}';
parser.parse(str) parser.parse(str);
}) });
it('should handle parsing of separators', function() { it('should handle parsing of separators', function() {
const str = 'classDiagram\n' + const str =
'classDiagram\n' +
'class Foo1 {\n' + 'class Foo1 {\n' +
' You can use\n' + ' You can use\n' +
' several lines\n' + ' several lines\n' +
@ -98,111 +105,107 @@ describe('class diagram, ', function () {
'int age\n' + 'int age\n' +
'-- encrypted --\n' + '-- encrypted --\n' +
'String password\n' + 'String password\n' +
'}' '}';
parser.parse(str) parser.parse(str);
}) });
}) });
describe('when fetching data from an classDiagram graph it', function() { describe('when fetching data from an classDiagram graph it', function() {
beforeEach(function() { beforeEach(function() {
parser.yy = classDb parser.yy = classDb;
parser.yy.clear() parser.yy.clear();
}) });
it('should handle relation definitions EXTENSION', function() { it('should handle relation definitions EXTENSION', function() {
const str = 'classDiagram\n' + const str = 'classDiagram\n' + 'Class01 <|-- Class02';
'Class01 <|-- Class02'
parser.parse(str) parser.parse(str);
const relations = parser.yy.getRelations() const relations = parser.yy.getRelations();
expect(parser.yy.getClass('Class01').id).toBe('Class01') expect(parser.yy.getClass('Class01').id).toBe('Class01');
expect(parser.yy.getClass('Class02').id).toBe('Class02') expect(parser.yy.getClass('Class02').id).toBe('Class02');
expect(relations[0].relation.type1).toBe(classDb.relationType.EXTENSION) expect(relations[0].relation.type1).toBe(classDb.relationType.EXTENSION);
expect(relations[0].relation.type2).toBe('none') expect(relations[0].relation.type2).toBe('none');
expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE) expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE);
}) });
it('should handle relation definitions AGGREGATION and dotted line', function() { it('should handle relation definitions AGGREGATION and dotted line', function() {
const str = 'classDiagram\n' + const str = 'classDiagram\n' + 'Class01 o.. Class02';
'Class01 o.. Class02'
parser.parse(str) parser.parse(str);
const relations = parser.yy.getRelations() const relations = parser.yy.getRelations();
expect(parser.yy.getClass('Class01').id).toBe('Class01') expect(parser.yy.getClass('Class01').id).toBe('Class01');
expect(parser.yy.getClass('Class02').id).toBe('Class02') expect(parser.yy.getClass('Class02').id).toBe('Class02');
expect(relations[0].relation.type1).toBe(classDb.relationType.AGGREGATION) expect(relations[0].relation.type1).toBe(classDb.relationType.AGGREGATION);
expect(relations[0].relation.type2).toBe('none') expect(relations[0].relation.type2).toBe('none');
expect(relations[0].relation.lineType).toBe(classDb.lineType.DOTTED_LINE) expect(relations[0].relation.lineType).toBe(classDb.lineType.DOTTED_LINE);
}) });
it('should handle relation definitions COMPOSITION on both sides', function() { it('should handle relation definitions COMPOSITION on both sides', function() {
const str = 'classDiagram\n' + const str = 'classDiagram\n' + 'Class01 *--* Class02';
'Class01 *--* Class02'
parser.parse(str) parser.parse(str);
const relations = parser.yy.getRelations() const relations = parser.yy.getRelations();
expect(parser.yy.getClass('Class01').id).toBe('Class01') expect(parser.yy.getClass('Class01').id).toBe('Class01');
expect(parser.yy.getClass('Class02').id).toBe('Class02') expect(parser.yy.getClass('Class02').id).toBe('Class02');
expect(relations[0].relation.type1).toBe(classDb.relationType.COMPOSITION) expect(relations[0].relation.type1).toBe(classDb.relationType.COMPOSITION);
expect(relations[0].relation.type2).toBe(classDb.relationType.COMPOSITION) expect(relations[0].relation.type2).toBe(classDb.relationType.COMPOSITION);
expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE) expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE);
}) });
it('should handle relation definitions no types', function() { it('should handle relation definitions no types', function() {
const str = 'classDiagram\n' + const str = 'classDiagram\n' + 'Class01 -- Class02';
'Class01 -- Class02'
parser.parse(str) parser.parse(str);
const relations = parser.yy.getRelations() const relations = parser.yy.getRelations();
expect(parser.yy.getClass('Class01').id).toBe('Class01') expect(parser.yy.getClass('Class01').id).toBe('Class01');
expect(parser.yy.getClass('Class02').id).toBe('Class02') expect(parser.yy.getClass('Class02').id).toBe('Class02');
expect(relations[0].relation.type1).toBe('none') expect(relations[0].relation.type1).toBe('none');
expect(relations[0].relation.type2).toBe('none') expect(relations[0].relation.type2).toBe('none');
expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE) expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE);
}) });
it('should handle relation definitions with type only on right side', function() { it('should handle relation definitions with type only on right side', function() {
const str = 'classDiagram\n' + const str = 'classDiagram\n' + 'Class01 --|> Class02';
'Class01 --|> Class02'
parser.parse(str) parser.parse(str);
const relations = parser.yy.getRelations() const relations = parser.yy.getRelations();
expect(parser.yy.getClass('Class01').id).toBe('Class01') expect(parser.yy.getClass('Class01').id).toBe('Class01');
expect(parser.yy.getClass('Class02').id).toBe('Class02') expect(parser.yy.getClass('Class02').id).toBe('Class02');
expect(relations[0].relation.type1).toBe('none') expect(relations[0].relation.type1).toBe('none');
expect(relations[0].relation.type2).toBe(classDb.relationType.EXTENSION) expect(relations[0].relation.type2).toBe(classDb.relationType.EXTENSION);
expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE) expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE);
}) });
it('should handle multiple classes and relation definitions', function() { it('should handle multiple classes and relation definitions', function() {
const str = 'classDiagram\n' + const str =
'classDiagram\n' +
'Class01 <|-- Class02\n' + 'Class01 <|-- Class02\n' +
'Class03 *-- Class04\n' + 'Class03 *-- Class04\n' +
'Class05 o-- Class06\n' + 'Class05 o-- Class06\n' +
'Class07 .. Class08\n' + 'Class07 .. Class08\n' +
'Class09 -- Class10' 'Class09 -- Class10';
parser.parse(str) parser.parse(str);
const relations = parser.yy.getRelations() const relations = parser.yy.getRelations();
expect(parser.yy.getClass('Class01').id).toBe('Class01') expect(parser.yy.getClass('Class01').id).toBe('Class01');
expect(parser.yy.getClass('Class10').id).toBe('Class10') expect(parser.yy.getClass('Class10').id).toBe('Class10');
expect(relations.length).toBe(5) expect(relations.length).toBe(5);
expect(relations[0].relation.type1).toBe(classDb.relationType.EXTENSION) expect(relations[0].relation.type1).toBe(classDb.relationType.EXTENSION);
expect(relations[0].relation.type2).toBe('none') expect(relations[0].relation.type2).toBe('none');
expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE) expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE);
expect(relations[3].relation.type1).toBe('none') expect(relations[3].relation.type1).toBe('none');
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);
}) });
}) });
}) });

View File

@ -1,33 +1,33 @@
import * as d3 from 'd3' import * as d3 from 'd3';
import dagre from 'dagre-layout' import dagre from 'dagre-layout';
import graphlib from 'graphlibrary' import graphlib from 'graphlibrary';
import { logger } from '../../logger' import { logger } from '../../logger';
import classDb from './classDb' import classDb from './classDb';
import { parser } from './parser/classDiagram' import { parser } from './parser/classDiagram';
parser.yy = classDb parser.yy = classDb;
const idCache = {} const idCache = {};
let classCnt = 0 let classCnt = 0;
const conf = { const conf = {
dividerMargin: 10, dividerMargin: 10,
padding: 5, padding: 5,
textHeight: 10 textHeight: 10
} };
// Todo optimize // Todo optimize
const getGraphId = function(label) { const getGraphId = function(label) {
const keys = Object.keys(idCache) const keys = Object.keys(idCache);
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {
if (idCache[keys[i]].label === label) { if (idCache[keys[i]].label === label) {
return keys[i] return keys[i];
} }
} }
return undefined return undefined;
} };
/** /**
* Setup arrow head and define the marker. The result is appended to the svg. * Setup arrow head and define the marker. The result is appended to the svg.
@ -44,7 +44,7 @@ const insertMarkers = function (elem) {
.attr('markerHeight', 240) .attr('markerHeight', 240)
.attr('orient', 'auto') .attr('orient', 'auto')
.append('path') .append('path')
.attr('d', 'M 1,7 L18,13 V 1 Z') .attr('d', 'M 1,7 L18,13 V 1 Z');
elem elem
.append('defs') .append('defs')
@ -56,7 +56,7 @@ const insertMarkers = function (elem) {
.attr('markerHeight', 28) .attr('markerHeight', 28)
.attr('orient', 'auto') .attr('orient', 'auto')
.append('path') .append('path')
.attr('d', 'M 1,1 V 13 L18,7 Z') // this is actual shape for arrowhead .attr('d', 'M 1,1 V 13 L18,7 Z'); // this is actual shape for arrowhead
elem elem
.append('defs') .append('defs')
@ -69,7 +69,7 @@ const insertMarkers = function (elem) {
.attr('markerHeight', 240) .attr('markerHeight', 240)
.attr('orient', 'auto') .attr('orient', 'auto')
.append('path') .append('path')
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z') .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
elem elem
.append('defs') .append('defs')
@ -81,7 +81,7 @@ const insertMarkers = function (elem) {
.attr('markerHeight', 28) .attr('markerHeight', 28)
.attr('orient', 'auto') .attr('orient', 'auto')
.append('path') .append('path')
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z') .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
elem elem
.append('defs') .append('defs')
@ -94,7 +94,7 @@ const insertMarkers = function (elem) {
.attr('markerHeight', 240) .attr('markerHeight', 240)
.attr('orient', 'auto') .attr('orient', 'auto')
.append('path') .append('path')
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z') .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
elem elem
.append('defs') .append('defs')
@ -106,7 +106,7 @@ const insertMarkers = function (elem) {
.attr('markerHeight', 28) .attr('markerHeight', 28)
.attr('orient', 'auto') .attr('orient', 'auto')
.append('path') .append('path')
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z') .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
elem elem
.append('defs') .append('defs')
@ -119,7 +119,7 @@ const insertMarkers = function (elem) {
.attr('markerHeight', 240) .attr('markerHeight', 240)
.attr('orient', 'auto') .attr('orient', 'auto')
.append('path') .append('path')
.attr('d', 'M 5,7 L9,13 L1,7 L9,1 Z') .attr('d', 'M 5,7 L9,13 L1,7 L9,1 Z');
elem elem
.append('defs') .append('defs')
@ -131,96 +131,86 @@ const insertMarkers = function (elem) {
.attr('markerHeight', 28) .attr('markerHeight', 28)
.attr('orient', 'auto') .attr('orient', 'auto')
.append('path') .append('path')
.attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z') .attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z');
} };
let edgeCount = 0 let edgeCount = 0;
let total = 0 let total = 0;
const drawEdge = function(elem, path, relation) { const drawEdge = function(elem, path, relation) {
const getRelationType = function(type) { const getRelationType = function(type) {
switch (type) { switch (type) {
case classDb.relationType.AGGREGATION: case classDb.relationType.AGGREGATION:
return 'aggregation' return 'aggregation';
case classDb.relationType.EXTENSION: case classDb.relationType.EXTENSION:
return 'extension' return 'extension';
case classDb.relationType.COMPOSITION: case classDb.relationType.COMPOSITION:
return 'composition' return 'composition';
case classDb.relationType.DEPENDENCY: case classDb.relationType.DEPENDENCY:
return 'dependency' return 'dependency';
}
} }
};
path.points = path.points.filter(p => !Number.isNaN(p.y)) path.points = path.points.filter(p => !Number.isNaN(p.y));
// The data for our line // The data for our line
const lineData = path.points const lineData = path.points;
// This is the accessor function we talked about above // This is the accessor function we talked about above
const lineFunction = d3 const lineFunction = d3
.line() .line()
.x(function(d) { .x(function(d) {
return d.x return d.x;
}) })
.y(function(d) { .y(function(d) {
return d.y return d.y;
}) })
.curve(d3.curveBasis) .curve(d3.curveBasis);
const svgPath = elem const svgPath = elem
.append('path') .append('path')
.attr('d', lineFunction(lineData)) .attr('d', lineFunction(lineData))
.attr('id', 'edge' + edgeCount) .attr('id', 'edge' + edgeCount)
.attr('class', 'relation') .attr('class', 'relation');
let url = '' let url = '';
if (conf.arrowMarkerAbsolute) { if (conf.arrowMarkerAbsolute) {
url = url =
window.location.protocol + window.location.protocol +
'//' + '//' +
window.location.host + window.location.host +
window.location.pathname + window.location.pathname +
window.location.search window.location.search;
url = url.replace(/\(/g, '\\(') url = url.replace(/\(/g, '\\(');
url = url.replace(/\)/g, '\\)') url = url.replace(/\)/g, '\\)');
} }
if (relation.relation.type1 !== 'none') { if (relation.relation.type1 !== 'none') {
svgPath.attr( svgPath.attr(
'marker-start', 'marker-start',
'url(' + 'url(' + url + '#' + getRelationType(relation.relation.type1) + 'Start' + ')'
url + );
'#' +
getRelationType(relation.relation.type1) +
'Start' +
')'
)
} }
if (relation.relation.type2 !== 'none') { if (relation.relation.type2 !== 'none') {
svgPath.attr( svgPath.attr(
'marker-end', 'marker-end',
'url(' + 'url(' + url + '#' + getRelationType(relation.relation.type2) + 'End' + ')'
url + );
'#' +
getRelationType(relation.relation.type2) +
'End' +
')'
)
} }
let x, y let x, y;
const l = path.points.length const l = path.points.length;
if (l % 2 !== 0 && l > 1) { if (l % 2 !== 0 && l > 1) {
const p1 = path.points[Math.floor(l / 2)] const p1 = path.points[Math.floor(l / 2)];
const p2 = path.points[Math.ceil(l / 2)] const p2 = path.points[Math.ceil(l / 2)];
x = (p1.x + p2.x) / 2 x = (p1.x + p2.x) / 2;
y = (p1.y + p2.y) / 2 y = (p1.y + p2.y) / 2;
} else { } else {
const p = path.points[Math.floor(l / 2)] const p = path.points[Math.floor(l / 2)];
x = p.x x = p.x;
y = p.y y = p.y;
} }
if (typeof relation.title !== 'undefined') { if (typeof relation.title !== 'undefined') {
const g = elem.append('g').attr('class', 'classLabel') const g = elem.append('g').attr('class', 'classLabel');
const label = g const label = g
.append('text') .append('text')
.attr('class', 'label') .attr('class', 'label')
@ -228,189 +218,177 @@ const drawEdge = function (elem, path, relation) {
.attr('y', y) .attr('y', y)
.attr('fill', 'red') .attr('fill', 'red')
.attr('text-anchor', 'middle') .attr('text-anchor', 'middle')
.text(relation.title) .text(relation.title);
window.label = label window.label = label;
const bounds = label.node().getBBox() const bounds = label.node().getBBox();
g.insert('rect', ':first-child') g.insert('rect', ':first-child')
.attr('class', 'box') .attr('class', 'box')
.attr('x', bounds.x - conf.padding / 2) .attr('x', bounds.x - conf.padding / 2)
.attr('y', bounds.y - conf.padding / 2) .attr('y', bounds.y - conf.padding / 2)
.attr('width', bounds.width + conf.padding) .attr('width', bounds.width + conf.padding)
.attr('height', bounds.height + conf.padding) .attr('height', bounds.height + conf.padding);
} }
edgeCount++ edgeCount++;
} };
const drawClass = function(elem, classDef) { const drawClass = function(elem, classDef) {
logger.info('Rendering class ' + classDef) logger.info('Rendering class ' + classDef);
const addTspan = function(textEl, txt, isFirst) { const addTspan = function(textEl, txt, isFirst) {
const tSpan = textEl const tSpan = textEl
.append('tspan') .append('tspan')
.attr('x', conf.padding) .attr('x', conf.padding)
.text(txt) .text(txt);
if (!isFirst) { if (!isFirst) {
tSpan.attr('dy', conf.textHeight) tSpan.attr('dy', conf.textHeight);
}
} }
};
const id = 'classId' + (classCnt % total) const id = 'classId' + (classCnt % total);
const classInfo = { const classInfo = {
id: id, id: id,
label: classDef.id, label: classDef.id,
width: 0, width: 0,
height: 0 height: 0
} };
const g = elem const g = elem
.append('g') .append('g')
.attr('id', id) .attr('id', id)
.attr('class', 'classGroup') .attr('class', 'classGroup');
const title = g const title = g
.append('text') .append('text')
.attr('x', conf.padding) .attr('x', conf.padding)
.attr('y', conf.textHeight + conf.padding) .attr('y', conf.textHeight + conf.padding)
.text(classDef.id) .text(classDef.id);
const titleHeight = title.node().getBBox().height const titleHeight = title.node().getBBox().height;
const membersLine = g const membersLine = g
.append('line') // text label for the x axis .append('line') // text label for the x axis
.attr('x1', 0) .attr('x1', 0)
.attr('y1', conf.padding + titleHeight + conf.dividerMargin / 2) .attr('y1', conf.padding + titleHeight + conf.dividerMargin / 2)
.attr('y2', conf.padding + titleHeight + conf.dividerMargin / 2) .attr('y2', conf.padding + titleHeight + conf.dividerMargin / 2);
const members = g const members = g
.append('text') // text label for the x axis .append('text') // text label for the x axis
.attr('x', conf.padding) .attr('x', conf.padding)
.attr('y', titleHeight + conf.dividerMargin + conf.textHeight) .attr('y', titleHeight + conf.dividerMargin + conf.textHeight)
.attr('fill', 'white') .attr('fill', 'white')
.attr('class', 'classText') .attr('class', 'classText');
let isFirst = true let isFirst = true;
classDef.members.forEach(function(member) { classDef.members.forEach(function(member) {
addTspan(members, member, isFirst) addTspan(members, member, isFirst);
isFirst = false isFirst = false;
}) });
const membersBox = members.node().getBBox() const membersBox = members.node().getBBox();
const methodsLine = g const methodsLine = g
.append('line') // text label for the x axis .append('line') // text label for the x axis
.attr('x1', 0) .attr('x1', 0)
.attr( .attr('y1', conf.padding + titleHeight + conf.dividerMargin + membersBox.height)
'y1', .attr('y2', conf.padding + titleHeight + conf.dividerMargin + membersBox.height);
conf.padding + titleHeight + conf.dividerMargin + membersBox.height
)
.attr(
'y2',
conf.padding + titleHeight + conf.dividerMargin + membersBox.height
)
const methods = g const methods = g
.append('text') // text label for the x axis .append('text') // text label for the x axis
.attr('x', conf.padding) .attr('x', conf.padding)
.attr( .attr('y', titleHeight + 2 * conf.dividerMargin + membersBox.height + conf.textHeight)
'y',
titleHeight + 2 * conf.dividerMargin + membersBox.height + conf.textHeight
)
.attr('fill', 'white') .attr('fill', 'white')
.attr('class', 'classText') .attr('class', 'classText');
isFirst = true isFirst = true;
classDef.methods.forEach(function(method) { classDef.methods.forEach(function(method) {
addTspan(methods, method, isFirst) addTspan(methods, method, isFirst);
isFirst = false isFirst = false;
}) });
const classBox = g.node().getBBox() const classBox = g.node().getBBox();
g.insert('rect', ':first-child') 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) membersLine.attr('x2', classBox.width + 2 * conf.padding);
methodsLine.attr('x2', classBox.width + 2 * conf.padding) methodsLine.attr('x2', classBox.width + 2 * conf.padding);
classInfo.width = classBox.width + 2 * conf.padding classInfo.width = classBox.width + 2 * conf.padding;
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;
classCnt++ classCnt++;
return classInfo return classInfo;
} };
export const setConf = function(cnf) { export const setConf = function(cnf) {
const keys = Object.keys(cnf) const keys = Object.keys(cnf);
keys.forEach(function(key) { keys.forEach(function(key) {
conf[key] = cnf[key] conf[key] = cnf[key];
}) });
} };
/** /**
* Draws a flowchart in the tag with id: id based on the graph definition in text. * Draws a flowchart in the tag with id: id based on the graph definition in text.
* @param text * @param text
* @param id * @param id
*/ */
export const draw = function(text, id) { export const draw = function(text, id) {
parser.yy.clear() parser.yy.clear();
parser.parse(text) parser.parse(text);
logger.info('Rendering diagram ' + text) logger.info('Rendering diagram ' + text);
/// / Fetch the default direction, use TD if none was found /// / Fetch the default direction, use TD if none was found
const diagram = d3.select(`[id='${id}']`) const diagram = d3.select(`[id='${id}']`);
insertMarkers(diagram) insertMarkers(diagram);
// Layout graph, Create a new directed graph // Layout graph, Create a new directed graph
const g = new graphlib.Graph({ const g = new graphlib.Graph({
multigraph: true multigraph: true
}) });
// Set an object for the graph label // Set an object for the graph label
g.setGraph({ g.setGraph({
isMultiGraph: true isMultiGraph: true
}) });
// Default to assigning a new object as a label for each new edge. // Default to assigning a new object as a label for each new edge.
g.setDefaultEdgeLabel(function() { g.setDefaultEdgeLabel(function() {
return {} return {};
}) });
const classes = classDb.getClasses() const classes = classDb.getClasses();
const keys = Object.keys(classes) const keys = Object.keys(classes);
total = keys.length total = keys.length;
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {
const classDef = classes[keys[i]] const classDef = classes[keys[i]];
const node = drawClass(diagram, classDef) const node = drawClass(diagram, classDef);
// Add nodes to the graph. The first argument is the node id. The second is // Add nodes to the graph. The first argument is the node id. The second is
// metadata about the node. In this case we're going to add labels to each of // metadata about the node. In this case we're going to add labels to each of
// our nodes. // our nodes.
g.setNode(node.id, node) g.setNode(node.id, node);
logger.info('Org height: ' + node.height) logger.info('Org height: ' + node.height);
} }
const relations = classDb.getRelations() const relations = classDb.getRelations();
relations.forEach(function(relation) { relations.forEach(function(relation) {
logger.info( logger.info(
'tjoho' + 'tjoho' + getGraphId(relation.id1) + getGraphId(relation.id2) + JSON.stringify(relation)
getGraphId(relation.id1) + );
getGraphId(relation.id2) +
JSON.stringify(relation)
)
g.setEdge(getGraphId(relation.id1), getGraphId(relation.id2), { g.setEdge(getGraphId(relation.id1), getGraphId(relation.id2), {
relation: relation relation: relation
}) });
}) });
dagre.layout(g) dagre.layout(g);
g.nodes().forEach(function(v) { g.nodes().forEach(function(v) {
if (typeof v !== 'undefined' && typeof g.node(v) !== 'undefined') { if (typeof v !== 'undefined' && typeof g.node(v) !== 'undefined') {
logger.debug('Node ' + v + ': ' + JSON.stringify(g.node(v))) logger.debug('Node ' + v + ': ' + JSON.stringify(g.node(v)));
d3.select('#' + v).attr( d3.select('#' + v).attr(
'transform', 'transform',
'translate(' + 'translate(' +
@ -418,27 +396,22 @@ export const draw = function (text, id) {
',' + ',' +
(g.node(v).y - g.node(v).height / 2) + (g.node(v).y - g.node(v).height / 2) +
' )' ' )'
) );
} }
}) });
g.edges().forEach(function(e) { g.edges().forEach(function(e) {
if (typeof e !== 'undefined' && typeof g.edge(e) !== 'undefined') { if (typeof e !== 'undefined' && typeof g.edge(e) !== 'undefined') {
logger.debug( logger.debug('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(g.edge(e)));
'Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(g.edge(e)) drawEdge(diagram, g.edge(e), g.edge(e).relation);
)
drawEdge(diagram, g.edge(e), g.edge(e).relation)
} }
}) });
diagram.attr('height', '100%') diagram.attr('height', '100%');
diagram.attr('width', '100%') diagram.attr('width', '100%');
diagram.attr( diagram.attr('viewBox', '0 0 ' + (g.graph().width + 20) + ' ' + (g.graph().height + 20));
'viewBox', };
'0 0 ' + (g.graph().width + 20) + ' ' + (g.graph().height + 20)
)
}
export default { export default {
setConf, setConf,
draw draw
} };

View File

@ -1,33 +1,33 @@
import * as d3 from 'd3' import * as d3 from 'd3';
import { sanitizeUrl } from '@braintree/sanitize-url' import { sanitizeUrl } from '@braintree/sanitize-url';
import { logger } from '../../logger' import { logger } from '../../logger';
import utils from '../../utils' import utils from '../../utils';
import { getConfig } from '../../config' import { getConfig } from '../../config';
const config = getConfig() const config = getConfig();
let vertices = {} let vertices = {};
let edges = [] let edges = [];
let classes = [] let classes = [];
let subGraphs = [] let subGraphs = [];
let subGraphLookup = {} let subGraphLookup = {};
let tooltips = {} let tooltips = {};
let subCount = 0 let subCount = 0;
let direction let direction;
// Functions to be run after graph rendering // Functions to be run after graph rendering
let funs = [] let funs = [];
const sanitize = text => { const sanitize = text => {
let txt = text let txt = text;
if (config.securityLevel !== 'loose') { if (config.securityLevel !== 'loose') {
txt = txt.replace(/<br>/g, '#br#') txt = txt.replace(/<br>/g, '#br#');
txt = txt.replace(/<br\S*?\/>/g, '#br#') txt = txt.replace(/<br\S*?\/>/g, '#br#');
txt = txt.replace(/</g, '&lt;').replace(/>/g, '&gt;') txt = txt.replace(/</g, '&lt;').replace(/>/g, '&gt;');
txt = txt.replace(/=/g, '&equals;') txt = txt.replace(/=/g, '&equals;');
txt = txt.replace(/#br#/g, '<br/>') txt = txt.replace(/#br#/g, '<br/>');
} }
return txt return txt;
} };
/** /**
* Function called by parser when a node definition has been found * Function called by parser when a node definition has been found
@ -38,52 +38,52 @@ const sanitize = text => {
* @param classes * @param classes
*/ */
export const addVertex = function(_id, text, type, style, classes) { export const addVertex = function(_id, text, type, style, classes) {
let txt let txt;
let id = _id let id = _id;
if (typeof id === 'undefined') { if (typeof id === 'undefined') {
return return;
} }
if (id.trim().length === 0) { if (id.trim().length === 0) {
return return;
} }
if (id[0].match(/\d/)) id = 's' + id if (id[0].match(/\d/)) id = 's' + id;
if (typeof vertices[id] === 'undefined') { if (typeof vertices[id] === 'undefined') {
vertices[id] = { id: id, styles: [], classes: [] } vertices[id] = { id: id, styles: [], classes: [] };
} }
if (typeof text !== 'undefined') { if (typeof text !== 'undefined') {
txt = sanitize(text.trim()) txt = sanitize(text.trim());
// strip quotes if string starts and exnds with a quote // strip quotes if string starts and exnds with a quote
if (txt[0] === '"' && txt[txt.length - 1] === '"') { if (txt[0] === '"' && txt[txt.length - 1] === '"') {
txt = txt.substring(1, txt.length - 1) txt = txt.substring(1, txt.length - 1);
} }
vertices[id].text = txt vertices[id].text = txt;
} else { } else {
if (!vertices[id].text) { if (!vertices[id].text) {
vertices[id].text = _id vertices[id].text = _id;
} }
} }
if (typeof type !== 'undefined') { if (typeof type !== 'undefined') {
vertices[id].type = type vertices[id].type = type;
} }
if (typeof style !== 'undefined') { if (typeof style !== 'undefined') {
if (style !== null) { if (style !== null) {
style.forEach(function(s) { style.forEach(function(s) {
vertices[id].styles.push(s) vertices[id].styles.push(s);
}) });
} }
} }
if (typeof classes !== 'undefined') { if (typeof classes !== 'undefined') {
if (classes !== null) { if (classes !== null) {
classes.forEach(function(s) { classes.forEach(function(s) {
vertices[id].classes.push(s) vertices[id].classes.push(s);
}) });
}
} }
} }
};
/** /**
* Function called by parser when a link/edge definition has been found * Function called by parser when a link/edge definition has been found
@ -93,30 +93,30 @@ export const addVertex = function (_id, text, type, style, classes) {
* @param linktext * @param linktext
*/ */
export const addLink = function(_start, _end, type, linktext) { export const addLink = function(_start, _end, type, linktext) {
let start = _start let start = _start;
let end = _end let end = _end;
if (start[0].match(/\d/)) start = 's' + start if (start[0].match(/\d/)) start = 's' + start;
if (end[0].match(/\d/)) end = 's' + end if (end[0].match(/\d/)) end = 's' + end;
logger.info('Got edge...', start, end) logger.info('Got edge...', start, end);
const edge = { start: start, end: end, type: undefined, text: '' } const edge = { start: start, end: end, type: undefined, text: '' };
linktext = type.text linktext = type.text;
if (typeof linktext !== 'undefined') { if (typeof linktext !== 'undefined') {
edge.text = sanitize(linktext.trim()) edge.text = sanitize(linktext.trim());
// strip quotes if string starts and exnds with a quote // strip quotes if string starts and exnds with a quote
if (edge.text[0] === '"' && edge.text[edge.text.length - 1] === '"') { if (edge.text[0] === '"' && edge.text[edge.text.length - 1] === '"') {
edge.text = edge.text.substring(1, edge.text.length - 1) edge.text = edge.text.substring(1, edge.text.length - 1);
} }
} }
if (typeof type !== 'undefined') { if (typeof type !== 'undefined') {
edge.type = type.type edge.type = type.type;
edge.stroke = type.stroke edge.stroke = type.stroke;
}
edges.push(edge)
} }
edges.push(edge);
};
/** /**
* Updates a link's line interpolation algorithm * Updates a link's line interpolation algorithm
@ -126,12 +126,12 @@ export const addLink = function (_start, _end, type, linktext) {
export const updateLinkInterpolate = function(positions, interp) { export const updateLinkInterpolate = function(positions, interp) {
positions.forEach(function(pos) { positions.forEach(function(pos) {
if (pos === 'default') { if (pos === 'default') {
edges.defaultInterpolate = interp edges.defaultInterpolate = interp;
} else { } else {
edges[pos].interpolate = interp edges[pos].interpolate = interp;
}
})
} }
});
};
/** /**
* Updates a link with a style * Updates a link with a style
@ -141,37 +141,37 @@ export const updateLinkInterpolate = function (positions, interp) {
export const updateLink = function(positions, style) { export const updateLink = function(positions, style) {
positions.forEach(function(pos) { positions.forEach(function(pos) {
if (pos === 'default') { if (pos === 'default') {
edges.defaultStyle = style edges.defaultStyle = style;
} else { } else {
if (utils.isSubstringInArray('fill', style) === -1) { if (utils.isSubstringInArray('fill', style) === -1) {
style.push('fill:none') style.push('fill:none');
} }
edges[pos].style = style edges[pos].style = style;
}
})
} }
});
};
export const addClass = function(id, style) { export const addClass = function(id, style) {
if (typeof classes[id] === 'undefined') { if (typeof classes[id] === 'undefined') {
classes[id] = { id: id, styles: [] } classes[id] = { id: id, styles: [] };
} }
if (typeof style !== 'undefined') { if (typeof style !== 'undefined') {
if (style !== null) { if (style !== null) {
style.forEach(function(s) { style.forEach(function(s) {
classes[id].styles.push(s) classes[id].styles.push(s);
}) });
}
} }
} }
};
/** /**
* Called by parser when a graph definition is found, stores the direction of the chart. * Called by parser when a graph definition is found, stores the direction of the chart.
* @param dir * @param dir
*/ */
export const setDirection = function(dir) { export const setDirection = function(dir) {
direction = dir direction = dir;
} };
/** /**
* Called by parser when a special node is found, e.g. a clickable element. * Called by parser when a special node is found, e.g. a clickable element.
@ -180,46 +180,50 @@ export const setDirection = function (dir) {
*/ */
export const setClass = function(ids, className) { export const setClass = function(ids, className) {
ids.split(',').forEach(function(_id) { ids.split(',').forEach(function(_id) {
let id = _id let id = _id;
if (_id[0].match(/\d/)) id = 's' + id if (_id[0].match(/\d/)) id = 's' + id;
if (typeof vertices[id] !== 'undefined') { if (typeof vertices[id] !== 'undefined') {
vertices[id].classes.push(className) vertices[id].classes.push(className);
} }
if (typeof subGraphLookup[id] !== 'undefined') { if (typeof subGraphLookup[id] !== 'undefined') {
subGraphLookup[id].classes.push(className) subGraphLookup[id].classes.push(className);
}
})
} }
});
};
const setTooltip = function(ids, tooltip) { const setTooltip = function(ids, tooltip) {
ids.split(',').forEach(function(id) { ids.split(',').forEach(function(id) {
if (typeof tooltip !== 'undefined') { if (typeof tooltip !== 'undefined') {
tooltips[id] = sanitize(tooltip) tooltips[id] = sanitize(tooltip);
}
})
} }
});
};
const setClickFun = function(_id, functionName) { const setClickFun = function(_id, functionName) {
let id = _id let id = _id;
if (_id[0].match(/\d/)) id = 's' + id if (_id[0].match(/\d/)) id = 's' + id;
if (config.securityLevel !== 'loose') { if (config.securityLevel !== 'loose') {
return return;
} }
if (typeof functionName === 'undefined') { if (typeof functionName === 'undefined') {
return return;
} }
if (typeof vertices[id] !== 'undefined') { if (typeof vertices[id] !== 'undefined') {
funs.push(function(element) { funs.push(function(element) {
const elem = document.querySelector(`[id="${id}"]`) const elem = document.querySelector(`[id="${id}"]`);
if (elem !== null) { if (elem !== null) {
elem.addEventListener('click', function () { elem.addEventListener(
window[functionName](id) 'click',
}, false) function() {
} window[functionName](id);
}) },
false
);
} }
});
} }
};
/** /**
* Called by parser when a link is found. Adds the URL to the vertex data. * Called by parser when a link is found. Adds the URL to the vertex data.
@ -229,22 +233,22 @@ const setClickFun = function (_id, functionName) {
*/ */
export const setLink = function(ids, linkStr, tooltip) { export const setLink = function(ids, linkStr, tooltip) {
ids.split(',').forEach(function(_id) { ids.split(',').forEach(function(_id) {
let id = _id let id = _id;
if (_id[0].match(/\d/)) id = 's' + id if (_id[0].match(/\d/)) id = 's' + id;
if (typeof vertices[id] !== 'undefined') { if (typeof vertices[id] !== 'undefined') {
if (config.securityLevel !== 'loose') { if (config.securityLevel !== 'loose') {
vertices[id].link = sanitizeUrl(linkStr) // .replace(/javascript:.*/g, '') vertices[id].link = sanitizeUrl(linkStr); // .replace(/javascript:.*/g, '')
} else { } else {
vertices[id].link = linkStr vertices[id].link = linkStr;
} }
} }
}) });
setTooltip(ids, tooltip) setTooltip(ids, tooltip);
setClass(ids, 'clickable') setClass(ids, 'clickable');
} };
export const getTooltip = function(id) { export const getTooltip = function(id) {
return tooltips[id] return tooltips[id];
} };
/** /**
* Called by parser when a click definition is found. Registers an event handler. * Called by parser when a click definition is found. Registers an event handler.
@ -253,208 +257,218 @@ export const getTooltip = function (id) {
* @param tooltip Tooltip for the clickable element * @param tooltip Tooltip for the clickable element
*/ */
export const setClickEvent = function(ids, functionName, tooltip) { export const setClickEvent = function(ids, functionName, tooltip) {
ids.split(',').forEach(function (id) { setClickFun(id, functionName) }) ids.split(',').forEach(function(id) {
setTooltip(ids, tooltip) setClickFun(id, functionName);
setClass(ids, 'clickable') });
} setTooltip(ids, tooltip);
setClass(ids, 'clickable');
};
export const bindFunctions = function(element) { export const bindFunctions = function(element) {
funs.forEach(function(fun) { funs.forEach(function(fun) {
fun(element) fun(element);
}) });
} };
export const getDirection = function() { export const getDirection = function() {
return direction return direction;
} };
/** /**
* Retrieval function for fetching the found nodes after parsing has completed. * Retrieval function for fetching the found nodes after parsing has completed.
* @returns {{}|*|vertices} * @returns {{}|*|vertices}
*/ */
export const getVertices = function() { export const getVertices = function() {
return vertices return vertices;
} };
/** /**
* Retrieval function for fetching the found links after parsing has completed. * Retrieval function for fetching the found links after parsing has completed.
* @returns {{}|*|edges} * @returns {{}|*|edges}
*/ */
export const getEdges = function() { export const getEdges = function() {
return edges return edges;
} };
/** /**
* Retrieval function for fetching the found class definitions after parsing has completed. * Retrieval function for fetching the found class definitions after parsing has completed.
* @returns {{}|*|classes} * @returns {{}|*|classes}
*/ */
export const getClasses = function() { export const getClasses = function() {
return classes return classes;
} };
const setupToolTips = function(element) { const setupToolTips = function(element) {
let tooltipElem = d3.select('.mermaidTooltip') let tooltipElem = d3.select('.mermaidTooltip');
if ((tooltipElem._groups || tooltipElem)[0][0] === null) { if ((tooltipElem._groups || tooltipElem)[0][0] === null) {
tooltipElem = d3.select('body') tooltipElem = d3
.select('body')
.append('div') .append('div')
.attr('class', 'mermaidTooltip') .attr('class', 'mermaidTooltip')
.style('opacity', 0) .style('opacity', 0);
} }
const svg = d3.select(element).select('svg') const svg = d3.select(element).select('svg');
const nodes = svg.selectAll('g.node') const nodes = svg.selectAll('g.node');
nodes nodes
.on('mouseover', function() { .on('mouseover', function() {
const el = d3.select(this) const el = d3.select(this);
const title = el.attr('title') const title = el.attr('title');
// Dont try to draw a tooltip if no data is provided // Dont try to draw a tooltip if no data is provided
if (title === null) { if (title === null) {
return return;
} }
const rect = this.getBoundingClientRect() const rect = this.getBoundingClientRect();
tooltipElem.transition() tooltipElem
.transition()
.duration(200) .duration(200)
.style('opacity', '.9') .style('opacity', '.9');
tooltipElem.html(el.attr('title')) tooltipElem
.style('left', (rect.left + (rect.right - rect.left) / 2) + 'px') .html(el.attr('title'))
.style('top', (rect.top - 14 + document.body.scrollTop) + 'px') .style('left', rect.left + (rect.right - rect.left) / 2 + 'px')
el.classed('hover', true) .style('top', rect.top - 14 + document.body.scrollTop + 'px');
el.classed('hover', true);
}) })
.on('mouseout', function() { .on('mouseout', function() {
tooltipElem.transition() tooltipElem
.transition()
.duration(500) .duration(500)
.style('opacity', 0) .style('opacity', 0);
const el = d3.select(this) const el = d3.select(this);
el.classed('hover', false) el.classed('hover', false);
}) });
} };
funs.push(setupToolTips) funs.push(setupToolTips);
/** /**
* Clears the internal graph db so that a new graph can be parsed. * Clears the internal graph db so that a new graph can be parsed.
*/ */
export const clear = function() { export const clear = function() {
vertices = {} vertices = {};
classes = {} classes = {};
edges = [] edges = [];
funs = [] funs = [];
funs.push(setupToolTips) funs.push(setupToolTips);
subGraphs = [] subGraphs = [];
subGraphLookup = {} subGraphLookup = {};
subCount = 0 subCount = 0;
tooltips = [] tooltips = [];
} };
/** /**
* *
* @returns {string} * @returns {string}
*/ */
export const defaultStyle = function() { export const defaultStyle = function() {
return 'fill:#ffa;stroke: #f66; stroke-width: 3px; stroke-dasharray: 5, 5;fill:#ffa;stroke: #666;' return 'fill:#ffa;stroke: #f66; stroke-width: 3px; stroke-dasharray: 5, 5;fill:#ffa;stroke: #666;';
} };
/** /**
* Clears the internal graph db so that a new graph can be parsed. * Clears the internal graph db so that a new graph can be parsed.
*/ */
export const addSubGraph = function(_id, list, _title) { export const addSubGraph = function(_id, list, _title) {
let id = _id let id = _id;
let title = _title let title = _title;
if (_id === _title && _title.match(/\s/)) { if (_id === _title && _title.match(/\s/)) {
id = undefined id = undefined;
} }
function uniq(a) { function uniq(a) {
const prims = { 'boolean': {}, 'number': {}, 'string': {} } const prims = { boolean: {}, number: {}, string: {} };
const objs = [] const objs = [];
return a.filter(function(item) { return a.filter(function(item) {
const type = typeof item const type = typeof item;
if (item.trim() === '') { if (item.trim() === '') {
return false return false;
} }
if (type in prims) { return prims[type].hasOwnProperty(item) ? false : (prims[type][item] = true) } else { return objs.indexOf(item) >= 0 ? false : objs.push(item) } if (type in prims) {
}) return prims[type].hasOwnProperty(item) ? false : (prims[type][item] = true);
} else {
return objs.indexOf(item) >= 0 ? false : objs.push(item);
}
});
} }
let nodeList = [] let nodeList = [];
nodeList = uniq(nodeList.concat.apply(nodeList, list)) nodeList = uniq(nodeList.concat.apply(nodeList, list));
for (let i = 0; i < nodeList.length; i++) { for (let i = 0; i < nodeList.length; i++) {
if (nodeList[i][0].match(/\d/)) nodeList[i] = 's' + nodeList[i] if (nodeList[i][0].match(/\d/)) nodeList[i] = 's' + nodeList[i];
} }
id = id || ('subGraph' + subCount) id = id || 'subGraph' + subCount;
if (id[0].match(/\d/)) id = 's' + id if (id[0].match(/\d/)) id = 's' + id;
title = title || '' title = title || '';
title = sanitize(title) title = sanitize(title);
subCount = subCount + 1 subCount = subCount + 1;
const subGraph = { id: id, nodes: nodeList, title: title.trim(), classes: [] } const subGraph = { id: id, nodes: nodeList, title: title.trim(), classes: [] };
subGraphs.push(subGraph) subGraphs.push(subGraph);
subGraphLookup[id] = subGraph subGraphLookup[id] = subGraph;
return id return id;
} };
const getPosForId = function(id) { const getPosForId = function(id) {
for (let i = 0; i < subGraphs.length; i++) { for (let i = 0; i < subGraphs.length; i++) {
if (subGraphs[i].id === id) { if (subGraphs[i].id === id) {
return i return i;
} }
} }
return -1 return -1;
} };
let secCount = -1 let secCount = -1;
const posCrossRef = [] const posCrossRef = [];
const indexNodes2 = function(id, pos) { const indexNodes2 = function(id, pos) {
const nodes = subGraphs[pos].nodes const nodes = subGraphs[pos].nodes;
secCount = secCount + 1 secCount = secCount + 1;
if (secCount > 2000) { if (secCount > 2000) {
return return;
} }
posCrossRef[secCount] = pos posCrossRef[secCount] = pos;
// Check if match // Check if match
if (subGraphs[pos].id === id) { if (subGraphs[pos].id === id) {
return { return {
result: true, result: true,
count: 0 count: 0
} };
} }
let count = 0 let count = 0;
let posCount = 1 let posCount = 1;
while (count < nodes.length) { while (count < nodes.length) {
const childPos = getPosForId(nodes[count]) const childPos = getPosForId(nodes[count]);
// Ignore regular nodes (pos will be -1) // Ignore regular nodes (pos will be -1)
if (childPos >= 0) { if (childPos >= 0) {
const res = indexNodes2(id, childPos) const res = indexNodes2(id, childPos);
if (res.result) { if (res.result) {
return { return {
result: true, result: true,
count: posCount + res.count count: posCount + res.count
} };
} else { } else {
posCount = posCount + res.count posCount = posCount + res.count;
} }
} }
count = count + 1 count = count + 1;
} }
return { return {
result: false, result: false,
count: posCount count: posCount
} };
} };
export const getDepthFirstPos = function(pos) { export const getDepthFirstPos = function(pos) {
return posCrossRef[pos] return posCrossRef[pos];
} };
export const indexNodes = function() { export const indexNodes = function() {
secCount = -1 secCount = -1;
if (subGraphs.length > 0) { if (subGraphs.length > 0) {
indexNodes2('none', subGraphs.length - 1, 0) indexNodes2('none', subGraphs.length - 1, 0);
}
} }
};
export const getSubGraphs = function() { export const getSubGraphs = function() {
return subGraphs return subGraphs;
} };
export default { export default {
addVertex, addVertex,
@ -478,4 +492,4 @@ export default {
getDepthFirstPos, getDepthFirstPos,
indexNodes, indexNodes,
getSubGraphs getSubGraphs
} };

View File

@ -1,22 +1,21 @@
import graphlib from 'graphlibrary' import graphlib from 'graphlibrary';
import * as d3 from 'd3' import * as d3 from 'd3';
import flowDb from './flowDb' import flowDb from './flowDb';
import flow from './parser/flow' import flow from './parser/flow';
import { getConfig } from '../../config' import { getConfig } from '../../config';
import dagreD3 from 'dagre-d3-renderer' import dagreD3 from 'dagre-d3-renderer';
import addHtmlLabel from 'dagre-d3-renderer/lib/label/add-html-label.js' import addHtmlLabel from 'dagre-d3-renderer/lib/label/add-html-label.js';
import { logger } from '../../logger' import { logger } from '../../logger';
import { interpolateToCurve } from '../../utils' import { interpolateToCurve } from '../../utils';
const conf = { const conf = {};
}
export const setConf = function(cnf) { export const setConf = function(cnf) {
const keys = Object.keys(cnf) const keys = Object.keys(cnf);
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {
conf[keys[i]] = cnf[keys[i]] conf[keys[i]] = cnf[keys[i]];
}
} }
};
/** /**
* Function that adds the vertices found in the graph definition to the graph to be rendered. * Function that adds the vertices found in the graph definition to the graph to be rendered.
@ -24,133 +23,148 @@ export const setConf = function (cnf) {
* @param g The graph that is to be drawn. * @param g The graph that is to be drawn.
*/ */
export const addVertices = function(vert, g, svgId) { export const addVertices = function(vert, g, svgId) {
const svg = d3.select(`[id="${svgId}"]`) const svg = d3.select(`[id="${svgId}"]`);
const keys = Object.keys(vert) const keys = Object.keys(vert);
const styleFromStyleArr = function(styleStr, arr, { label }) { const styleFromStyleArr = function(styleStr, arr, { label }) {
if (!label) { if (!label) {
// Create a compound style definition from the style definitions found for the node in the graph definition // Create a compound style definition from the style definitions found for the node in the graph definition
for (let i = 0; i < arr.length; i++) { for (let i = 0; i < arr.length; i++) {
if (typeof arr[i] !== 'undefined') { if (typeof arr[i] !== 'undefined') {
styleStr = styleStr + arr[i] + ';' styleStr = styleStr + arr[i] + ';';
} }
} }
} else { } else {
for (let i = 0; i < arr.length; i++) { for (let i = 0; i < arr.length; i++) {
if (typeof arr[i] !== 'undefined') { if (typeof arr[i] !== 'undefined') {
if (arr[i].match('^color:')) styleStr = styleStr + arr[i] + ';' if (arr[i].match('^color:')) styleStr = styleStr + arr[i] + ';';
} }
} }
} }
return styleStr return styleStr;
} };
// Iterate through each item in the vertex object (containing all the vertices found) in the graph definition // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
keys.forEach(function(id) { keys.forEach(function(id) {
const vertex = vert[id] const vertex = vert[id];
/** /**
* Variable for storing the classes for the vertex * Variable for storing the classes for the vertex
* @type {string} * @type {string}
*/ */
let classStr = '' let classStr = '';
if (vertex.classes.length > 0) { if (vertex.classes.length > 0) {
classStr = vertex.classes.join(' ') classStr = vertex.classes.join(' ');
} }
/** /**
* Variable for storing the extracted style for the vertex * Variable for storing the extracted style for the vertex
* @type {string} * @type {string}
*/ */
let style = '' let style = '';
// Create a compound style definition from the style definitions found for the node in the graph definition // Create a compound style definition from the style definitions found for the node in the graph definition
style = styleFromStyleArr(style, vertex.styles, { label: false }) style = styleFromStyleArr(style, vertex.styles, { label: false });
let labelStyle = '' let labelStyle = '';
labelStyle = styleFromStyleArr(labelStyle, vertex.styles, { label: true }) labelStyle = styleFromStyleArr(labelStyle, vertex.styles, { label: true });
// Use vertex id as text in the box if no text is provided by the graph definition // Use vertex id as text in the box if no text is provided by the graph definition
let vertexText = vertex.text !== undefined ? vertex.text : vertex.id let vertexText = vertex.text !== undefined ? vertex.text : vertex.id;
// We create a SVG label, either by delegating to addHtmlLabel or manually // We create a SVG label, either by delegating to addHtmlLabel or manually
let vertexNode let vertexNode;
if (getConfig().flowchart.htmlLabels) { if (getConfig().flowchart.htmlLabels) {
// TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that? // TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that?
const node = { label: vertexText.replace(/fa[lrsb]?:fa-[\w-]+/g, s => `<i class='${s.replace(':', ' ')}'></i>`) } const node = {
vertexNode = addHtmlLabel(svg, node).node() label: vertexText.replace(
vertexNode.parentNode.removeChild(vertexNode) /fa[lrsb]?:fa-[\w-]+/g,
s => `<i class='${s.replace(':', ' ')}'></i>`
)
};
vertexNode = addHtmlLabel(svg, node).node();
vertexNode.parentNode.removeChild(vertexNode);
} else { } else {
const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text') const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
const rows = vertexText.split(/<br[/]{0,1}>/) const rows = vertexText.split(/<br[/]{0,1}>/);
for (let j = 0; j < rows.length; j++) { for (let j = 0; j < rows.length; j++) {
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan') const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve') tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
tspan.setAttribute('dy', '1em') tspan.setAttribute('dy', '1em');
tspan.setAttribute('x', '1') tspan.setAttribute('x', '1');
tspan.textContent = rows[j] tspan.textContent = rows[j];
svgLabel.appendChild(tspan) svgLabel.appendChild(tspan);
} }
vertexNode = svgLabel vertexNode = svgLabel;
} }
// If the node has a link, we wrap it in a SVG link // If the node has a link, we wrap it in a SVG link
if (vertex.link) { if (vertex.link) {
const link = document.createElementNS('http://www.w3.org/2000/svg', 'a') const link = document.createElementNS('http://www.w3.org/2000/svg', 'a');
link.setAttributeNS('http://www.w3.org/2000/svg', 'href', vertex.link) link.setAttributeNS('http://www.w3.org/2000/svg', 'href', vertex.link);
link.setAttributeNS('http://www.w3.org/2000/svg', 'rel', 'noopener') link.setAttributeNS('http://www.w3.org/2000/svg', 'rel', 'noopener');
link.appendChild(vertexNode) link.appendChild(vertexNode);
vertexNode = link vertexNode = link;
} }
let radious = 0 let radious = 0;
let _shape = '' let _shape = '';
// Set the shape based parameters // Set the shape based parameters
switch (vertex.type) { switch (vertex.type) {
case 'round': case 'round':
radious = 5 radious = 5;
_shape = 'rect' _shape = 'rect';
break break;
case 'square': case 'square':
_shape = 'rect' _shape = 'rect';
break break;
case 'diamond': case 'diamond':
_shape = 'question' _shape = 'question';
break break;
case 'odd': case 'odd':
_shape = 'rect_left_inv_arrow' _shape = 'rect_left_inv_arrow';
break break;
case 'lean_right': case 'lean_right':
_shape = 'lean_right' _shape = 'lean_right';
break break;
case 'lean_left': case 'lean_left':
_shape = 'lean_left' _shape = 'lean_left';
break break;
case 'trapezoid': case 'trapezoid':
_shape = 'trapezoid' _shape = 'trapezoid';
break break;
case 'inv_trapezoid': case 'inv_trapezoid':
_shape = 'inv_trapezoid' _shape = 'inv_trapezoid';
break break;
case 'odd_right': case 'odd_right':
_shape = 'rect_left_inv_arrow' _shape = 'rect_left_inv_arrow';
break break;
case 'circle': case 'circle':
_shape = 'circle' _shape = 'circle';
break break;
case 'ellipse': case 'ellipse':
_shape = 'ellipse' _shape = 'ellipse';
break break;
case 'group': case 'group':
_shape = 'rect' _shape = 'rect';
break break;
default: default:
_shape = 'rect' _shape = 'rect';
} }
// Add the node // Add the node
g.setNode(vertex.id, { labelType: 'svg', labelStyle: labelStyle, shape: _shape, label: vertexNode, rx: radious, ry: radious, 'class': classStr, style: style, id: vertex.id }) g.setNode(vertex.id, {
}) labelType: 'svg',
} labelStyle: labelStyle,
shape: _shape,
label: vertexNode,
rx: radious,
ry: radious,
class: classStr,
style: style,
id: vertex.id
});
});
};
/** /**
* Add edges to graph based on parsed graph defninition * Add edges to graph based on parsed graph defninition
@ -158,94 +172,94 @@ export const addVertices = function (vert, g, svgId) {
* @param {Object} g The graph object * @param {Object} g The graph object
*/ */
export const addEdges = function(edges, g) { export const addEdges = function(edges, g) {
let cnt = 0 let cnt = 0;
let defaultStyle let defaultStyle;
if (typeof edges.defaultStyle !== 'undefined') { if (typeof edges.defaultStyle !== 'undefined') {
defaultStyle = edges.defaultStyle.toString().replace(/,/g, ';') defaultStyle = edges.defaultStyle.toString().replace(/,/g, ';');
} }
edges.forEach(function(edge) { edges.forEach(function(edge) {
cnt++ cnt++;
const edgeData = {} const edgeData = {};
// Set link type for rendering // Set link type for rendering
if (edge.type === 'arrow_open') { if (edge.type === 'arrow_open') {
edgeData.arrowhead = 'none' edgeData.arrowhead = 'none';
} else { } else {
edgeData.arrowhead = 'normal' edgeData.arrowhead = 'normal';
} }
let style = '' let style = '';
if (typeof edge.style !== 'undefined') { if (typeof edge.style !== 'undefined') {
edge.style.forEach(function(s) { edge.style.forEach(function(s) {
style = style + s + ';' style = style + s + ';';
}) });
} else { } else {
switch (edge.stroke) { switch (edge.stroke) {
case 'normal': case 'normal':
style = 'fill:none' style = 'fill:none';
if (typeof defaultStyle !== 'undefined') { if (typeof defaultStyle !== 'undefined') {
style = defaultStyle style = defaultStyle;
} }
break break;
case 'dotted': case 'dotted':
style = 'stroke: #333; fill:none;stroke-width:2px;stroke-dasharray:3;' style = 'stroke: #333; fill:none;stroke-width:2px;stroke-dasharray:3;';
break break;
case 'thick': case 'thick':
style = 'stroke: #333; stroke-width: 3.5px;fill:none' style = 'stroke: #333; stroke-width: 3.5px;fill:none';
break break;
} }
} }
edgeData.style = style edgeData.style = style;
if (typeof edge.interpolate !== 'undefined') { if (typeof edge.interpolate !== 'undefined') {
edgeData.curve = interpolateToCurve(edge.interpolate, d3.curveLinear) edgeData.curve = interpolateToCurve(edge.interpolate, d3.curveLinear);
} else if (typeof edges.defaultInterpolate !== 'undefined') { } else if (typeof edges.defaultInterpolate !== 'undefined') {
edgeData.curve = interpolateToCurve(edges.defaultInterpolate, d3.curveLinear) edgeData.curve = interpolateToCurve(edges.defaultInterpolate, d3.curveLinear);
} else { } else {
edgeData.curve = interpolateToCurve(conf.curve, d3.curveLinear) edgeData.curve = interpolateToCurve(conf.curve, d3.curveLinear);
} }
if (typeof edge.text === 'undefined') { if (typeof edge.text === 'undefined') {
if (typeof edge.style !== 'undefined') { if (typeof edge.style !== 'undefined') {
edgeData.arrowheadStyle = 'fill: #333' edgeData.arrowheadStyle = 'fill: #333';
} }
} else { } else {
edgeData.arrowheadStyle = 'fill: #333' edgeData.arrowheadStyle = 'fill: #333';
if (typeof edge.style === 'undefined') { if (typeof edge.style === 'undefined') {
edgeData.labelpos = 'c' edgeData.labelpos = 'c';
if (getConfig().flowchart.htmlLabels) { if (getConfig().flowchart.htmlLabels) {
edgeData.labelType = 'html' edgeData.labelType = 'html';
edgeData.label = '<span class="edgeLabel">' + edge.text + '</span>' edgeData.label = '<span class="edgeLabel">' + edge.text + '</span>';
} else { } else {
edgeData.labelType = 'text' edgeData.labelType = 'text';
edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none' edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none';
edgeData.label = edge.text.replace(/<br>/g, '\n') edgeData.label = edge.text.replace(/<br>/g, '\n');
} }
} else { } else {
edgeData.label = edge.text.replace(/<br>/g, '\n') edgeData.label = edge.text.replace(/<br>/g, '\n');
} }
} }
// Add the edge to the graph // Add the edge to the graph
g.setEdge(edge.start, edge.end, edgeData, cnt) g.setEdge(edge.start, edge.end, edgeData, cnt);
}) });
} };
/** /**
* Returns the all the styles from classDef statements in the graph definition. * Returns the all the styles from classDef statements in the graph definition.
* @returns {object} classDef styles * @returns {object} classDef styles
*/ */
export const getClasses = function(text) { export const getClasses = function(text) {
logger.info('Extracting classes') logger.info('Extracting classes');
flowDb.clear() flowDb.clear();
const parser = flow.parser const parser = flow.parser;
parser.yy = flowDb parser.yy = flowDb;
// Parse the graph definition // Parse the graph definition
parser.parse(text) parser.parse(text);
return flowDb.getClasses() return flowDb.getClasses();
} };
/** /**
* Draws a flowchart in the tag with id: id based on the graph definition in text. * Draws a flowchart in the tag with id: id based on the graph definition in text.
@ -253,22 +267,22 @@ export const getClasses = function (text) {
* @param id * @param id
*/ */
export const draw = function(text, id) { export const draw = function(text, id) {
logger.info('Drawing flowchart') logger.info('Drawing flowchart');
flowDb.clear() flowDb.clear();
const parser = flow.parser const parser = flow.parser;
parser.yy = flowDb parser.yy = flowDb;
// Parse the graph definition // Parse the graph definition
try { try {
parser.parse(text) parser.parse(text);
} catch (err) { } catch (err) {
logger.debug('Parsing failed') logger.debug('Parsing failed');
} }
// Fetch the default direction, use TD if none was found // Fetch the default direction, use TD if none was found
let dir = flowDb.getDirection() let dir = flowDb.getDirection();
if (typeof dir === 'undefined') { if (typeof dir === 'undefined') {
dir = 'TD' dir = 'TD';
} }
// Create the input mermaid.graph // Create the input mermaid.graph
@ -280,196 +294,238 @@ export const draw = function (text, id) {
rankdir: dir, rankdir: dir,
marginx: 20, marginx: 20,
marginy: 20 marginy: 20
}) })
.setDefaultEdgeLabel(function() { .setDefaultEdgeLabel(function() {
return {} return {};
}) });
let subG let subG;
const subGraphs = flowDb.getSubGraphs() const subGraphs = flowDb.getSubGraphs();
for (let i = subGraphs.length - 1; i >= 0; i--) { for (let i = subGraphs.length - 1; i >= 0; i--) {
subG = subGraphs[i] subG = subGraphs[i];
flowDb.addVertex(subG.id, subG.title, 'group', undefined, subG.classes) flowDb.addVertex(subG.id, subG.title, 'group', undefined, subG.classes);
} }
// Fetch the verices/nodes and edges/links from the parsed graph definition // Fetch the verices/nodes and edges/links from the parsed graph definition
const vert = flowDb.getVertices() const vert = flowDb.getVertices();
const edges = flowDb.getEdges() const edges = flowDb.getEdges();
let i = 0 let i = 0;
for (i = subGraphs.length - 1; i >= 0; i--) { for (i = subGraphs.length - 1; i >= 0; i--) {
subG = subGraphs[i] subG = subGraphs[i];
d3.selectAll('cluster').append('text') d3.selectAll('cluster').append('text');
for (let j = 0; j < subG.nodes.length; j++) { for (let j = 0; j < subG.nodes.length; j++) {
g.setParent(subG.nodes[j], subG.id) g.setParent(subG.nodes[j], subG.id);
} }
} }
addVertices(vert, g, id) addVertices(vert, g, id);
addEdges(edges, g) addEdges(edges, g);
// Create the renderer // Create the renderer
const Render = dagreD3.render const Render = dagreD3.render;
const render = new Render() const render = new Render();
// Add custom shape for rhombus type of boc (decision) // Add custom shape for rhombus type of boc (decision)
render.shapes().question = function(parent, bbox, node) { render.shapes().question = function(parent, bbox, node) {
const w = bbox.width const w = bbox.width;
const h = bbox.height const h = bbox.height;
const s = (w + h) * 0.9 const s = (w + h) * 0.9;
const points = [ const points = [
{ x: s / 2, y: 0 }, { x: s / 2, y: 0 },
{ x: s, y: -s / 2 }, { x: s, y: -s / 2 },
{ x: s / 2, y: -s }, { x: s / 2, y: -s },
{ x: 0, y: -s / 2 } { x: 0, y: -s / 2 }
] ];
const shapeSvg = parent.insert('polygon', ':first-child') const shapeSvg = parent
.attr('points', points.map(function (d) { .insert('polygon', ':first-child')
return d.x + ',' + d.y .attr(
}).join(' ')) 'points',
points
.map(function(d) {
return d.x + ',' + d.y;
})
.join(' ')
)
.attr('rx', 5) .attr('rx', 5)
.attr('ry', 5) .attr('ry', 5)
.attr('transform', 'translate(' + (-s / 2) + ',' + (s * 2 / 4) + ')') .attr('transform', 'translate(' + -s / 2 + ',' + (s * 2) / 4 + ')');
node.intersect = function(point) { node.intersect = function(point) {
return dagreD3.intersect.polygon(node, points, point) return dagreD3.intersect.polygon(node, points, point);
} };
return shapeSvg return shapeSvg;
} };
// Add custom shape for box with inverted arrow on left side // Add custom shape for box with inverted arrow on left side
render.shapes().rect_left_inv_arrow = function(parent, bbox, node) { render.shapes().rect_left_inv_arrow = function(parent, bbox, node) {
const w = bbox.width const w = bbox.width;
const h = bbox.height const h = bbox.height;
const points = [ const points = [
{ x: -h / 2, y: 0 }, { x: -h / 2, y: 0 },
{ x: w, y: 0 }, { x: w, y: 0 },
{ x: w, y: -h }, { x: w, y: -h },
{ x: -h / 2, y: -h }, { x: -h / 2, y: -h },
{ x: 0, y: -h / 2 } { x: 0, y: -h / 2 }
] ];
const shapeSvg = parent.insert('polygon', ':first-child') const shapeSvg = parent
.attr('points', points.map(function (d) { .insert('polygon', ':first-child')
return d.x + ',' + d.y .attr(
}).join(' ')) 'points',
.attr('transform', 'translate(' + (-w / 2) + ',' + (h * 2 / 4) + ')') points
.map(function(d) {
return d.x + ',' + d.y;
})
.join(' ')
)
.attr('transform', 'translate(' + -w / 2 + ',' + (h * 2) / 4 + ')');
node.intersect = function(point) { node.intersect = function(point) {
return dagreD3.intersect.polygon(node, points, point) return dagreD3.intersect.polygon(node, points, point);
} };
return shapeSvg return shapeSvg;
} };
// Add custom shape for box with inverted arrow on left side // Add custom shape for box with inverted arrow on left side
render.shapes().lean_right = function(parent, bbox, node) { render.shapes().lean_right = function(parent, bbox, node) {
const w = bbox.width const w = bbox.width;
const h = bbox.height const h = bbox.height;
const points = [ const points = [
{ x: -2 * h / 6, y: 0 }, { x: (-2 * h) / 6, y: 0 },
{ x: w - h / 6, y: 0 }, { x: w - h / 6, y: 0 },
{ x: w + 2 * h / 6, y: -h }, { x: w + (2 * h) / 6, y: -h },
{ x: h / 6, y: -h } { x: h / 6, y: -h }
] ];
const shapeSvg = parent.insert('polygon', ':first-child') const shapeSvg = parent
.attr('points', points.map(function (d) { .insert('polygon', ':first-child')
return d.x + ',' + d.y .attr(
}).join(' ')) 'points',
.attr('transform', 'translate(' + (-w / 2) + ',' + (h * 2 / 4) + ')') points
.map(function(d) {
return d.x + ',' + d.y;
})
.join(' ')
)
.attr('transform', 'translate(' + -w / 2 + ',' + (h * 2) / 4 + ')');
node.intersect = function(point) { node.intersect = function(point) {
return dagreD3.intersect.polygon(node, points, point) return dagreD3.intersect.polygon(node, points, point);
} };
return shapeSvg return shapeSvg;
} };
// Add custom shape for box with inverted arrow on left side // Add custom shape for box with inverted arrow on left side
render.shapes().lean_left = function(parent, bbox, node) { render.shapes().lean_left = function(parent, bbox, node) {
const w = bbox.width const w = bbox.width;
const h = bbox.height const h = bbox.height;
const points = [ const points = [
{ x: 2 * h / 6, y: 0 }, { x: (2 * h) / 6, y: 0 },
{ x: w + h / 6, y: 0 }, { x: w + h / 6, y: 0 },
{ x: w - 2 * h / 6, y: -h }, { x: w - (2 * h) / 6, y: -h },
{ x: -h / 6, y: -h } { x: -h / 6, y: -h }
] ];
const shapeSvg = parent.insert('polygon', ':first-child') const shapeSvg = parent
.attr('points', points.map(function (d) { .insert('polygon', ':first-child')
return d.x + ',' + d.y .attr(
}).join(' ')) 'points',
.attr('transform', 'translate(' + (-w / 2) + ',' + (h * 2 / 4) + ')') points
.map(function(d) {
return d.x + ',' + d.y;
})
.join(' ')
)
.attr('transform', 'translate(' + -w / 2 + ',' + (h * 2) / 4 + ')');
node.intersect = function(point) { node.intersect = function(point) {
return dagreD3.intersect.polygon(node, points, point) return dagreD3.intersect.polygon(node, points, point);
} };
return shapeSvg return shapeSvg;
} };
// Add custom shape for box with inverted arrow on left side // Add custom shape for box with inverted arrow on left side
render.shapes().trapezoid = function(parent, bbox, node) { render.shapes().trapezoid = function(parent, bbox, node) {
const w = bbox.width const w = bbox.width;
const h = bbox.height const h = bbox.height;
const points = [ const points = [
{ x: -2 * h / 6, y: 0 }, { x: (-2 * h) / 6, y: 0 },
{ x: w + 2 * h / 6, y: 0 }, { x: w + (2 * h) / 6, y: 0 },
{ x: w - h / 6, y: -h }, { x: w - h / 6, y: -h },
{ x: h / 6, y: -h } { x: h / 6, y: -h }
] ];
const shapeSvg = parent.insert('polygon', ':first-child') const shapeSvg = parent
.attr('points', points.map(function (d) { .insert('polygon', ':first-child')
return d.x + ',' + d.y .attr(
}).join(' ')) 'points',
.attr('transform', 'translate(' + (-w / 2) + ',' + (h * 2 / 4) + ')') points
.map(function(d) {
return d.x + ',' + d.y;
})
.join(' ')
)
.attr('transform', 'translate(' + -w / 2 + ',' + (h * 2) / 4 + ')');
node.intersect = function(point) { node.intersect = function(point) {
return dagreD3.intersect.polygon(node, points, point) return dagreD3.intersect.polygon(node, points, point);
} };
return shapeSvg return shapeSvg;
} };
// Add custom shape for box with inverted arrow on left side // Add custom shape for box with inverted arrow on left side
render.shapes().inv_trapezoid = function(parent, bbox, node) { render.shapes().inv_trapezoid = function(parent, bbox, node) {
const w = bbox.width const w = bbox.width;
const h = bbox.height const h = bbox.height;
const points = [ const points = [
{ x: h / 6, y: 0 }, { x: h / 6, y: 0 },
{ x: w - h / 6, y: 0 }, { x: w - h / 6, y: 0 },
{ x: w + 2 * h / 6, y: -h }, { x: w + (2 * h) / 6, y: -h },
{ x: -2 * h / 6, y: -h } { x: (-2 * h) / 6, y: -h }
] ];
const shapeSvg = parent.insert('polygon', ':first-child') const shapeSvg = parent
.attr('points', points.map(function (d) { .insert('polygon', ':first-child')
return d.x + ',' + d.y .attr(
}).join(' ')) 'points',
.attr('transform', 'translate(' + (-w / 2) + ',' + (h * 2 / 4) + ')') points
.map(function(d) {
return d.x + ',' + d.y;
})
.join(' ')
)
.attr('transform', 'translate(' + -w / 2 + ',' + (h * 2) / 4 + ')');
node.intersect = function(point) { node.intersect = function(point) {
return dagreD3.intersect.polygon(node, points, point) return dagreD3.intersect.polygon(node, points, point);
} };
return shapeSvg return shapeSvg;
} };
// Add custom shape for box with inverted arrow on right side // Add custom shape for box with inverted arrow on right side
render.shapes().rect_right_inv_arrow = function(parent, bbox, node) { render.shapes().rect_right_inv_arrow = function(parent, bbox, node) {
const w = bbox.width const w = bbox.width;
const h = bbox.height const h = bbox.height;
const points = [ const points = [
{ x: 0, y: 0 }, { x: 0, y: 0 },
{ x: w + h / 2, y: 0 }, { x: w + h / 2, y: 0 },
{ x: w, y: -h / 2 }, { x: w, y: -h / 2 },
{ x: w + h / 2, y: -h }, { x: w + h / 2, y: -h },
{ x: 0, y: -h } { x: 0, y: -h }
] ];
const shapeSvg = parent.insert('polygon', ':first-child') const shapeSvg = parent
.attr('points', points.map(function (d) { .insert('polygon', ':first-child')
return d.x + ',' + d.y .attr(
}).join(' ')) 'points',
.attr('transform', 'translate(' + (-w / 2) + ',' + (h * 2 / 4) + ')') points
.map(function(d) {
return d.x + ',' + d.y;
})
.join(' ')
)
.attr('transform', 'translate(' + -w / 2 + ',' + (h * 2) / 4 + ')');
node.intersect = function(point) { node.intersect = function(point) {
return dagreD3.intersect.polygon(node, points, point) return dagreD3.intersect.polygon(node, points, point);
} };
return shapeSvg return shapeSvg;
} };
// Add our custom arrow - an empty arrowhead // Add our custom arrow - an empty arrowhead
render.arrows().none = function normal(parent, id, edge, type) { render.arrows().none = function normal(parent, id, edge, type) {
const marker = parent.append('marker') const marker = parent
.append('marker')
.attr('id', id) .attr('id', id)
.attr('viewBox', '0 0 10 10') .attr('viewBox', '0 0 10 10')
.attr('refX', 9) .attr('refX', 9)
@ -477,16 +533,16 @@ export const draw = function (text, id) {
.attr('markerUnits', 'strokeWidth') .attr('markerUnits', 'strokeWidth')
.attr('markerWidth', 8) .attr('markerWidth', 8)
.attr('markerHeight', 6) .attr('markerHeight', 6)
.attr('orient', 'auto') .attr('orient', 'auto');
const path = marker.append('path') const path = marker.append('path').attr('d', 'M 0 0 L 0 0 L 0 0 z');
.attr('d', 'M 0 0 L 0 0 L 0 0 z') dagreD3.util.applyStyle(path, edge[type + 'Style']);
dagreD3.util.applyStyle(path, edge[type + 'Style']) };
}
// Override normal arrowhead defined in d3. Remove style & add class to allow css styling. // Override normal arrowhead defined in d3. Remove style & add class to allow css styling.
render.arrows().normal = function normal(parent, id, edge, type) { render.arrows().normal = function normal(parent, id, edge, type) {
const marker = parent.append('marker') const marker = parent
.append('marker')
.attr('id', id) .attr('id', id)
.attr('viewBox', '0 0 10 10') .attr('viewBox', '0 0 10 10')
.attr('refX', 9) .attr('refX', 9)
@ -494,76 +550,76 @@ export const draw = function (text, id) {
.attr('markerUnits', 'strokeWidth') .attr('markerUnits', 'strokeWidth')
.attr('markerWidth', 8) .attr('markerWidth', 8)
.attr('markerHeight', 6) .attr('markerHeight', 6)
.attr('orient', 'auto') .attr('orient', 'auto');
marker.append('path') marker
.append('path')
.attr('d', 'M 0 0 L 10 5 L 0 10 z') .attr('d', 'M 0 0 L 10 5 L 0 10 z')
.attr('class', 'arrowheadPath') .attr('class', 'arrowheadPath')
.style('stroke-width', 1) .style('stroke-width', 1)
.style('stroke-dasharray', '1,0') .style('stroke-dasharray', '1,0');
} };
// Set up an SVG group so that we can translate the final graph. // Set up an SVG group so that we can translate the final graph.
const svg = d3.select(`[id="${id}"]`) const svg = d3.select(`[id="${id}"]`);
// Run the renderer. This is what draws the final graph. // Run the renderer. This is what draws the final graph.
const element = d3.select('#' + id + ' g') const element = d3.select('#' + id + ' g');
render(element, g) render(element, g);
element.selectAll('g.node') element.selectAll('g.node').attr('title', function() {
.attr('title', function () { return flowDb.getTooltip(this.id);
return flowDb.getTooltip(this.id) });
})
const padding = 8 const padding = 8;
const width = g.maxX - g.minX + padding * 2 const width = g.maxX - g.minX + padding * 2;
const height = g.maxY - g.minY + padding * 2 const height = g.maxY - g.minY + padding * 2;
svg.attr('width', '100%') svg.attr('width', '100%');
svg.attr('style', `max-width: ${width}px;`) svg.attr('style', `max-width: ${width}px;`);
svg.attr('viewBox', `0 0 ${width} ${height}`) svg.attr('viewBox', `0 0 ${width} ${height}`);
svg.select('g').attr('transform', `translate(${padding - g.minX}, ${padding - g.minY})`) svg.select('g').attr('transform', `translate(${padding - g.minX}, ${padding - g.minY})`);
// Index nodes // Index nodes
flowDb.indexNodes('subGraph' + i) flowDb.indexNodes('subGraph' + i);
// reposition labels // reposition labels
for (i = 0; i < subGraphs.length; i++) { for (i = 0; i < subGraphs.length; i++) {
subG = subGraphs[i] subG = subGraphs[i];
if (subG.title !== 'undefined') { if (subG.title !== 'undefined') {
const clusterRects = document.querySelectorAll('#' + id + ' #' + subG.id + ' rect') const clusterRects = document.querySelectorAll('#' + id + ' #' + subG.id + ' rect');
const clusterEl = document.querySelectorAll('#' + id + ' #' + subG.id) const clusterEl = document.querySelectorAll('#' + id + ' #' + subG.id);
const xPos = clusterRects[0].x.baseVal.value const xPos = clusterRects[0].x.baseVal.value;
const yPos = clusterRects[0].y.baseVal.value const yPos = clusterRects[0].y.baseVal.value;
const width = clusterRects[0].width.baseVal.value const width = clusterRects[0].width.baseVal.value;
const cluster = d3.select(clusterEl[0]) const cluster = d3.select(clusterEl[0]);
const te = cluster.select('.label') const te = cluster.select('.label');
te.attr('transform', `translate(${xPos + width / 2}, ${yPos + 14})`) te.attr('transform', `translate(${xPos + width / 2}, ${yPos + 14})`);
te.attr('id', id + 'Text') te.attr('id', id + 'Text');
} }
} }
// Add label rects for non html labels // Add label rects for non html labels
if (!getConfig().flowchart.htmlLabels) { if (!getConfig().flowchart.htmlLabels) {
const labels = document.querySelectorAll('#' + id + ' .edgeLabel .label') const labels = document.querySelectorAll('#' + id + ' .edgeLabel .label');
for (let k = 0; k < labels.length; k++) { for (let k = 0; k < labels.length; k++) {
const label = labels[k] const label = labels[k];
// Get dimensions of label // Get dimensions of label
const dim = label.getBBox() const dim = label.getBBox();
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect') const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('rx', 0) rect.setAttribute('rx', 0);
rect.setAttribute('ry', 0) rect.setAttribute('ry', 0);
rect.setAttribute('width', dim.width) rect.setAttribute('width', dim.width);
rect.setAttribute('height', dim.height) rect.setAttribute('height', dim.height);
rect.setAttribute('style', 'fill:#e8e8e8;') rect.setAttribute('style', 'fill:#e8e8e8;');
label.insertBefore(rect, label.firstChild) label.insertBefore(rect, label.firstChild);
}
} }
} }
};
export default { export default {
setConf, setConf,
@ -571,4 +627,4 @@ export default {
addEdges, addEdges,
getClasses, getClasses,
draw draw
} };

View File

@ -1,16 +1,16 @@
import flowDb from '../flowDb' import flowDb from '../flowDb';
import flow from './flow' import flow from './flow';
import { setConfig } from '../../../config' import { setConfig } from '../../../config';
setConfig({ setConfig({
securityLevel: 'strict', securityLevel: 'strict'
}) });
describe('when parsing flowcharts', function() { describe('when parsing flowcharts', function() {
beforeEach(function() { beforeEach(function() {
flow.parser.yy = flowDb flow.parser.yy = flowDb;
flow.parser.yy.clear() flow.parser.yy.clear();
}) });
it('should handle chaining of vertices', function() { it('should handle chaining of vertices', function() {
const res = flow.parser.parse(` const res = flow.parser.parse(`
@ -18,21 +18,20 @@ describe('when parsing flowcharts', function () {
A-->B-->C; A-->B-->C;
`); `);
const vert = flow.parser.yy.getVertices() const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges() const edges = flow.parser.yy.getEdges();
expect(vert['A'].id).toBe('A') expect(vert['A'].id).toBe('A');
expect(vert['B'].id).toBe('B') expect(vert['B'].id).toBe('B');
expect(vert['C'].id).toBe('C') expect(vert['C'].id).toBe('C');
expect(edges.length).toBe(2) expect(edges.length).toBe(2);
expect(edges[0].start).toBe('A') expect(edges[0].start).toBe('A');
expect(edges[0].end).toBe('B') expect(edges[0].end).toBe('B');
expect(edges[0].type).toBe('arrow') expect(edges[0].type).toBe('arrow');
expect(edges[0].text).toBe('') expect(edges[0].text).toBe('');
expect(edges[1].start).toBe('B') expect(edges[1].start).toBe('B');
expect(edges[1].end).toBe('C') expect(edges[1].end).toBe('C');
expect(edges[1].type).toBe('arrow') expect(edges[1].type).toBe('arrow');
expect(edges[1].text).toBe('') expect(edges[1].text).toBe('');
}) });
});
})

File diff suppressed because it is too large Load Diff

View File

@ -1,74 +1,74 @@
import flowDb from '../flowDb' import flowDb from '../flowDb';
import flow from './flow' import flow from './flow';
import { setConfig } from '../../../config' import { setConfig } from '../../../config';
setConfig({ setConfig({
securityLevel: 'strict', securityLevel: 'strict'
}) });
describe('when parsing subgraphs', function() { describe('when parsing subgraphs', function() {
beforeEach(function() { beforeEach(function() {
flow.parser.yy = flowDb flow.parser.yy = flowDb;
flow.parser.yy.clear() flow.parser.yy.clear();
}) });
it('should handle subgraph with tab indentation', function() { it('should handle subgraph with tab indentation', function() {
const res = flow.parser.parse('graph TB\nsubgraph One\n\ta1-->a2\nend') const res = flow.parser.parse('graph TB\nsubgraph One\n\ta1-->a2\nend');
const subgraphs = flow.parser.yy.getSubGraphs() const subgraphs = flow.parser.yy.getSubGraphs();
expect(subgraphs.length).toBe(1) expect(subgraphs.length).toBe(1);
const subgraph = subgraphs[0] const subgraph = subgraphs[0];
expect(subgraph.nodes.length).toBe(2) expect(subgraph.nodes.length).toBe(2);
expect(subgraph.nodes[0]).toBe('a2') expect(subgraph.nodes[0]).toBe('a2');
expect(subgraph.nodes[1]).toBe('a1') expect(subgraph.nodes[1]).toBe('a1');
expect(subgraph.title).toBe('One') expect(subgraph.title).toBe('One');
expect(subgraph.id).toBe('One') expect(subgraph.id).toBe('One');
}) });
it('should handle subgraph with chaining nodes indentation', function() { it('should handle subgraph with chaining nodes indentation', function() {
const res = flow.parser.parse('graph TB\nsubgraph One\n\ta1-->a2-->a3\nend') const res = flow.parser.parse('graph TB\nsubgraph One\n\ta1-->a2-->a3\nend');
const subgraphs = flow.parser.yy.getSubGraphs() const subgraphs = flow.parser.yy.getSubGraphs();
expect(subgraphs.length).toBe(1) expect(subgraphs.length).toBe(1);
const subgraph = subgraphs[0] const subgraph = subgraphs[0];
expect(subgraph.nodes.length).toBe(3) expect(subgraph.nodes.length).toBe(3);
expect(subgraph.nodes[0]).toBe('a3') expect(subgraph.nodes[0]).toBe('a3');
expect(subgraph.nodes[1]).toBe('a2') expect(subgraph.nodes[1]).toBe('a2');
expect(subgraph.nodes[2]).toBe('a1') expect(subgraph.nodes[2]).toBe('a1');
expect(subgraph.title).toBe('One') expect(subgraph.title).toBe('One');
expect(subgraph.id).toBe('One') expect(subgraph.id).toBe('One');
}) });
it('should handle subgraph with multiple words in title', function() { it('should handle subgraph with multiple words in title', function() {
const res = flow.parser.parse('graph TB\nsubgraph "Some Title"\n\ta1-->a2\nend') const res = flow.parser.parse('graph TB\nsubgraph "Some Title"\n\ta1-->a2\nend');
const subgraphs = flow.parser.yy.getSubGraphs() const subgraphs = flow.parser.yy.getSubGraphs();
expect(subgraphs.length).toBe(1) expect(subgraphs.length).toBe(1);
const subgraph = subgraphs[0] const subgraph = subgraphs[0];
expect(subgraph.nodes.length).toBe(2) expect(subgraph.nodes.length).toBe(2);
expect(subgraph.nodes[0]).toBe('a2') expect(subgraph.nodes[0]).toBe('a2');
expect(subgraph.nodes[1]).toBe('a1') expect(subgraph.nodes[1]).toBe('a1');
expect(subgraph.title).toBe('Some Title') expect(subgraph.title).toBe('Some Title');
expect(subgraph.id).toBe('subGraph0') expect(subgraph.id).toBe('subGraph0');
}); });
it('should handle subgraph with id and title notation', function() { it('should handle subgraph with id and title notation', function() {
const res = flow.parser.parse('graph TB\nsubgraph some-id[Some Title]\n\ta1-->a2\nend') const res = flow.parser.parse('graph TB\nsubgraph some-id[Some Title]\n\ta1-->a2\nend');
const subgraphs = flow.parser.yy.getSubGraphs() const subgraphs = flow.parser.yy.getSubGraphs();
expect(subgraphs.length).toBe(1) expect(subgraphs.length).toBe(1);
const subgraph = subgraphs[0] const subgraph = subgraphs[0];
expect(subgraph.nodes.length).toBe(2) expect(subgraph.nodes.length).toBe(2);
expect(subgraph.nodes[0]).toBe('a2') expect(subgraph.nodes[0]).toBe('a2');
expect(subgraph.nodes[1]).toBe('a1') expect(subgraph.nodes[1]).toBe('a1');
expect(subgraph.title).toBe('Some Title') expect(subgraph.title).toBe('Some Title');
expect(subgraph.id).toBe('some-id') expect(subgraph.id).toBe('some-id');
}); });
xit('should handle subgraph without id and space in title', function() { xit('should handle subgraph without id and space in title', function() {
const res = flow.parser.parse('graph TB\nsubgraph Some Title\n\ta1-->a2\nend') const res = flow.parser.parse('graph TB\nsubgraph Some Title\n\ta1-->a2\nend');
const subgraphs = flow.parser.yy.getSubGraphs() const subgraphs = flow.parser.yy.getSubGraphs();
expect(subgraphs.length).toBe(1) expect(subgraphs.length).toBe(1);
const subgraph = subgraphs[0] const subgraph = subgraphs[0];
expect(subgraph.nodes.length).toBe(2) expect(subgraph.nodes.length).toBe(2);
expect(subgraph.nodes[0]).toBe('a1') expect(subgraph.nodes[0]).toBe('a1');
expect(subgraph.nodes[1]).toBe('a2') expect(subgraph.nodes[1]).toBe('a2');
expect(subgraph.title).toBe('Some Title') expect(subgraph.title).toBe('Some Title');
expect(subgraph.id).toBe('some-id') expect(subgraph.id).toBe('some-id');
}); });
it('should handle subgraph id starting with a number', function() { it('should handle subgraph id starting with a number', function() {
@ -76,148 +76,148 @@ describe('when parsing subgraphs', function () {
A[Christmas] -->|Get money| B(Go shopping) A[Christmas] -->|Get money| B(Go shopping)
subgraph 1test subgraph 1test
A A
end`) end`);
const subgraphs = flow.parser.yy.getSubGraphs() const subgraphs = flow.parser.yy.getSubGraphs();
expect(subgraphs.length).toBe(1) expect(subgraphs.length).toBe(1);
const subgraph = subgraphs[0] const subgraph = subgraphs[0];
expect(subgraph.nodes.length).toBe(1) expect(subgraph.nodes.length).toBe(1);
expect(subgraph.nodes[0]).toBe('A') expect(subgraph.nodes[0]).toBe('A');
expect(subgraph.id).toBe('s1test') expect(subgraph.id).toBe('s1test');
}); });
it('should handle subgraphs1', function() { it('should handle subgraphs1', function() {
const res = flow.parser.parse('graph TD;A-->B;subgraph myTitle;c-->d;end;') const res = flow.parser.parse('graph TD;A-->B;subgraph myTitle;c-->d;end;');
const vert = flow.parser.yy.getVertices() const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges() const edges = flow.parser.yy.getEdges();
expect(edges[0].type).toBe('arrow') expect(edges[0].type).toBe('arrow');
}) });
it('should handle subgraphs with title in quotes', function() { it('should handle subgraphs with title in quotes', function() {
const res = flow.parser.parse('graph TD;A-->B;subgraph "title in quotes";c-->d;end;') const res = flow.parser.parse('graph TD;A-->B;subgraph "title in quotes";c-->d;end;');
const vert = flow.parser.yy.getVertices() const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges() const edges = flow.parser.yy.getEdges();
const subgraphs = flow.parser.yy.getSubGraphs() const subgraphs = flow.parser.yy.getSubGraphs();
expect(subgraphs.length).toBe(1) expect(subgraphs.length).toBe(1);
const subgraph = subgraphs[0] const subgraph = subgraphs[0];
expect(subgraph.title).toBe('title in quotes') expect(subgraph.title).toBe('title in quotes');
expect(edges[0].type).toBe('arrow') expect(edges[0].type).toBe('arrow');
}) });
it('should handle subgraphs in old style that was broken', function() { it('should handle subgraphs in old style that was broken', function() {
const res = flow.parser.parse('graph TD;A-->B;subgraph old style that is broken;c-->d;end;') const res = flow.parser.parse('graph TD;A-->B;subgraph old style that is broken;c-->d;end;');
const vert = flow.parser.yy.getVertices() const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges() const edges = flow.parser.yy.getEdges();
const subgraphs = flow.parser.yy.getSubGraphs() const subgraphs = flow.parser.yy.getSubGraphs();
expect(subgraphs.length).toBe(1) expect(subgraphs.length).toBe(1);
const subgraph = subgraphs[0] const subgraph = subgraphs[0];
expect(subgraph.title).toBe('old style that is broken') expect(subgraph.title).toBe('old style that is broken');
expect(edges[0].type).toBe('arrow') expect(edges[0].type).toBe('arrow');
}) });
it('should handle subgraphs with dashes in the title', function() { it('should handle subgraphs with dashes in the title', function() {
const res = flow.parser.parse('graph TD;A-->B;subgraph a-b-c;c-->d;end;') const res = flow.parser.parse('graph TD;A-->B;subgraph a-b-c;c-->d;end;');
const vert = flow.parser.yy.getVertices() const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges() const edges = flow.parser.yy.getEdges();
const subgraphs = flow.parser.yy.getSubGraphs() const subgraphs = flow.parser.yy.getSubGraphs();
expect(subgraphs.length).toBe(1) expect(subgraphs.length).toBe(1);
const subgraph = subgraphs[0] const subgraph = subgraphs[0];
expect(subgraph.title).toBe('a-b-c') expect(subgraph.title).toBe('a-b-c');
expect(edges[0].type).toBe('arrow') expect(edges[0].type).toBe('arrow');
}) });
it('should handle subgraphs with id and title in brackets', function() { it('should handle subgraphs with id and title in brackets', function() {
const res = flow.parser.parse('graph TD;A-->B;subgraph uid1[text of doom];c-->d;end;') const res = flow.parser.parse('graph TD;A-->B;subgraph uid1[text of doom];c-->d;end;');
const vert = flow.parser.yy.getVertices() const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges() const edges = flow.parser.yy.getEdges();
const subgraphs = flow.parser.yy.getSubGraphs() const subgraphs = flow.parser.yy.getSubGraphs();
expect(subgraphs.length).toBe(1) expect(subgraphs.length).toBe(1);
const subgraph = subgraphs[0] const subgraph = subgraphs[0];
expect(subgraph.title).toBe('text of doom') expect(subgraph.title).toBe('text of doom');
expect(subgraph.id).toBe('uid1') expect(subgraph.id).toBe('uid1');
expect(edges[0].type).toBe('arrow') expect(edges[0].type).toBe('arrow');
}) });
it('should handle subgraphs with id and title in brackets and quotes', function() { it('should handle subgraphs with id and title in brackets and quotes', function() {
const res = flow.parser.parse('graph TD;A-->B;subgraph uid2["text of doom"];c-->d;end;') const res = flow.parser.parse('graph TD;A-->B;subgraph uid2["text of doom"];c-->d;end;');
const vert = flow.parser.yy.getVertices() const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges() const edges = flow.parser.yy.getEdges();
const subgraphs = flow.parser.yy.getSubGraphs() const subgraphs = flow.parser.yy.getSubGraphs();
expect(subgraphs.length).toBe(1) expect(subgraphs.length).toBe(1);
const subgraph = subgraphs[0] const subgraph = subgraphs[0];
expect(subgraph.title).toBe('text of doom') expect(subgraph.title).toBe('text of doom');
expect(subgraph.id).toBe('uid2') expect(subgraph.id).toBe('uid2');
expect(edges[0].type).toBe('arrow') expect(edges[0].type).toBe('arrow');
}) });
it('should handle subgraphs with id and title in brackets without spaces', function() { it('should handle subgraphs with id and title in brackets without spaces', function() {
const res = flow.parser.parse('graph TD;A-->B;subgraph uid2[textofdoom];c-->d;end;') const res = flow.parser.parse('graph TD;A-->B;subgraph uid2[textofdoom];c-->d;end;');
const vert = flow.parser.yy.getVertices() const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges() const edges = flow.parser.yy.getEdges();
const subgraphs = flow.parser.yy.getSubGraphs() const subgraphs = flow.parser.yy.getSubGraphs();
expect(subgraphs.length).toBe(1) expect(subgraphs.length).toBe(1);
const subgraph = subgraphs[0] const subgraph = subgraphs[0];
expect(subgraph.title).toBe('textofdoom') expect(subgraph.title).toBe('textofdoom');
expect(subgraph.id).toBe('uid2') expect(subgraph.id).toBe('uid2');
expect(edges[0].type).toBe('arrow') expect(edges[0].type).toBe('arrow');
}) });
it('should handle subgraphs2', function() { it('should handle subgraphs2', function() {
const res = flow.parser.parse('graph TD\nA-->B\nsubgraph myTitle\n\n c-->d \nend\n') const res = flow.parser.parse('graph TD\nA-->B\nsubgraph myTitle\n\n c-->d \nend\n');
const vert = flow.parser.yy.getVertices() const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges() const edges = flow.parser.yy.getEdges();
expect(edges[0].type).toBe('arrow') expect(edges[0].type).toBe('arrow');
}) });
it('should handle nested subgraphs', function() { it('should handle nested subgraphs', function() {
const str = 'graph TD\n' + const str =
'graph TD\n' +
'A-->B\n' + 'A-->B\n' +
'subgraph myTitle\n\n' + 'subgraph myTitle\n\n' +
' c-->d \n\n' + ' c-->d \n\n' +
' subgraph inner\n\n e-->f \n end \n\n' + ' subgraph inner\n\n e-->f \n end \n\n' +
' subgraph inner\n\n h-->i \n end \n\n' + ' subgraph inner\n\n h-->i \n end \n\n' +
'end\n' 'end\n';
const res = flow.parser.parse(str) const res = flow.parser.parse(str);
}) });
it('should handle subgraphs4', function() { it('should handle subgraphs4', function() {
const res = flow.parser.parse('graph TD\nA-->B\nsubgraph myTitle\nc-->d\nend;') const res = flow.parser.parse('graph TD\nA-->B\nsubgraph myTitle\nc-->d\nend;');
const vert = flow.parser.yy.getVertices() const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges() const edges = flow.parser.yy.getEdges();
expect(edges[0].type).toBe('arrow') expect(edges[0].type).toBe('arrow');
}) });
it('should handle subgraphs5', function() { it('should handle subgraphs5', function() {
const res = flow.parser.parse('graph TD\nA-->B\nsubgraph myTitle\nc-- text -->d\nd-->e\n end;') const res = flow.parser.parse('graph TD\nA-->B\nsubgraph myTitle\nc-- text -->d\nd-->e\n end;');
const vert = flow.parser.yy.getVertices() const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges() const edges = flow.parser.yy.getEdges();
expect(edges[0].type).toBe('arrow') expect(edges[0].type).toBe('arrow');
}) });
});
})

View File

@ -1,217 +1,214 @@
import moment from 'moment-mini' import moment from 'moment-mini';
import { sanitizeUrl } from '@braintree/sanitize-url' import { sanitizeUrl } from '@braintree/sanitize-url';
import { logger } from '../../logger' import { logger } from '../../logger';
import { getConfig } from '../../config' import { getConfig } from '../../config';
const config = getConfig() const config = getConfig();
let dateFormat = '' let dateFormat = '';
let axisFormat = '' let axisFormat = '';
let excludes = [] let excludes = [];
let title = '' let title = '';
let sections = [] let sections = [];
let tasks = [] let tasks = [];
let currentSection = '' let currentSection = '';
const tags = ['active', 'done', 'crit', 'milestone'] const tags = ['active', 'done', 'crit', 'milestone'];
let funs = [] let funs = [];
let inclusiveEndDates = false let inclusiveEndDates = false;
export const clear = function() { export const clear = function() {
sections = [] sections = [];
tasks = [] tasks = [];
currentSection = '' currentSection = '';
funs = [] funs = [];
title = '' title = '';
taskCnt = 0 taskCnt = 0;
lastTask = undefined lastTask = undefined;
lastTaskID = undefined lastTaskID = undefined;
rawTasks = [] rawTasks = [];
dateFormat = '' dateFormat = '';
axisFormat = '' axisFormat = '';
excludes = [] excludes = [];
inclusiveEndDates = false inclusiveEndDates = false;
} };
export const setAxisFormat = function(txt) { export const setAxisFormat = function(txt) {
axisFormat = txt axisFormat = txt;
} };
export const getAxisFormat = function() { export const getAxisFormat = function() {
return axisFormat return axisFormat;
} };
export const setDateFormat = function(txt) { export const setDateFormat = function(txt) {
dateFormat = txt dateFormat = txt;
} };
export const enableInclusiveEndDates = function() { export const enableInclusiveEndDates = function() {
inclusiveEndDates = true inclusiveEndDates = true;
} };
export const endDatesAreInclusive = function() { export const endDatesAreInclusive = function() {
return inclusiveEndDates return inclusiveEndDates;
} };
export const getDateFormat = function() { export const getDateFormat = function() {
return dateFormat return dateFormat;
} };
export const setExcludes = function(txt) { export const setExcludes = function(txt) {
excludes = txt.toLowerCase().split(/[\s,]+/) excludes = txt.toLowerCase().split(/[\s,]+/);
} };
export const getExcludes = function() { export const getExcludes = function() {
return excludes return excludes;
} };
export const setTitle = function(txt) { export const setTitle = function(txt) {
title = txt title = txt;
} };
export const getTitle = function() { export const getTitle = function() {
return title return title;
} };
export const addSection = function(txt) { export const addSection = function(txt) {
currentSection = txt currentSection = txt;
sections.push(txt) sections.push(txt);
} };
export const getSections = function() { export const getSections = function() {
return sections return sections;
} };
export const getTasks = function() { export const getTasks = function() {
let allItemsPricessed = compileTasks() let allItemsPricessed = compileTasks();
const maxDepth = 10 const maxDepth = 10;
let iterationCount = 0 let iterationCount = 0;
while (!allItemsPricessed && (iterationCount < maxDepth)) { while (!allItemsPricessed && iterationCount < maxDepth) {
allItemsPricessed = compileTasks() allItemsPricessed = compileTasks();
iterationCount++ iterationCount++;
} }
tasks = rawTasks tasks = rawTasks;
return tasks return tasks;
} };
const isInvalidDate = function(date, dateFormat, excludes) { const isInvalidDate = function(date, dateFormat, excludes) {
if (date.isoWeekday() >= 6 && excludes.indexOf('weekends') >= 0) { if (date.isoWeekday() >= 6 && excludes.indexOf('weekends') >= 0) {
return true return true;
} }
if (excludes.indexOf(date.format('dddd').toLowerCase()) >= 0) { if (excludes.indexOf(date.format('dddd').toLowerCase()) >= 0) {
return true return true;
}
return excludes.indexOf(date.format(dateFormat.trim())) >= 0
} }
return excludes.indexOf(date.format(dateFormat.trim())) >= 0;
};
const checkTaskDates = function(task, dateFormat, excludes) { const checkTaskDates = function(task, dateFormat, excludes) {
if (!excludes.length || task.manualEndTime) return if (!excludes.length || task.manualEndTime) return;
let startTime = moment(task.startTime, dateFormat, true) let startTime = moment(task.startTime, dateFormat, true);
startTime.add(1, 'd') startTime.add(1, 'd');
let endTime = moment(task.endTime, dateFormat, true) let endTime = moment(task.endTime, dateFormat, true);
let renderEndTime = fixTaskDates(startTime, endTime, dateFormat, excludes) let renderEndTime = fixTaskDates(startTime, endTime, dateFormat, excludes);
task.endTime = endTime.toDate() task.endTime = endTime.toDate();
task.renderEndTime = renderEndTime task.renderEndTime = renderEndTime;
} };
const fixTaskDates = function(startTime, endTime, dateFormat, excludes) { const fixTaskDates = function(startTime, endTime, dateFormat, excludes) {
let invalid = false let invalid = false;
let renderEndTime = null let renderEndTime = null;
while (startTime.date() <= endTime.date()) { while (startTime.date() <= endTime.date()) {
if (!invalid) { if (!invalid) {
renderEndTime = endTime.toDate() renderEndTime = endTime.toDate();
} }
invalid = isInvalidDate(startTime, dateFormat, excludes) invalid = isInvalidDate(startTime, dateFormat, excludes);
if (invalid) { if (invalid) {
endTime.add(1, 'd') endTime.add(1, 'd');
} }
startTime.add(1, 'd') startTime.add(1, 'd');
}
return renderEndTime
} }
return renderEndTime;
};
const getStartDate = function(prevTime, dateFormat, str) { const getStartDate = function(prevTime, dateFormat, str) {
str = str.trim() str = str.trim();
// Test for after // Test for after
const re = /^after\s+([\d\w-]+)/ const re = /^after\s+([\d\w-]+)/;
const afterStatement = re.exec(str.trim()) const afterStatement = re.exec(str.trim());
if (afterStatement !== null) { if (afterStatement !== null) {
const task = findTaskById(afterStatement[1]) const task = findTaskById(afterStatement[1]);
if (typeof task === 'undefined') { if (typeof task === 'undefined') {
const dt = new Date() const dt = new Date();
dt.setHours(0, 0, 0, 0) dt.setHours(0, 0, 0, 0);
return dt return dt;
} }
return task.endTime return task.endTime;
} }
// Check for actual date set // Check for actual date set
let mDate = moment(str, dateFormat.trim(), true) let mDate = moment(str, dateFormat.trim(), true);
if (mDate.isValid()) { if (mDate.isValid()) {
return mDate.toDate() return mDate.toDate();
} else { } else {
logger.debug('Invalid date:' + str) logger.debug('Invalid date:' + str);
logger.debug('With date format:' + dateFormat.trim()) logger.debug('With date format:' + dateFormat.trim());
} }
// Default date - now // Default date - now
return new Date() return new Date();
} };
const durationToDate = function(durationStatement, relativeTime) { const durationToDate = function(durationStatement, relativeTime) {
if (durationStatement !== null) { if (durationStatement !== null) {
switch (durationStatement[2]) { switch (durationStatement[2]) {
case 's': case 's':
relativeTime.add(durationStatement[1], 'seconds') relativeTime.add(durationStatement[1], 'seconds');
break break;
case 'm': case 'm':
relativeTime.add(durationStatement[1], 'minutes') relativeTime.add(durationStatement[1], 'minutes');
break break;
case 'h': case 'h':
relativeTime.add(durationStatement[1], 'hours') relativeTime.add(durationStatement[1], 'hours');
break break;
case 'd': case 'd':
relativeTime.add(durationStatement[1], 'days') relativeTime.add(durationStatement[1], 'days');
break break;
case 'w': case 'w':
relativeTime.add(durationStatement[1], 'weeks') relativeTime.add(durationStatement[1], 'weeks');
break break;
} }
} }
// Default date - now // Default date - now
return relativeTime.toDate() return relativeTime.toDate();
} };
const getEndDate = function(prevTime, dateFormat, str, inclusive) { const getEndDate = function(prevTime, dateFormat, str, inclusive) {
inclusive = inclusive || false inclusive = inclusive || false;
str = str.trim() str = str.trim();
// Check for actual date // Check for actual date
let mDate = moment(str, dateFormat.trim(), true) let mDate = moment(str, dateFormat.trim(), true);
if (mDate.isValid()) { if (mDate.isValid()) {
if (inclusive) { if (inclusive) {
mDate.add(1, 'd') mDate.add(1, 'd');
} }
return mDate.toDate() return mDate.toDate();
} }
return durationToDate( return durationToDate(/^([\d]+)([wdhms])/.exec(str.trim()), moment(prevTime));
/^([\d]+)([wdhms])/.exec(str.trim()), };
moment(prevTime)
)
}
let taskCnt = 0 let taskCnt = 0;
const parseId = function(idStr) { const parseId = function(idStr) {
if (typeof idStr === 'undefined') { if (typeof idStr === 'undefined') {
taskCnt = taskCnt + 1 taskCnt = taskCnt + 1;
return 'task' + taskCnt return 'task' + taskCnt;
}
return idStr
} }
return idStr;
};
// id, startDate, endDate // id, startDate, endDate
// id, startDate, length // id, startDate, length
// id, after x, endDate // id, after x, endDate
@ -224,114 +221,114 @@ const parseId = function (idStr) {
// length // length
const compileData = function(prevTask, dataStr) { const compileData = function(prevTask, dataStr) {
let ds let ds;
if (dataStr.substr(0, 1) === ':') { if (dataStr.substr(0, 1) === ':') {
ds = dataStr.substr(1, dataStr.length) ds = dataStr.substr(1, dataStr.length);
} else { } else {
ds = dataStr ds = dataStr;
} }
const data = ds.split(',') const data = ds.split(',');
const task = {} const task = {};
// Get tags like active, done, crit and milestone // Get tags like active, done, crit and milestone
getTaskTags(data, task, tags) getTaskTags(data, task, tags);
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
data[i] = data[i].trim() data[i] = data[i].trim();
} }
let endTimeData = '' let endTimeData = '';
switch (data.length) { switch (data.length) {
case 1: case 1:
task.id = parseId() task.id = parseId();
task.startTime = prevTask.endTime task.startTime = prevTask.endTime;
endTimeData = data[0] endTimeData = data[0];
break break;
case 2: case 2:
task.id = parseId() task.id = parseId();
task.startTime = getStartDate(undefined, dateFormat, data[0]) task.startTime = getStartDate(undefined, dateFormat, data[0]);
endTimeData = data[1] endTimeData = data[1];
break break;
case 3: case 3:
task.id = parseId(data[0]) task.id = parseId(data[0]);
task.startTime = getStartDate(undefined, dateFormat, data[1]) task.startTime = getStartDate(undefined, dateFormat, data[1]);
endTimeData = data[2] endTimeData = data[2];
break break;
default: default:
} }
if (endTimeData) { if (endTimeData) {
task.endTime = getEndDate(task.startTime, dateFormat, endTimeData, inclusiveEndDates) task.endTime = getEndDate(task.startTime, dateFormat, endTimeData, inclusiveEndDates);
task.manualEndTime = moment(endTimeData, 'YYYY-MM-DD', true).isValid() task.manualEndTime = moment(endTimeData, 'YYYY-MM-DD', true).isValid();
checkTaskDates(task, dateFormat, excludes) checkTaskDates(task, dateFormat, excludes);
} }
return task return task;
} };
const parseData = function(prevTaskId, dataStr) { const parseData = function(prevTaskId, dataStr) {
let ds let ds;
if (dataStr.substr(0, 1) === ':') { if (dataStr.substr(0, 1) === ':') {
ds = dataStr.substr(1, dataStr.length) ds = dataStr.substr(1, dataStr.length);
} else { } else {
ds = dataStr ds = dataStr;
} }
const data = ds.split(',') const data = ds.split(',');
const task = {} const task = {};
// Get tags like active, done, crit and milestone // Get tags like active, done, crit and milestone
getTaskTags(data, task, tags) getTaskTags(data, task, tags);
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
data[i] = data[i].trim() data[i] = data[i].trim();
} }
switch (data.length) { switch (data.length) {
case 1: case 1:
task.id = parseId() task.id = parseId();
task.startTime = { task.startTime = {
type: 'prevTaskEnd', type: 'prevTaskEnd',
id: prevTaskId id: prevTaskId
} };
task.endTime = { task.endTime = {
data: data[0] data: data[0]
} };
break break;
case 2: case 2:
task.id = parseId() task.id = parseId();
task.startTime = { task.startTime = {
type: 'getStartDate', type: 'getStartDate',
startData: data[0] startData: data[0]
} };
task.endTime = { task.endTime = {
data: data[1] data: data[1]
} };
break break;
case 3: case 3:
task.id = parseId(data[0]) task.id = parseId(data[0]);
task.startTime = { task.startTime = {
type: 'getStartDate', type: 'getStartDate',
startData: data[1] startData: data[1]
} };
task.endTime = { task.endTime = {
data: data[2] data: data[2]
} };
break break;
default: default:
} }
return task return task;
} };
let lastTask let lastTask;
let lastTaskID let lastTaskID;
let rawTasks = [] let rawTasks = [];
const taskDb = {} const taskDb = {};
export const addTask = function(descr, data) { export const addTask = function(descr, data) {
const rawTask = { const rawTask = {
section: currentSection, section: currentSection,
@ -342,28 +339,28 @@ export const addTask = function (descr, data) {
raw: { data: data }, raw: { data: data },
task: descr, task: descr,
classes: [] classes: []
} };
const taskInfo = parseData(lastTaskID, data) const taskInfo = parseData(lastTaskID, data);
rawTask.raw.startTime = taskInfo.startTime rawTask.raw.startTime = taskInfo.startTime;
rawTask.raw.endTime = taskInfo.endTime rawTask.raw.endTime = taskInfo.endTime;
rawTask.id = taskInfo.id rawTask.id = taskInfo.id;
rawTask.prevTaskId = lastTaskID rawTask.prevTaskId = lastTaskID;
rawTask.active = taskInfo.active rawTask.active = taskInfo.active;
rawTask.done = taskInfo.done rawTask.done = taskInfo.done;
rawTask.crit = taskInfo.crit rawTask.crit = taskInfo.crit;
rawTask.milestone = taskInfo.milestone rawTask.milestone = taskInfo.milestone;
const pos = rawTasks.push(rawTask) const pos = rawTasks.push(rawTask);
lastTaskID = rawTask.id lastTaskID = rawTask.id;
// Store cross ref // Store cross ref
taskDb[rawTask.id] = pos - 1 taskDb[rawTask.id] = pos - 1;
} };
export const findTaskById = function(id) { export const findTaskById = function(id) {
const pos = taskDb[id] const pos = taskDb[id];
return rawTasks[pos] return rawTasks[pos];
} };
export const addTaskOrg = function(descr, data) { export const addTaskOrg = function(descr, data) {
const newTask = { const newTask = {
@ -372,56 +369,65 @@ export const addTaskOrg = function (descr, data) {
description: descr, description: descr,
task: descr, task: descr,
classes: [] classes: []
} };
const taskInfo = compileData(lastTask, data) const taskInfo = compileData(lastTask, data);
newTask.startTime = taskInfo.startTime newTask.startTime = taskInfo.startTime;
newTask.endTime = taskInfo.endTime newTask.endTime = taskInfo.endTime;
newTask.id = taskInfo.id newTask.id = taskInfo.id;
newTask.active = taskInfo.active newTask.active = taskInfo.active;
newTask.done = taskInfo.done newTask.done = taskInfo.done;
newTask.crit = taskInfo.crit newTask.crit = taskInfo.crit;
newTask.milestone = taskInfo.milestone newTask.milestone = taskInfo.milestone;
lastTask = newTask lastTask = newTask;
tasks.push(newTask) tasks.push(newTask);
} };
const compileTasks = function() { const compileTasks = function() {
const compileTask = function(pos) { const compileTask = function(pos) {
const task = rawTasks[pos] const task = rawTasks[pos];
let startTime = '' let startTime = '';
switch (rawTasks[pos].raw.startTime.type) { switch (rawTasks[pos].raw.startTime.type) {
case 'prevTaskEnd': case 'prevTaskEnd':
const prevTask = findTaskById(task.prevTaskId) const prevTask = findTaskById(task.prevTaskId);
task.startTime = prevTask.endTime task.startTime = prevTask.endTime;
break break;
case 'getStartDate': case 'getStartDate':
startTime = getStartDate(undefined, dateFormat, rawTasks[pos].raw.startTime.startData) startTime = getStartDate(undefined, dateFormat, rawTasks[pos].raw.startTime.startData);
if (startTime) { if (startTime) {
rawTasks[pos].startTime = startTime rawTasks[pos].startTime = startTime;
} }
break break;
} }
if (rawTasks[pos].startTime) { if (rawTasks[pos].startTime) {
rawTasks[pos].endTime = getEndDate(rawTasks[pos].startTime, dateFormat, rawTasks[pos].raw.endTime.data, inclusiveEndDates) rawTasks[pos].endTime = getEndDate(
rawTasks[pos].startTime,
dateFormat,
rawTasks[pos].raw.endTime.data,
inclusiveEndDates
);
if (rawTasks[pos].endTime) { if (rawTasks[pos].endTime) {
rawTasks[pos].processed = true rawTasks[pos].processed = true;
rawTasks[pos].manualEndTime = moment(rawTasks[pos].raw.endTime.data, 'YYYY-MM-DD', true).isValid() rawTasks[pos].manualEndTime = moment(
checkTaskDates(rawTasks[pos], dateFormat, excludes) rawTasks[pos].raw.endTime.data,
'YYYY-MM-DD',
true
).isValid();
checkTaskDates(rawTasks[pos], dateFormat, excludes);
} }
} }
return rawTasks[pos].processed return rawTasks[pos].processed;
} };
let allProcessed = true let allProcessed = true;
for (let i = 0; i < rawTasks.length; i++) { for (let i = 0; i < rawTasks.length; i++) {
compileTask(i) compileTask(i);
allProcessed = allProcessed && rawTasks[i].processed allProcessed = allProcessed && rawTasks[i].processed;
}
return allProcessed
} }
return allProcessed;
};
/** /**
* Called by parser when a link is found. Adds the URL to the vertex data. * Called by parser when a link is found. Adds the URL to the vertex data.
@ -429,18 +435,20 @@ const compileTasks = function () {
* @param linkStr URL to create a link for * @param linkStr URL to create a link for
*/ */
export const setLink = function(ids, _linkStr) { export const setLink = function(ids, _linkStr) {
let linkStr = _linkStr let linkStr = _linkStr;
if (config.securityLevel !== 'loose') { if (config.securityLevel !== 'loose') {
linkStr = sanitizeUrl(_linkStr) linkStr = sanitizeUrl(_linkStr);
} }
ids.split(',').forEach(function(id) { ids.split(',').forEach(function(id) {
let rawTask = findTaskById(id) let rawTask = findTaskById(id);
if (typeof rawTask !== 'undefined') { if (typeof rawTask !== 'undefined') {
pushFun(id, () => { window.open(linkStr, '_self') }) pushFun(id, () => {
} window.open(linkStr, '_self');
}) });
setClass(ids, 'clickable')
} }
});
setClass(ids, 'clickable');
};
/** /**
* Called by parser when a special node is found, e.g. a clickable element. * Called by parser when a special node is found, e.g. a clickable element.
@ -449,41 +457,43 @@ export const setLink = function (ids, _linkStr) {
*/ */
export const setClass = function(ids, className) { export const setClass = function(ids, className) {
ids.split(',').forEach(function(id) { ids.split(',').forEach(function(id) {
let rawTask = findTaskById(id) let rawTask = findTaskById(id);
if (typeof rawTask !== 'undefined') { if (typeof rawTask !== 'undefined') {
rawTask.classes.push(className) rawTask.classes.push(className);
}
})
} }
});
};
const setClickFun = function(id, functionName, functionArgs) { const setClickFun = function(id, functionName, functionArgs) {
if (config.securityLevel !== 'loose') { if (config.securityLevel !== 'loose') {
return return;
} }
if (typeof functionName === 'undefined') { if (typeof functionName === 'undefined') {
return return;
} }
let argList = [] let argList = [];
if (typeof functionArgs === 'string') { if (typeof functionArgs === 'string') {
/* Splits functionArgs by ',', ignoring all ',' in double quoted strings */ /* Splits functionArgs by ',', ignoring all ',' in double quoted strings */
argList = functionArgs.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/) argList = functionArgs.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/);
for (let i = 0; i < argList.length; i++) { for (let i = 0; i < argList.length; i++) {
let item = argList[i].trim() let item = argList[i].trim();
/* Removes all double quotes at the start and end of an argument */ /* Removes all double quotes at the start and end of an argument */
/* This preserves all starting and ending whitespace inside */ /* This preserves all starting and ending whitespace inside */
if (item.charAt(0) === '"' && item.charAt(item.length - 1) === '"') { if (item.charAt(0) === '"' && item.charAt(item.length - 1) === '"') {
item = item.substr(1, item.length - 2) item = item.substr(1, item.length - 2);
} }
argList[i] = item argList[i] = item;
} }
} }
let rawTask = findTaskById(id) let rawTask = findTaskById(id);
if (typeof rawTask !== 'undefined') { if (typeof rawTask !== 'undefined') {
pushFun(id, () => { window[functionName](...argList) }) pushFun(id, () => {
} window[functionName](...argList);
});
} }
};
/** /**
* The callbackFunction is executed in a click event bound to the task with the specified id or the task's assigned text * The callbackFunction is executed in a click event bound to the task with the specified id or the task's assigned text
@ -493,23 +503,23 @@ const setClickFun = function (id, functionName, functionArgs) {
const pushFun = function(id, callbackFunction) { const pushFun = function(id, callbackFunction) {
funs.push(function(element) { funs.push(function(element) {
// const elem = d3.select(element).select(`[id="${id}"]`) // const elem = d3.select(element).select(`[id="${id}"]`)
const elem = document.querySelector(`[id="${id}"]`) const elem = document.querySelector(`[id="${id}"]`);
if (elem !== null) { if (elem !== null) {
elem.addEventListener('click', function() { elem.addEventListener('click', function() {
callbackFunction() callbackFunction();
}) });
} }
}) });
funs.push(function(element) { funs.push(function(element) {
// const elem = d3.select(element).select(`[id="${id}-text"]`) // const elem = d3.select(element).select(`[id="${id}-text"]`)
const elem = document.querySelector(`[id="${id}-text"]`) const elem = document.querySelector(`[id="${id}-text"]`);
if (elem !== null) { if (elem !== null) {
elem.addEventListener('click', function() { elem.addEventListener('click', function() {
callbackFunction() callbackFunction();
}) });
}
})
} }
});
};
/** /**
* Called by parser when a click definition is found. Registers an event handler. * Called by parser when a click definition is found. Registers an event handler.
@ -519,10 +529,10 @@ const pushFun = function (id, callbackFunction) {
*/ */
export const setClickEvent = function(ids, functionName, functionArgs) { export const setClickEvent = function(ids, functionName, functionArgs) {
ids.split(',').forEach(function(id) { ids.split(',').forEach(function(id) {
setClickFun(id, functionName, functionArgs) setClickFun(id, functionName, functionArgs);
}) });
setClass(ids, 'clickable') setClass(ids, 'clickable');
} };
/** /**
* Binds all functions previously added to fun (specified through click) to the element * Binds all functions previously added to fun (specified through click) to the element
@ -530,9 +540,9 @@ export const setClickEvent = function (ids, functionName, functionArgs) {
*/ */
export const bindFunctions = function(element) { export const bindFunctions = function(element) {
funs.forEach(function(fun) { funs.forEach(function(fun) {
fun(element) fun(element);
}) });
} };
export default { export default {
clear, clear,
@ -556,20 +566,20 @@ export default {
setLink, setLink,
bindFunctions, bindFunctions,
durationToDate durationToDate
} };
function getTaskTags(data, task, tags) { function getTaskTags(data, task, tags) {
let matchFound = true let matchFound = true;
while (matchFound) { while (matchFound) {
matchFound = false matchFound = false;
tags.forEach(function(t) { tags.forEach(function(t) {
const pattern = '^\\s*' + t + '\\s*$' const pattern = '^\\s*' + t + '\\s*$';
const regex = new RegExp(pattern) const regex = new RegExp(pattern);
if (data[0].match(regex)) { if (data[0].match(regex)) {
task[t] = true task[t] = true;
data.shift(1) data.shift(1);
matchFound = true matchFound = true;
} }
}) });
} }
} }

View File

@ -1,11 +1,11 @@
/* eslint-env jasmine */ /* eslint-env jasmine */
import moment from 'moment-mini' import moment from 'moment-mini';
import ganttDb from './ganttDb' import ganttDb from './ganttDb';
describe('when using the ganttDb', function() { describe('when using the ganttDb', function() {
beforeEach(function() { beforeEach(function() {
ganttDb.clear() ganttDb.clear();
}) });
describe('when using relative times', function() { describe('when using relative times', function() {
it.each` it.each`
@ -13,20 +13,20 @@ describe('when using the ganttDb', function () {
${' 1d'} | ${moment('2019-01-01')} | ${moment('2019-01-02').toDate()} ${' 1d'} | ${moment('2019-01-01')} | ${moment('2019-01-02').toDate()}
${' 1w'} | ${moment('2019-01-01')} | ${moment('2019-01-08').toDate()} ${' 1w'} | ${moment('2019-01-01')} | ${moment('2019-01-08').toDate()}
`('should add $diff to $date resulting in $expected', ({ diff, date, expected }) => { `('should add $diff to $date resulting in $expected', ({ diff, date, expected }) => {
expect(ganttDb.durationToDate(diff, date)).toEqual(expected) expect(ganttDb.durationToDate(diff, date)).toEqual(expected);
}) });
}) });
describe('when calling the clear function', function() { describe('when calling the clear function', function() {
beforeEach(function() { beforeEach(function() {
ganttDb.setDateFormat('YYYY-MM-DD') ganttDb.setDateFormat('YYYY-MM-DD');
ganttDb.enableInclusiveEndDates() ganttDb.enableInclusiveEndDates();
ganttDb.setExcludes('weekends 2019-02-06,friday') ganttDb.setExcludes('weekends 2019-02-06,friday');
ganttDb.addSection('weekends skip test') ganttDb.addSection('weekends skip test');
ganttDb.addTask('test1', 'id1,2019-02-01,1d') ganttDb.addTask('test1', 'id1,2019-02-01,1d');
ganttDb.addTask('test2', 'id2,after id1,2d') ganttDb.addTask('test2', 'id2,after id1,2d');
ganttDb.clear() ganttDb.clear();
}) });
it.each` it.each`
fn | expected fn | expected
@ -38,9 +38,9 @@ describe('when using the ganttDb', function () {
${'getSections'} | ${[]} ${'getSections'} | ${[]}
${'endDatesAreInclusive'} | ${false} ${'endDatesAreInclusive'} | ${false}
`('should clear $fn', ({ fn, expected }) => { `('should clear $fn', ({ fn, expected }) => {
expect(ganttDb[ fn ]()).toEqual(expected) expect(ganttDb[fn]()).toEqual(expected);
}) });
}) });
it.each` it.each`
testName | section | taskName | taskData | expStartDate | expEndDate | expId | expTask testName | section | taskName | taskData | expStartDate | expEndDate | expId | expTask
@ -53,134 +53,147 @@ describe('when using the ganttDb', function () {
${'should handle fixed dates without id'} | ${'testa1'} | ${'test1'} | ${'2013-01-01,2013-01-12'} | ${new Date(2013, 0, 1)} | ${new Date(2013, 0, 12)} | ${'task1'} | ${'test1'} ${'should handle fixed dates without id'} | ${'testa1'} | ${'test1'} | ${'2013-01-01,2013-01-12'} | ${new Date(2013, 0, 1)} | ${new Date(2013, 0, 12)} | ${'task1'} | ${'test1'}
${'should handle duration instead of a fixed date to determine end date without id'} | ${'testa1'} | ${'test1'} | ${'2013-01-01,4d'} | ${new Date(2013, 0, 1)} | ${new Date(2013, 0, 5)} | ${'task1'} | ${'test1'} ${'should handle duration instead of a fixed date to determine end date without id'} | ${'testa1'} | ${'test1'} | ${'2013-01-01,4d'} | ${new Date(2013, 0, 1)} | ${new Date(2013, 0, 5)} | ${'task1'} | ${'test1'}
`('$testName', ({ section, taskName, taskData, expStartDate, expEndDate, expId, expTask }) => { `('$testName', ({ section, taskName, taskData, expStartDate, expEndDate, expId, expTask }) => {
ganttDb.setDateFormat('YYYY-MM-DD') ganttDb.setDateFormat('YYYY-MM-DD');
ganttDb.addSection(section) ganttDb.addSection(section);
ganttDb.addTask(taskName, taskData) ganttDb.addTask(taskName, taskData);
const tasks = ganttDb.getTasks() const tasks = ganttDb.getTasks();
expect(tasks[0].startTime).toEqual(expStartDate) expect(tasks[0].startTime).toEqual(expStartDate);
expect(tasks[0].endTime).toEqual(expEndDate) expect(tasks[0].endTime).toEqual(expEndDate);
expect(tasks[0].id).toEqual(expId) expect(tasks[0].id).toEqual(expId);
expect(tasks[0].task).toEqual(expTask) expect(tasks[0].task).toEqual(expTask);
}) });
it.each` it.each`
section | taskName1 | taskName2 | taskData1 | taskData2 | expStartDate2 | expEndDate2 | expId2 | expTask2 section | taskName1 | taskName2 | taskData1 | taskData2 | expStartDate2 | expEndDate2 | expId2 | expTask2
${'testa1'} | ${'test1'} | ${'test2'} | ${'id1,2013-01-01,2w'} | ${'id2,after id1,1d'} | ${new Date(2013, 0, 15)} | ${undefined} | ${'id2'} | ${'test2'} ${'testa1'} | ${'test1'} | ${'test2'} | ${'id1,2013-01-01,2w'} | ${'id2,after id1,1d'} | ${new Date(2013, 0, 15)} | ${undefined} | ${'id2'} | ${'test2'}
${'testa1'} | ${'test1'} | ${'test2'} | ${'id1,2013-01-01,2w'} | ${'id2,after id3,1d'} | ${new Date((new Date()).setHours(0, 0, 0, 0))} | ${undefined} | ${'id2'} | ${'test2'} ${'testa1'} | ${'test1'} | ${'test2'} | ${'id1,2013-01-01,2w'} | ${'id2,after id3,1d'} | ${new Date(new Date().setHours(0, 0, 0, 0))} | ${undefined} | ${'id2'} | ${'test2'}
${'testa1'} | ${'test1'} | ${'test2'} | ${'id1,2013-01-01,2w'} | ${'after id1,1d'} | ${new Date(2013, 0, 15)} | ${undefined} | ${'task1'} | ${'test2'} ${'testa1'} | ${'test1'} | ${'test2'} | ${'id1,2013-01-01,2w'} | ${'after id1,1d'} | ${new Date(2013, 0, 15)} | ${undefined} | ${'task1'} | ${'test2'}
${'testa1'} | ${'test1'} | ${'test2'} | ${'id1,2013-01-01,2w'} | ${'2013-01-26'} | ${new Date(2013, 0, 15)} | ${new Date(2013, 0, 26)} | ${'task1'} | ${'test2'} ${'testa1'} | ${'test1'} | ${'test2'} | ${'id1,2013-01-01,2w'} | ${'2013-01-26'} | ${new Date(2013, 0, 15)} | ${new Date(2013, 0, 26)} | ${'task1'} | ${'test2'}
${'testa1'} | ${'test1'} | ${'test2'} | ${'id1,2013-01-01,2w'} | ${'2d'} | ${new Date(2013, 0, 15)} | ${new Date(2013, 0, 17)} | ${'task1'} | ${'test2'} ${'testa1'} | ${'test1'} | ${'test2'} | ${'id1,2013-01-01,2w'} | ${'2d'} | ${new Date(2013, 0, 15)} | ${new Date(2013, 0, 17)} | ${'task1'} | ${'test2'}
`('$testName', ({ section, taskName1, taskName2, taskData1, taskData2, expStartDate2, expEndDate2, expId2, expTask2 }) => { `(
ganttDb.setDateFormat('YYYY-MM-DD') '$testName',
ganttDb.addSection(section) ({
ganttDb.addTask(taskName1, taskData1) section,
ganttDb.addTask(taskName2, taskData2) taskName1,
const tasks = ganttDb.getTasks() taskName2,
expect(tasks[1].startTime).toEqual(expStartDate2) taskData1,
taskData2,
expStartDate2,
expEndDate2,
expId2,
expTask2
}) => {
ganttDb.setDateFormat('YYYY-MM-DD');
ganttDb.addSection(section);
ganttDb.addTask(taskName1, taskData1);
ganttDb.addTask(taskName2, taskData2);
const tasks = ganttDb.getTasks();
expect(tasks[1].startTime).toEqual(expStartDate2);
if (!expEndDate2 === undefined) { if (!expEndDate2 === undefined) {
expect(tasks[1].endTime).toEqual(expEndDate2) expect(tasks[1].endTime).toEqual(expEndDate2);
} }
expect(tasks[1].id).toEqual(expId2) expect(tasks[1].id).toEqual(expId2);
expect(tasks[1].task).toEqual(expTask2) expect(tasks[1].task).toEqual(expTask2);
}) }
);
it('should handle relative start date based on id regardless of sections', function() { it('should handle relative start date based on id regardless of sections', function() {
ganttDb.setDateFormat('YYYY-MM-DD') ganttDb.setDateFormat('YYYY-MM-DD');
ganttDb.addSection('testa1') ganttDb.addSection('testa1');
ganttDb.addTask('test1', 'id1,2013-01-01,2w') ganttDb.addTask('test1', 'id1,2013-01-01,2w');
ganttDb.addTask('test2', 'id2,after id3,1d') ganttDb.addTask('test2', 'id2,after id3,1d');
ganttDb.addSection('testa2') ganttDb.addSection('testa2');
ganttDb.addTask('test3', 'id3,after id1,2d') ganttDb.addTask('test3', 'id3,after id1,2d');
const tasks = ganttDb.getTasks() const tasks = ganttDb.getTasks();
expect(tasks[1].startTime).toEqual(new Date(2013, 0, 17)) expect(tasks[1].startTime).toEqual(new Date(2013, 0, 17));
expect(tasks[1].endTime).toEqual(new Date(2013, 0, 18)) expect(tasks[1].endTime).toEqual(new Date(2013, 0, 18));
expect(tasks[1].id).toEqual('id2') expect(tasks[1].id).toEqual('id2');
expect(tasks[1].task).toEqual('test2') expect(tasks[1].task).toEqual('test2');
expect(tasks[2].id).toEqual('id3') expect(tasks[2].id).toEqual('id3');
expect(tasks[2].task).toEqual('test3') expect(tasks[2].task).toEqual('test3');
expect(tasks[2].startTime).toEqual(new Date(2013, 0, 15)) expect(tasks[2].startTime).toEqual(new Date(2013, 0, 15));
expect(tasks[2].endTime).toEqual(new Date(2013, 0, 17)) expect(tasks[2].endTime).toEqual(new Date(2013, 0, 17));
}) });
it('should ignore weekends', function() { it('should ignore weekends', function() {
ganttDb.setDateFormat('YYYY-MM-DD') ganttDb.setDateFormat('YYYY-MM-DD');
ganttDb.setExcludes('weekends 2019-02-06,friday') ganttDb.setExcludes('weekends 2019-02-06,friday');
ganttDb.addSection('weekends skip test') ganttDb.addSection('weekends skip test');
ganttDb.addTask('test1', 'id1,2019-02-01,1d') ganttDb.addTask('test1', 'id1,2019-02-01,1d');
ganttDb.addTask('test2', 'id2,after id1,2d') ganttDb.addTask('test2', 'id2,after id1,2d');
ganttDb.addTask('test3', 'id3,after id2,7d') ganttDb.addTask('test3', 'id3,after id2,7d');
ganttDb.addTask('test4', 'id4,2019-02-01,2019-02-20') // Fixed endTime ganttDb.addTask('test4', 'id4,2019-02-01,2019-02-20'); // Fixed endTime
ganttDb.addTask('test5', 'id5,after id4,1d') ganttDb.addTask('test5', 'id5,after id4,1d');
ganttDb.addSection('full ending taks on last day') ganttDb.addSection('full ending taks on last day');
ganttDb.addTask('test6', 'id6,2019-02-13,2d') ganttDb.addTask('test6', 'id6,2019-02-13,2d');
ganttDb.addTask('test7', 'id7,after id6,1d') ganttDb.addTask('test7', 'id7,after id6,1d');
const tasks = ganttDb.getTasks() const tasks = ganttDb.getTasks();
expect(tasks[0].startTime).toEqual(moment('2019-02-01', 'YYYY-MM-DD').toDate()) expect(tasks[0].startTime).toEqual(moment('2019-02-01', 'YYYY-MM-DD').toDate());
expect(tasks[0].endTime).toEqual(moment('2019-02-04', 'YYYY-MM-DD').toDate()) expect(tasks[0].endTime).toEqual(moment('2019-02-04', 'YYYY-MM-DD').toDate());
expect(tasks[0].renderEndTime).toEqual(moment('2019-02-02', 'YYYY-MM-DD').toDate()) expect(tasks[0].renderEndTime).toEqual(moment('2019-02-02', 'YYYY-MM-DD').toDate());
expect(tasks[0].id).toEqual('id1') expect(tasks[0].id).toEqual('id1');
expect(tasks[0].task).toEqual('test1') expect(tasks[0].task).toEqual('test1');
expect(tasks[1].startTime).toEqual(moment('2019-02-04', 'YYYY-MM-DD').toDate()) expect(tasks[1].startTime).toEqual(moment('2019-02-04', 'YYYY-MM-DD').toDate());
expect(tasks[1].endTime).toEqual(moment('2019-02-07', 'YYYY-MM-DD').toDate()) expect(tasks[1].endTime).toEqual(moment('2019-02-07', 'YYYY-MM-DD').toDate());
expect(tasks[1].renderEndTime).toEqual(moment('2019-02-06', 'YYYY-MM-DD').toDate()) expect(tasks[1].renderEndTime).toEqual(moment('2019-02-06', 'YYYY-MM-DD').toDate());
expect(tasks[1].id).toEqual('id2') expect(tasks[1].id).toEqual('id2');
expect(tasks[1].task).toEqual('test2') expect(tasks[1].task).toEqual('test2');
expect(tasks[2].startTime).toEqual(moment('2019-02-07', 'YYYY-MM-DD').toDate()) expect(tasks[2].startTime).toEqual(moment('2019-02-07', 'YYYY-MM-DD').toDate());
expect(tasks[2].endTime).toEqual(moment('2019-02-20', 'YYYY-MM-DD').toDate()) expect(tasks[2].endTime).toEqual(moment('2019-02-20', 'YYYY-MM-DD').toDate());
expect(tasks[2].renderEndTime).toEqual(moment('2019-02-20', 'YYYY-MM-DD').toDate()) expect(tasks[2].renderEndTime).toEqual(moment('2019-02-20', 'YYYY-MM-DD').toDate());
expect(tasks[2].id).toEqual('id3') expect(tasks[2].id).toEqual('id3');
expect(tasks[2].task).toEqual('test3') expect(tasks[2].task).toEqual('test3');
expect(tasks[3].startTime).toEqual(moment('2019-02-01', 'YYYY-MM-DD').toDate()) expect(tasks[3].startTime).toEqual(moment('2019-02-01', 'YYYY-MM-DD').toDate());
expect(tasks[3].endTime).toEqual(moment('2019-02-20', 'YYYY-MM-DD').toDate()) expect(tasks[3].endTime).toEqual(moment('2019-02-20', 'YYYY-MM-DD').toDate());
expect(tasks[3].renderEndTime).toBeNull() // Fixed end expect(tasks[3].renderEndTime).toBeNull(); // Fixed end
expect(tasks[3].id).toEqual('id4') expect(tasks[3].id).toEqual('id4');
expect(tasks[3].task).toEqual('test4') expect(tasks[3].task).toEqual('test4');
expect(tasks[4].startTime).toEqual(moment('2019-02-20', 'YYYY-MM-DD').toDate()) expect(tasks[4].startTime).toEqual(moment('2019-02-20', 'YYYY-MM-DD').toDate());
expect(tasks[4].endTime).toEqual(moment('2019-02-21', 'YYYY-MM-DD').toDate()) expect(tasks[4].endTime).toEqual(moment('2019-02-21', 'YYYY-MM-DD').toDate());
expect(tasks[4].renderEndTime).toEqual(moment('2019-02-21', 'YYYY-MM-DD').toDate()) expect(tasks[4].renderEndTime).toEqual(moment('2019-02-21', 'YYYY-MM-DD').toDate());
expect(tasks[4].id).toEqual('id5') expect(tasks[4].id).toEqual('id5');
expect(tasks[4].task).toEqual('test5') expect(tasks[4].task).toEqual('test5');
expect(tasks[5].startTime).toEqual(moment('2019-02-13', 'YYYY-MM-DD').toDate()) expect(tasks[5].startTime).toEqual(moment('2019-02-13', 'YYYY-MM-DD').toDate());
expect(tasks[5].endTime).toEqual(moment('2019-02-18', 'YYYY-MM-DD').toDate()) expect(tasks[5].endTime).toEqual(moment('2019-02-18', 'YYYY-MM-DD').toDate());
expect(tasks[5].renderEndTime).toEqual(moment('2019-02-15', 'YYYY-MM-DD').toDate()) expect(tasks[5].renderEndTime).toEqual(moment('2019-02-15', 'YYYY-MM-DD').toDate());
expect(tasks[5].id).toEqual('id6') expect(tasks[5].id).toEqual('id6');
expect(tasks[5].task).toEqual('test6') expect(tasks[5].task).toEqual('test6');
expect(tasks[6].startTime).toEqual(moment('2019-02-18', 'YYYY-MM-DD').toDate()) expect(tasks[6].startTime).toEqual(moment('2019-02-18', 'YYYY-MM-DD').toDate());
expect(tasks[6].endTime).toEqual(moment('2019-02-19', 'YYYY-MM-DD').toDate()) expect(tasks[6].endTime).toEqual(moment('2019-02-19', 'YYYY-MM-DD').toDate());
expect(tasks[6].id).toEqual('id7') expect(tasks[6].id).toEqual('id7');
expect(tasks[6].task).toEqual('test7') expect(tasks[6].task).toEqual('test7');
}) });
describe('when setting inclusive end dates', function() { describe('when setting inclusive end dates', function() {
beforeEach(function() { beforeEach(function() {
ganttDb.setDateFormat('YYYY-MM-DD') ganttDb.setDateFormat('YYYY-MM-DD');
ganttDb.enableInclusiveEndDates() ganttDb.enableInclusiveEndDates();
ganttDb.addTask('test1', 'id1,2019-02-01,1d') ganttDb.addTask('test1', 'id1,2019-02-01,1d');
ganttDb.addTask('test2', 'id2,2019-02-01,2019-02-03') ganttDb.addTask('test2', 'id2,2019-02-01,2019-02-03');
}) });
it('should automatically add one day to all end dates', function() { it('should automatically add one day to all end dates', function() {
const tasks = ganttDb.getTasks() const tasks = ganttDb.getTasks();
expect(tasks[0].startTime).toEqual(moment('2019-02-01', 'YYYY-MM-DD').toDate()) expect(tasks[0].startTime).toEqual(moment('2019-02-01', 'YYYY-MM-DD').toDate());
expect(tasks[0].endTime).toEqual(moment('2019-02-02', 'YYYY-MM-DD').toDate()) expect(tasks[0].endTime).toEqual(moment('2019-02-02', 'YYYY-MM-DD').toDate());
expect(tasks[0].id).toEqual('id1') expect(tasks[0].id).toEqual('id1');
expect(tasks[0].task).toEqual('test1') expect(tasks[0].task).toEqual('test1');
expect(tasks[1].startTime).toEqual(moment('2019-02-01', 'YYYY-MM-DD').toDate()) expect(tasks[1].startTime).toEqual(moment('2019-02-01', 'YYYY-MM-DD').toDate());
expect(tasks[1].endTime).toEqual(moment('2019-02-04', 'YYYY-MM-DD').toDate()) expect(tasks[1].endTime).toEqual(moment('2019-02-04', 'YYYY-MM-DD').toDate());
expect(tasks[1].renderEndTime).toBeNull() // Fixed end expect(tasks[1].renderEndTime).toBeNull(); // Fixed end
expect(tasks[1].manualEndTime).toBeTruthy() expect(tasks[1].manualEndTime).toBeTruthy();
expect(tasks[1].id).toEqual('id2') expect(tasks[1].id).toEqual('id2');
expect(tasks[1].task).toEqual('test2') expect(tasks[1].task).toEqual('test2');
}) });
}) });
}) });

View File

@ -1,9 +1,9 @@
import * as d3 from 'd3' import * as d3 from 'd3';
import { parser } from './parser/gantt' import { parser } from './parser/gantt';
import ganttDb from './ganttDb' import ganttDb from './ganttDb';
parser.yy = ganttDb parser.yy = ganttDb;
const conf = { const conf = {
titleTopMargin: 25, titleTopMargin: 25,
@ -15,287 +15,316 @@ const conf = {
gridLineStartPadding: 35, gridLineStartPadding: 35,
fontSize: 11, fontSize: 11,
fontFamily: '"Open-Sans", "sans-serif"' fontFamily: '"Open-Sans", "sans-serif"'
} };
export const setConf = function(cnf) { export const setConf = function(cnf) {
const keys = Object.keys(cnf) const keys = Object.keys(cnf);
keys.forEach(function(key) { keys.forEach(function(key) {
conf[key] = cnf[key] conf[key] = cnf[key];
}) });
} };
let w let w;
export const draw = function(text, id) { export const draw = function(text, id) {
parser.yy.clear() parser.yy.clear();
parser.parse(text) parser.parse(text);
const elem = document.getElementById(id) const elem = document.getElementById(id);
w = elem.parentElement.offsetWidth w = elem.parentElement.offsetWidth;
if (typeof w === 'undefined') { if (typeof w === 'undefined') {
w = 1200 w = 1200;
} }
if (typeof conf.useWidth !== 'undefined') { if (typeof conf.useWidth !== 'undefined') {
w = conf.useWidth w = conf.useWidth;
} }
const taskArray = parser.yy.getTasks() const taskArray = parser.yy.getTasks();
// Set height based on number of tasks // Set height based on number of tasks
const h = taskArray.length * (conf.barHeight + conf.barGap) + 2 * conf.topPadding const h = taskArray.length * (conf.barHeight + conf.barGap) + 2 * conf.topPadding;
elem.setAttribute('height', '100%') elem.setAttribute('height', '100%');
// Set viewBox // Set viewBox
elem.setAttribute('viewBox', '0 0 ' + w + ' ' + h) elem.setAttribute('viewBox', '0 0 ' + w + ' ' + h);
const svg = d3.select(`[id="${id}"]`) const svg = d3.select(`[id="${id}"]`);
// Set timescale // Set timescale
const timeScale = d3.scaleTime() const timeScale = d3
.domain([d3.min(taskArray, function (d) { .scaleTime()
return d.startTime .domain([
d3.min(taskArray, function(d) {
return d.startTime;
}), }),
d3.max(taskArray, function(d) { d3.max(taskArray, function(d) {
return d.endTime return d.endTime;
})]) })
.rangeRound([0, w - conf.leftPadding - conf.rightPadding]) ])
.rangeRound([0, w - conf.leftPadding - conf.rightPadding]);
let categories = [] let categories = [];
for (let i = 0; i < taskArray.length; i++) { for (let i = 0; i < taskArray.length; i++) {
categories.push(taskArray[i].type) categories.push(taskArray[i].type);
} }
const catsUnfiltered = categories // for vert labels const catsUnfiltered = categories; // for vert labels
categories = checkUnique(categories) categories = checkUnique(categories);
makeGant(taskArray, w, h) makeGant(taskArray, w, h);
if (typeof conf.useWidth !== 'undefined') { if (typeof conf.useWidth !== 'undefined') {
elem.setAttribute('width', w) elem.setAttribute('width', w);
} }
svg.append('text') svg
.append('text')
.text(parser.yy.getTitle()) .text(parser.yy.getTitle())
.attr('x', w / 2) .attr('x', w / 2)
.attr('y', conf.titleTopMargin) .attr('y', conf.titleTopMargin)
.attr('class', 'titleText') .attr('class', 'titleText');
function makeGant(tasks, pageWidth, pageHeight) { function makeGant(tasks, pageWidth, pageHeight) {
const barHeight = conf.barHeight const barHeight = conf.barHeight;
const gap = barHeight + conf.barGap const gap = barHeight + conf.barGap;
const topPadding = conf.topPadding const topPadding = conf.topPadding;
const leftPadding = conf.leftPadding const leftPadding = conf.leftPadding;
const colorScale = d3.scaleLinear() const colorScale = d3
.scaleLinear()
.domain([0, categories.length]) .domain([0, categories.length])
.range(['#00B9FA', '#F95002']) .range(['#00B9FA', '#F95002'])
.interpolate(d3.interpolateHcl) .interpolate(d3.interpolateHcl);
makeGrid(leftPadding, topPadding, pageWidth, pageHeight) makeGrid(leftPadding, topPadding, pageWidth, pageHeight);
drawRects(tasks, gap, topPadding, leftPadding, barHeight, colorScale, pageWidth, pageHeight) drawRects(tasks, gap, topPadding, leftPadding, barHeight, colorScale, pageWidth, pageHeight);
vertLabels(gap, topPadding, leftPadding, barHeight, colorScale) vertLabels(gap, topPadding, leftPadding, barHeight, colorScale);
drawToday(leftPadding, topPadding, pageWidth, pageHeight) drawToday(leftPadding, topPadding, pageWidth, pageHeight);
} }
function drawRects(theArray, theGap, theTopPad, theSidePad, theBarHeight, theColorScale, w, h) { function drawRects(theArray, theGap, theTopPad, theSidePad, theBarHeight, theColorScale, w, h) {
// Draw background rects covering the entire width of the graph, these form the section rows. // Draw background rects covering the entire width of the graph, these form the section rows.
svg.append('g') svg
.append('g')
.selectAll('rect') .selectAll('rect')
.data(theArray) .data(theArray)
.enter() .enter()
.append('rect') .append('rect')
.attr('x', 0) .attr('x', 0)
.attr('y', function(d, i) { .attr('y', function(d, i) {
return i * theGap + theTopPad - 2 return i * theGap + theTopPad - 2;
}) })
.attr('width', function() { .attr('width', function() {
return w - conf.rightPadding / 2 return w - conf.rightPadding / 2;
}) })
.attr('height', theGap) .attr('height', theGap)
.attr('class', function(d) { .attr('class', function(d) {
for (let i = 0; i < categories.length; i++) { for (let i = 0; i < categories.length; i++) {
if (d.type === categories[i]) { if (d.type === categories[i]) {
return 'section section' + (i % conf.numberSectionStyles) return 'section section' + (i % conf.numberSectionStyles);
} }
} }
return 'section section0' return 'section section0';
}) });
// Draw the rects representing the tasks // Draw the rects representing the tasks
const rectangles = svg.append('g') const rectangles = svg
.append('g')
.selectAll('rect') .selectAll('rect')
.data(theArray) .data(theArray)
.enter() .enter();
rectangles.append('rect') rectangles
.attr('id', function (d) { return d.id }) .append('rect')
.attr('id', function(d) {
return d.id;
})
.attr('rx', 3) .attr('rx', 3)
.attr('ry', 3) .attr('ry', 3)
.attr('x', function(d) { .attr('x', function(d) {
if (d.milestone) { if (d.milestone) {
return timeScale(d.startTime) + theSidePad + (0.5 * (timeScale(d.endTime) - timeScale(d.startTime))) - (0.5 * theBarHeight) return (
timeScale(d.startTime) +
theSidePad +
0.5 * (timeScale(d.endTime) - timeScale(d.startTime)) -
0.5 * theBarHeight
);
} }
return timeScale(d.startTime) + theSidePad return timeScale(d.startTime) + theSidePad;
}) })
.attr('y', function(d, i) { .attr('y', function(d, i) {
return i * theGap + theTopPad return i * theGap + theTopPad;
}) })
.attr('width', function(d) { .attr('width', function(d) {
if (d.milestone) { if (d.milestone) {
return theBarHeight return theBarHeight;
} }
return (timeScale(d.renderEndTime || d.endTime) - timeScale(d.startTime)) return timeScale(d.renderEndTime || d.endTime) - timeScale(d.startTime);
}) })
.attr('height', theBarHeight) .attr('height', theBarHeight)
.attr('transform-origin', function(d, i) { .attr('transform-origin', function(d, i) {
return (timeScale(d.startTime) + theSidePad + 0.5 * (timeScale(d.endTime) - timeScale(d.startTime))).toString() + 'px ' + (i * theGap + theTopPad + 0.5 * theBarHeight).toString() + 'px' return (
(
timeScale(d.startTime) +
theSidePad +
0.5 * (timeScale(d.endTime) - timeScale(d.startTime))
).toString() +
'px ' +
(i * theGap + theTopPad + 0.5 * theBarHeight).toString() +
'px'
);
}) })
.attr('class', function(d) { .attr('class', function(d) {
const res = 'task' const res = 'task';
let classStr = '' let classStr = '';
if (d.classes.length > 0) { if (d.classes.length > 0) {
classStr = d.classes.join(' ') classStr = d.classes.join(' ');
} }
let secNum = 0 let secNum = 0;
for (let i = 0; i < categories.length; i++) { for (let i = 0; i < categories.length; i++) {
if (d.type === categories[i]) { if (d.type === categories[i]) {
secNum = (i % conf.numberSectionStyles) secNum = i % conf.numberSectionStyles;
} }
} }
let taskClass = '' let taskClass = '';
if (d.active) { if (d.active) {
if (d.crit) { if (d.crit) {
taskClass += ' activeCrit' taskClass += ' activeCrit';
} else { } else {
taskClass = ' active' taskClass = ' active';
} }
} else if (d.done) { } else if (d.done) {
if (d.crit) { if (d.crit) {
taskClass = ' doneCrit' taskClass = ' doneCrit';
} else { } else {
taskClass = ' done' taskClass = ' done';
} }
} else { } else {
if (d.crit) { if (d.crit) {
taskClass += ' crit' taskClass += ' crit';
} }
} }
if (taskClass.length === 0) { if (taskClass.length === 0) {
taskClass = ' task' taskClass = ' task';
} }
if (d.milestone) { if (d.milestone) {
taskClass = ' milestone ' + taskClass taskClass = ' milestone ' + taskClass;
} }
taskClass += secNum taskClass += secNum;
taskClass += ' ' + classStr taskClass += ' ' + classStr;
return res + taskClass return res + taskClass;
}) });
// Append task labels // Append task labels
rectangles.append('text') rectangles
.attr('id', function (d) { return d.id + '-text' }) .append('text')
.attr('id', function(d) {
return d.id + '-text';
})
.text(function(d) { .text(function(d) {
return d.task return d.task;
}) })
.attr('font-size', conf.fontSize) .attr('font-size', conf.fontSize)
.attr('x', function(d) { .attr('x', function(d) {
let startX = timeScale(d.startTime) let startX = timeScale(d.startTime);
let endX = timeScale(d.renderEndTime || d.endTime) let endX = timeScale(d.renderEndTime || d.endTime);
if (d.milestone) { if (d.milestone) {
startX += (0.5 * (timeScale(d.endTime) - timeScale(d.startTime))) - (0.5 * theBarHeight) startX += 0.5 * (timeScale(d.endTime) - timeScale(d.startTime)) - 0.5 * theBarHeight;
} }
if (d.milestone) { if (d.milestone) {
endX = startX + theBarHeight endX = startX + theBarHeight;
} }
const textWidth = this.getBBox().width const textWidth = this.getBBox().width;
// Check id text width > width of rectangle // Check id text width > width of rectangle
if (textWidth > (endX - startX)) { if (textWidth > endX - startX) {
if (endX + textWidth + 1.5 * conf.leftPadding > w) { if (endX + textWidth + 1.5 * conf.leftPadding > w) {
return startX + theSidePad - 5 return startX + theSidePad - 5;
} else { } else {
return endX + theSidePad + 5 return endX + theSidePad + 5;
} }
} else { } else {
return (endX - startX) / 2 + startX + theSidePad return (endX - startX) / 2 + startX + theSidePad;
} }
}) })
.attr('y', function(d, i) { .attr('y', function(d, i) {
return i * theGap + (conf.barHeight / 2) + (conf.fontSize / 2 - 2) + theTopPad return i * theGap + conf.barHeight / 2 + (conf.fontSize / 2 - 2) + theTopPad;
}) })
.attr('text-height', theBarHeight) .attr('text-height', theBarHeight)
.attr('class', function(d) { .attr('class', function(d) {
const startX = timeScale(d.startTime) const startX = timeScale(d.startTime);
let endX = timeScale(d.endTime) let endX = timeScale(d.endTime);
if (d.milestone) { if (d.milestone) {
endX = startX + theBarHeight endX = startX + theBarHeight;
} }
const textWidth = this.getBBox().width const textWidth = this.getBBox().width;
let classStr = '' let classStr = '';
if (d.classes.length > 0) { if (d.classes.length > 0) {
classStr = d.classes.join(' ') classStr = d.classes.join(' ');
} }
let secNum = 0 let secNum = 0;
for (let i = 0; i < categories.length; i++) { for (let i = 0; i < categories.length; i++) {
if (d.type === categories[i]) { if (d.type === categories[i]) {
secNum = (i % conf.numberSectionStyles) secNum = i % conf.numberSectionStyles;
} }
} }
let taskType = '' let taskType = '';
if (d.active) { if (d.active) {
if (d.crit) { if (d.crit) {
taskType = 'activeCritText' + secNum taskType = 'activeCritText' + secNum;
} else { } else {
taskType = 'activeText' + secNum taskType = 'activeText' + secNum;
} }
} }
if (d.done) { if (d.done) {
if (d.crit) { if (d.crit) {
taskType = taskType + ' doneCritText' + secNum taskType = taskType + ' doneCritText' + secNum;
} else { } else {
taskType = taskType + ' doneText' + secNum taskType = taskType + ' doneText' + secNum;
} }
} else { } else {
if (d.crit) { if (d.crit) {
taskType = taskType + ' critText' + secNum taskType = taskType + ' critText' + secNum;
} }
} }
if (d.milestone) { if (d.milestone) {
taskType += ' milestoneText' taskType += ' milestoneText';
} }
// Check id text width > width of rectangle // Check id text width > width of rectangle
if (textWidth > (endX - startX)) { if (textWidth > endX - startX) {
if (endX + textWidth + 1.5 * conf.leftPadding > w) { if (endX + textWidth + 1.5 * conf.leftPadding > w) {
return classStr + ' taskTextOutsideLeft taskTextOutside' + secNum + ' ' + taskType return classStr + ' taskTextOutsideLeft taskTextOutside' + secNum + ' ' + taskType;
} else { } else {
return classStr + ' taskTextOutsideRight taskTextOutside' + secNum + ' ' + taskType return classStr + ' taskTextOutsideRight taskTextOutside' + secNum + ' ' + taskType;
} }
} else { } else {
return classStr + ' taskText taskText' + secNum + ' ' + taskType return classStr + ' taskText taskText' + secNum + ' ' + taskType;
} }
}) });
} }
function makeGrid(theSidePad, theTopPad, w, h) { function makeGrid(theSidePad, theTopPad, w, h) {
let xAxis = d3.axisBottom(timeScale) let xAxis = d3
.axisBottom(timeScale)
.tickSize(-h + theTopPad + conf.gridLineStartPadding) .tickSize(-h + theTopPad + conf.gridLineStartPadding)
.tickFormat(d3.timeFormat(parser.yy.getAxisFormat() || conf.axisFormat || '%Y-%m-%d')) .tickFormat(d3.timeFormat(parser.yy.getAxisFormat() || conf.axisFormat || '%Y-%m-%d'));
svg.append('g') svg
.append('g')
.attr('class', 'grid') .attr('class', 'grid')
.attr('transform', 'translate(' + theSidePad + ', ' + (h - 50) + ')') .attr('transform', 'translate(' + theSidePad + ', ' + (h - 50) + ')')
.call(xAxis) .call(xAxis)
@ -304,90 +333,92 @@ export const draw = function (text, id) {
.attr('fill', '#000') .attr('fill', '#000')
.attr('stroke', 'none') .attr('stroke', 'none')
.attr('font-size', 10) .attr('font-size', 10)
.attr('dy', '1em') .attr('dy', '1em');
} }
function vertLabels(theGap, theTopPad) { function vertLabels(theGap, theTopPad) {
const numOccurances = [] const numOccurances = [];
let prevGap = 0 let prevGap = 0;
for (let i = 0; i < categories.length; i++) { for (let i = 0; i < categories.length; i++) {
numOccurances[i] = [categories[i], getCount(categories[i], catsUnfiltered)] numOccurances[i] = [categories[i], getCount(categories[i], catsUnfiltered)];
} }
svg.append('g') // without doing this, impossible to put grid lines behind text svg
.append('g') // without doing this, impossible to put grid lines behind text
.selectAll('text') .selectAll('text')
.data(numOccurances) .data(numOccurances)
.enter() .enter()
.append('text') .append('text')
.text(function(d) { .text(function(d) {
return d[0] return d[0];
}) })
.attr('x', 10) .attr('x', 10)
.attr('y', function(d, i) { .attr('y', function(d, i) {
if (i > 0) { if (i > 0) {
for (let j = 0; j < i; j++) { for (let j = 0; j < i; j++) {
prevGap += numOccurances[i - 1][1] prevGap += numOccurances[i - 1][1];
return d[1] * theGap / 2 + prevGap * theGap + theTopPad return (d[1] * theGap) / 2 + prevGap * theGap + theTopPad;
} }
} else { } else {
return d[1] * theGap / 2 + theTopPad return (d[1] * theGap) / 2 + theTopPad;
} }
}) })
.attr('class', function(d) { .attr('class', function(d) {
for (let i = 0; i < categories.length; i++) { for (let i = 0; i < categories.length; i++) {
if (d[0] === categories[i]) { if (d[0] === categories[i]) {
return 'sectionTitle sectionTitle' + (i % conf.numberSectionStyles) return 'sectionTitle sectionTitle' + (i % conf.numberSectionStyles);
} }
} }
return 'sectionTitle' return 'sectionTitle';
}) });
} }
function drawToday(theSidePad, theTopPad, w, h) { function drawToday(theSidePad, theTopPad, w, h) {
const todayG = svg.append('g') const todayG = svg.append('g').attr('class', 'today');
.attr('class', 'today')
const today = new Date() const today = new Date();
todayG.append('line') todayG
.append('line')
.attr('x1', timeScale(today) + theSidePad) .attr('x1', timeScale(today) + theSidePad)
.attr('x2', timeScale(today) + theSidePad) .attr('x2', timeScale(today) + theSidePad)
.attr('y1', conf.titleTopMargin) .attr('y1', conf.titleTopMargin)
.attr('y2', h - conf.titleTopMargin) .attr('y2', h - conf.titleTopMargin)
.attr('class', 'today') .attr('class', 'today');
} }
// from this stackexchange question: http://stackoverflow.com/questions/1890203/unique-for-arrays-in-javascript // from this stackexchange question: http://stackoverflow.com/questions/1890203/unique-for-arrays-in-javascript
function checkUnique(arr) { function checkUnique(arr) {
const hash = {} const hash = {};
const result = [] const result = [];
for (let i = 0, l = arr.length; i < l; ++i) { for (let i = 0, l = arr.length; i < l; ++i) {
if (!hash.hasOwnProperty(arr[i])) { // it works with objects! in FF, at least if (!hash.hasOwnProperty(arr[i])) {
hash[arr[i]] = true // it works with objects! in FF, at least
result.push(arr[i]) hash[arr[i]] = true;
result.push(arr[i]);
} }
} }
return result return result;
} }
// from this stackexchange question: http://stackoverflow.com/questions/14227981/count-how-many-strings-in-an-array-have-duplicates-in-the-same-array // from this stackexchange question: http://stackoverflow.com/questions/14227981/count-how-many-strings-in-an-array-have-duplicates-in-the-same-array
function getCounts(arr) { function getCounts(arr) {
let i = arr.length // const to loop over let i = arr.length; // const to loop over
const obj = {} // obj to store results const obj = {}; // obj to store results
while (i) { while (i) {
obj[arr[--i]] = (obj[arr[i]] || 0) + 1 // count occurrences obj[arr[--i]] = (obj[arr[i]] || 0) + 1; // count occurrences
} }
return obj return obj;
} }
// get specific from everything // get specific from everything
function getCount(word, arr) { function getCount(word, arr) {
return getCounts(arr)[word] || 0 return getCounts(arr)[word] || 0;
}
} }
};
export default { export default {
setConf, setConf,
draw draw
} };

View File

@ -1,50 +1,52 @@
/* eslint-env jasmine */ /* eslint-env jasmine */
/* eslint-disable no-eval */ /* eslint-disable no-eval */
import { parser } from './gantt' import { parser } from './gantt';
import ganttDb from '../ganttDb' import ganttDb from '../ganttDb';
const parserFnConstructor = (str) => { const parserFnConstructor = str => {
return () => { return () => {
parser.parse(str) parser.parse(str);
} };
} };
describe('when parsing a gantt diagram it', function() { describe('when parsing a gantt diagram it', function() {
beforeEach(function() { beforeEach(function() {
parser.yy = ganttDb parser.yy = ganttDb;
parser.yy.clear() parser.yy.clear();
}) });
it('should handle a dateFormat definition', function() { it('should handle a dateFormat definition', function() {
const str = 'gantt\ndateFormat yyyy-mm-dd' const str = 'gantt\ndateFormat yyyy-mm-dd';
expect(parserFnConstructor(str)).not.toThrow() expect(parserFnConstructor(str)).not.toThrow();
}) });
it('should handle a inclusive end date definition', function() { it('should handle a inclusive end date definition', function() {
const str = 'gantt\ndateFormat yyyy-mm-dd\ninclusiveEndDates' const str = 'gantt\ndateFormat yyyy-mm-dd\ninclusiveEndDates';
expect(parserFnConstructor(str)).not.toThrow() expect(parserFnConstructor(str)).not.toThrow();
}) });
it('should handle a title definition', function() { it('should handle a title definition', function() {
const str = 'gantt\ndateFormat yyyy-mm-dd\ntitle Adding gantt diagram functionality to mermaid' const str = 'gantt\ndateFormat yyyy-mm-dd\ntitle Adding gantt diagram functionality to mermaid';
expect(parserFnConstructor(str)).not.toThrow() expect(parserFnConstructor(str)).not.toThrow();
}) });
it('should handle an excludes definition', function() { it('should handle an excludes definition', function() {
const str = 'gantt\ndateFormat yyyy-mm-dd\ntitle Adding gantt diagram functionality to mermaid\nexcludes weekdays 2019-02-01' const str =
'gantt\ndateFormat yyyy-mm-dd\ntitle Adding gantt diagram functionality to mermaid\nexcludes weekdays 2019-02-01';
expect(parserFnConstructor(str)).not.toThrow() expect(parserFnConstructor(str)).not.toThrow();
}) });
it('should handle a section definition', function() { it('should handle a section definition', function() {
const str = 'gantt\n' + const str =
'gantt\n' +
'dateFormat yyyy-mm-dd\n' + 'dateFormat yyyy-mm-dd\n' +
'title Adding gantt diagram functionality to mermaid\n' + 'title Adding gantt diagram functionality to mermaid\n' +
'excludes weekdays 2019-02-01\n' + 'excludes weekdays 2019-02-01\n' +
'section Documentation' 'section Documentation';
expect(parserFnConstructor(str)).not.toThrow() expect(parserFnConstructor(str)).not.toThrow();
}) });
/** /**
* Beslutsflöde inligt nedan. Obs bla bla bla * Beslutsflöde inligt nedan. Obs bla bla bla
* ``` * ```
@ -57,21 +59,22 @@ describe('when parsing a gantt diagram it', function () {
* params bapa - a unique bapap * params bapa - a unique bapap
*/ */
it('should handle a task definition', function() { it('should handle a task definition', function() {
const str = 'gantt\n' + const str =
'gantt\n' +
'dateFormat YYYY-MM-DD\n' + 'dateFormat YYYY-MM-DD\n' +
'title Adding gantt diagram functionality to mermaid\n' + 'title Adding gantt diagram functionality to mermaid\n' +
'section Documentation\n' + 'section Documentation\n' +
'Design jison grammar:des1, 2014-01-01, 2014-01-04' 'Design jison grammar:des1, 2014-01-01, 2014-01-04';
expect(parserFnConstructor(str)).not.toThrow() expect(parserFnConstructor(str)).not.toThrow();
const tasks = parser.yy.getTasks() const tasks = parser.yy.getTasks();
expect(tasks[0].startTime).toEqual(new Date(2014, 0, 1)) expect(tasks[0].startTime).toEqual(new Date(2014, 0, 1));
expect(tasks[0].endTime).toEqual(new Date(2014, 0, 4)) expect(tasks[0].endTime).toEqual(new Date(2014, 0, 4));
expect(tasks[0].id).toEqual('des1') expect(tasks[0].id).toEqual('des1');
expect(tasks[0].task).toEqual('Design jison grammar') expect(tasks[0].task).toEqual('Design jison grammar');
}) });
it.each` it.each`
tags | milestone | done | crit | active tags | milestone | done | crit | active
${'milestone'} | ${true} | ${false} | ${false} | ${false} ${'milestone'} | ${true} | ${false} | ${false} | ${false}
@ -80,24 +83,27 @@ describe('when parsing a gantt diagram it', function () {
${'active'} | ${false} | ${false} | ${false} | ${true} ${'active'} | ${false} | ${false} | ${false} | ${true}
${'crit,milestone,done'} | ${true} | ${true} | ${true} | ${false} ${'crit,milestone,done'} | ${true} | ${true} | ${true} | ${false}
`('should handle a task with tags $tags', ({ tags, milestone, done, crit, active }) => { `('should handle a task with tags $tags', ({ tags, milestone, done, crit, active }) => {
const str = 'gantt\n' + const str =
'gantt\n' +
'dateFormat YYYY-MM-DD\n' + 'dateFormat YYYY-MM-DD\n' +
'title Adding gantt diagram functionality to mermaid\n' + 'title Adding gantt diagram functionality to mermaid\n' +
'section Documentation\n' + 'section Documentation\n' +
'test task:' + tags + ', 2014-01-01, 2014-01-04' 'test task:' +
tags +
', 2014-01-01, 2014-01-04';
const allowedTags = ['active', 'done', 'crit', 'milestone'] const allowedTags = ['active', 'done', 'crit', 'milestone'];
expect(parserFnConstructor(str)).not.toThrow() expect(parserFnConstructor(str)).not.toThrow();
const tasks = parser.yy.getTasks() const tasks = parser.yy.getTasks();
allowedTags.forEach(function(t) { allowedTags.forEach(function(t) {
if (eval(t)) { if (eval(t)) {
expect(tasks[0][t]).toBeTruthy() expect(tasks[0][t]).toBeTruthy();
} else { } else {
expect(tasks[0][t]).toBeFalsy() expect(tasks[0][t]).toBeFalsy();
} }
}) });
}) });
}) });

View File

@ -1,69 +1,71 @@
import _ from 'lodash' import _ from 'lodash';
import { logger } from '../../logger' import { logger } from '../../logger';
let commits = {} let commits = {};
let head = null let head = null;
let branches = { 'master': head } let branches = { master: head };
let curBranch = 'master' let curBranch = 'master';
let direction = 'LR' let direction = 'LR';
let seq = 0 let seq = 0;
function getRandomInt(min, max) { function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min return Math.floor(Math.random() * (max - min)) + min;
} }
function getId() { function getId() {
const pool = '0123456789abcdef' const pool = '0123456789abcdef';
let id = '' let id = '';
for (let i = 0; i < 7; i++) { for (let i = 0; i < 7; i++) {
id += pool[getRandomInt(0, 16)] id += pool[getRandomInt(0, 16)];
} }
return id return id;
} }
function isfastforwardable(currentCommit, otherCommit) { function isfastforwardable(currentCommit, otherCommit) {
logger.debug('Entering isfastforwardable:', currentCommit.id, otherCommit.id) logger.debug('Entering isfastforwardable:', currentCommit.id, otherCommit.id);
while (currentCommit.seq <= otherCommit.seq && currentCommit !== otherCommit) { while (currentCommit.seq <= otherCommit.seq && currentCommit !== otherCommit) {
// only if other branch has more commits // only if other branch has more commits
if (otherCommit.parent == null) break if (otherCommit.parent == null) break;
if (Array.isArray(otherCommit.parent)) { if (Array.isArray(otherCommit.parent)) {
logger.debug('In merge commit:', otherCommit.parent) logger.debug('In merge commit:', otherCommit.parent);
return isfastforwardable(currentCommit, commits[otherCommit.parent[0]]) || return (
isfastforwardable(currentCommit, commits[otherCommit.parent[0]]) ||
isfastforwardable(currentCommit, commits[otherCommit.parent[1]]) isfastforwardable(currentCommit, commits[otherCommit.parent[1]])
);
} else { } else {
otherCommit = commits[otherCommit.parent] otherCommit = commits[otherCommit.parent];
} }
} }
logger.debug(currentCommit.id, otherCommit.id) logger.debug(currentCommit.id, otherCommit.id);
return currentCommit.id === otherCommit.id return currentCommit.id === otherCommit.id;
} }
function isReachableFrom(currentCommit, otherCommit) { function isReachableFrom(currentCommit, otherCommit) {
const currentSeq = currentCommit.seq const currentSeq = currentCommit.seq;
const otherSeq = otherCommit.seq const otherSeq = otherCommit.seq;
if (currentSeq > otherSeq) return isfastforwardable(otherCommit, currentCommit) if (currentSeq > otherSeq) return isfastforwardable(otherCommit, currentCommit);
return false return false;
} }
export const setDirection = function(dir) { export const setDirection = function(dir) {
direction = dir direction = dir;
} };
let options = {} let options = {};
export const setOptions = function(rawOptString) { export const setOptions = function(rawOptString) {
logger.debug('options str', rawOptString) logger.debug('options str', rawOptString);
rawOptString = rawOptString && rawOptString.trim() rawOptString = rawOptString && rawOptString.trim();
rawOptString = rawOptString || '{}' rawOptString = rawOptString || '{}';
try { try {
options = JSON.parse(rawOptString) options = JSON.parse(rawOptString);
} catch (e) { } catch (e) {
logger.error('error while parsing gitGraph options', e.message) logger.error('error while parsing gitGraph options', e.message);
}
} }
};
export const getOptions = function() { export const getOptions = function() {
return options return options;
} };
export const commit = function(msg) { export const commit = function(msg) {
const commit = { const commit = {
@ -71,28 +73,28 @@ export const commit = function (msg) {
message: msg, message: msg,
seq: seq++, seq: seq++,
parent: head == null ? null : head.id parent: head == null ? null : head.id
} };
head = commit head = commit;
commits[commit.id] = commit commits[commit.id] = commit;
branches[curBranch] = commit.id branches[curBranch] = commit.id;
logger.debug('in pushCommit ' + commit.id) logger.debug('in pushCommit ' + commit.id);
} };
export const branch = function(name) { export const branch = function(name) {
branches[name] = head != null ? head.id : null branches[name] = head != null ? head.id : null;
logger.debug('in createBranch') logger.debug('in createBranch');
} };
export const merge = function(otherBranch) { export const merge = function(otherBranch) {
const currentCommit = commits[branches[curBranch]] const currentCommit = commits[branches[curBranch]];
const otherCommit = commits[branches[otherBranch]] const otherCommit = commits[branches[otherBranch]];
if (isReachableFrom(currentCommit, otherCommit)) { if (isReachableFrom(currentCommit, otherCommit)) {
logger.debug('Already merged') logger.debug('Already merged');
return return;
} }
if (isfastforwardable(currentCommit, otherCommit)) { if (isfastforwardable(currentCommit, otherCommit)) {
branches[curBranch] = branches[otherBranch] branches[curBranch] = branches[otherBranch];
head = commits[branches[curBranch]] head = commits[branches[curBranch]];
} else { } else {
// create merge commit // create merge commit
const commit = { const commit = {
@ -100,113 +102,125 @@ export const merge = function (otherBranch) {
message: 'merged branch ' + otherBranch + ' into ' + curBranch, message: 'merged branch ' + otherBranch + ' into ' + curBranch,
seq: seq++, seq: seq++,
parent: [head == null ? null : head.id, branches[otherBranch]] parent: [head == null ? null : head.id, branches[otherBranch]]
};
head = commit;
commits[commit.id] = commit;
branches[curBranch] = commit.id;
} }
head = commit logger.debug(branches);
commits[commit.id] = commit logger.debug('in mergeBranch');
branches[curBranch] = commit.id };
}
logger.debug(branches)
logger.debug('in mergeBranch')
}
export const checkout = function(branch) { export const checkout = function(branch) {
logger.debug('in checkout') logger.debug('in checkout');
curBranch = branch curBranch = branch;
const id = branches[curBranch] const id = branches[curBranch];
head = commits[id] head = commits[id];
} };
export const reset = function(commitRef) { export const reset = function(commitRef) {
logger.debug('in reset', commitRef) logger.debug('in reset', commitRef);
const ref = commitRef.split(':')[0] const ref = commitRef.split(':')[0];
let parentCount = parseInt(commitRef.split(':')[1]) let parentCount = parseInt(commitRef.split(':')[1]);
let commit = ref === 'HEAD' ? head : commits[branches[ref]] let commit = ref === 'HEAD' ? head : commits[branches[ref]];
logger.debug(commit, parentCount) logger.debug(commit, parentCount);
while (parentCount > 0) { while (parentCount > 0) {
commit = commits[commit.parent] commit = commits[commit.parent];
parentCount-- parentCount--;
if (!commit) { if (!commit) {
const err = 'Critical error - unique parent commit not found during reset' const err = 'Critical error - unique parent commit not found during reset';
logger.error(err) logger.error(err);
throw err throw err;
} }
} }
head = commit head = commit;
branches[curBranch] = commit.id branches[curBranch] = commit.id;
} };
function upsert(arr, key, newval) { function upsert(arr, key, newval) {
const index = arr.indexOf(key) const index = arr.indexOf(key);
if (index === -1) { if (index === -1) {
arr.push(newval) arr.push(newval);
} else { } else {
arr.splice(index, 1, newval) arr.splice(index, 1, newval);
} }
} }
function prettyPrintCommitHistory(commitArr) { function prettyPrintCommitHistory(commitArr) {
const commit = _.maxBy(commitArr, 'seq') const commit = _.maxBy(commitArr, 'seq');
let line = '' let line = '';
commitArr.forEach(function(c) { commitArr.forEach(function(c) {
if (c === commit) { if (c === commit) {
line += '\t*' line += '\t*';
} else { } else {
line += '\t|' line += '\t|';
} }
}) });
const label = [line, commit.id, commit.seq] const label = [line, commit.id, commit.seq];
for (let branch in branches) { for (let branch in branches) {
if (branches[branch] === commit.id) label.push(branch) if (branches[branch] === commit.id) label.push(branch);
} }
logger.debug(label.join(' ')) logger.debug(label.join(' '));
if (Array.isArray(commit.parent)) { if (Array.isArray(commit.parent)) {
const newCommit = commits[commit.parent[0]] const newCommit = commits[commit.parent[0]];
upsert(commitArr, commit, newCommit) upsert(commitArr, commit, newCommit);
commitArr.push(commits[commit.parent[1]]) commitArr.push(commits[commit.parent[1]]);
} else if (commit.parent == null) { } else if (commit.parent == null) {
return return;
} else { } else {
const nextCommit = commits[commit.parent] const nextCommit = commits[commit.parent];
upsert(commitArr, commit, nextCommit) upsert(commitArr, commit, nextCommit);
} }
commitArr = _.uniqBy(commitArr, 'id') commitArr = _.uniqBy(commitArr, 'id');
prettyPrintCommitHistory(commitArr) prettyPrintCommitHistory(commitArr);
} }
export const prettyPrint = function() { export const prettyPrint = function() {
logger.debug(commits) logger.debug(commits);
const node = getCommitsArray()[0] const node = getCommitsArray()[0];
prettyPrintCommitHistory([node]) prettyPrintCommitHistory([node]);
} };
export const clear = function() { export const clear = function() {
commits = {} commits = {};
head = null head = null;
branches = { 'master': head } branches = { master: head };
curBranch = 'master' curBranch = 'master';
seq = 0 seq = 0;
} };
export const getBranchesAsObjArray = function() { export const getBranchesAsObjArray = function() {
const branchArr = [] const branchArr = [];
for (let branch in branches) { for (let branch in branches) {
branchArr.push({ name: branch, commit: commits[branches[branch]] }) branchArr.push({ name: branch, commit: commits[branches[branch]] });
}
return branchArr
} }
return branchArr;
};
export const getBranches = function () { return branches } export const getBranches = function() {
export const getCommits = function () { return commits } return branches;
};
export const getCommits = function() {
return commits;
};
export const getCommitsArray = function() { export const getCommitsArray = function() {
const commitArr = Object.keys(commits).map(function(key) { const commitArr = Object.keys(commits).map(function(key) {
return commits[key] return commits[key];
}) });
commitArr.forEach(function (o) { logger.debug(o.id) }) commitArr.forEach(function(o) {
return _.orderBy(commitArr, ['seq'], ['desc']) logger.debug(o.id);
} });
export const getCurrentBranch = function () { return curBranch } return _.orderBy(commitArr, ['seq'], ['desc']);
export const getDirection = function () { return direction } };
export const getHead = function () { return head } export const getCurrentBranch = function() {
return curBranch;
};
export const getDirection = function() {
return direction;
};
export const getHead = function() {
return head;
};
export default { export default {
setDirection, setDirection,
@ -226,4 +240,4 @@ export default {
getCurrentBranch, getCurrentBranch,
getDirection, getDirection,
getHead getHead
} };

View File

@ -1,201 +1,186 @@
/* eslint-env jasmine */ /* eslint-env jasmine */
import gitGraphAst from './gitGraphAst' import gitGraphAst from './gitGraphAst';
import { parser } from './parser/gitGraph' import { parser } from './parser/gitGraph';
describe('when parsing a gitGraph', function() { describe('when parsing a gitGraph', function() {
beforeEach(function() { beforeEach(function() {
parser.yy = gitGraphAst parser.yy = gitGraphAst;
parser.yy.clear() parser.yy.clear();
}) });
it('should handle a gitGraph defintion', function() { it('should handle a gitGraph defintion', function() {
const str = 'gitGraph:\n' + const str = 'gitGraph:\n' + 'commit\n';
'commit\n'
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(1) expect(Object.keys(commits).length).toBe(1);
expect(parser.yy.getCurrentBranch()).toBe('master') expect(parser.yy.getCurrentBranch()).toBe('master');
expect(parser.yy.getDirection()).toBe('LR') expect(parser.yy.getDirection()).toBe('LR');
expect(Object.keys(parser.yy.getBranches()).length).toBe(1) expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
}) });
it('should handle a gitGraph defintion with empty options', function() { it('should handle a gitGraph defintion with empty options', function() {
const str = 'gitGraph:\n' + const str = 'gitGraph:\n' + 'options\n' + 'end\n' + 'commit\n';
'options\n' +
'end\n' +
'commit\n'
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(parser.yy.getOptions()).toEqual({}) expect(parser.yy.getOptions()).toEqual({});
expect(Object.keys(commits).length).toBe(1) expect(Object.keys(commits).length).toBe(1);
expect(parser.yy.getCurrentBranch()).toBe('master') expect(parser.yy.getCurrentBranch()).toBe('master');
expect(parser.yy.getDirection()).toBe('LR') expect(parser.yy.getDirection()).toBe('LR');
expect(Object.keys(parser.yy.getBranches()).length).toBe(1) expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
}) });
it('should handle a gitGraph defintion with valid options', function() { it('should handle a gitGraph defintion with valid options', function() {
const str = 'gitGraph:\n' + const str = 'gitGraph:\n' + 'options\n' + '{"key": "value"}\n' + 'end\n' + 'commit\n';
'options\n' +
'{"key": "value"}\n' +
'end\n' +
'commit\n'
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(parser.yy.getOptions()['key']).toBe('value') expect(parser.yy.getOptions()['key']).toBe('value');
expect(Object.keys(commits).length).toBe(1) expect(Object.keys(commits).length).toBe(1);
expect(parser.yy.getCurrentBranch()).toBe('master') expect(parser.yy.getCurrentBranch()).toBe('master');
expect(parser.yy.getDirection()).toBe('LR') expect(parser.yy.getDirection()).toBe('LR');
expect(Object.keys(parser.yy.getBranches()).length).toBe(1) expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
}) });
it('should not fail on a gitGraph with malformed json', function() { it('should not fail on a gitGraph with malformed json', function() {
const str = 'gitGraph:\n' + const str = 'gitGraph:\n' + 'options\n' + '{"key": "value"\n' + 'end\n' + 'commit\n';
'options\n' +
'{"key": "value"\n' +
'end\n' +
'commit\n'
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(1) expect(Object.keys(commits).length).toBe(1);
expect(parser.yy.getCurrentBranch()).toBe('master') expect(parser.yy.getCurrentBranch()).toBe('master');
expect(parser.yy.getDirection()).toBe('LR') expect(parser.yy.getDirection()).toBe('LR');
expect(Object.keys(parser.yy.getBranches()).length).toBe(1) expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
}) });
it('should handle set direction', function() { it('should handle set direction', function() {
const str = 'gitGraph BT:\n' + const str = 'gitGraph BT:\n' + 'commit\n';
'commit\n'
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(1) expect(Object.keys(commits).length).toBe(1);
expect(parser.yy.getCurrentBranch()).toBe('master') expect(parser.yy.getCurrentBranch()).toBe('master');
expect(parser.yy.getDirection()).toBe('BT') expect(parser.yy.getDirection()).toBe('BT');
expect(Object.keys(parser.yy.getBranches()).length).toBe(1) expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
}) });
it('should checkout a branch', function() { it('should checkout a branch', function() {
const str = 'gitGraph:\n' + const str = 'gitGraph:\n' + 'branch new\n' + 'checkout new\n';
'branch new\n' +
'checkout new\n'
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(0) expect(Object.keys(commits).length).toBe(0);
expect(parser.yy.getCurrentBranch()).toBe('new') expect(parser.yy.getCurrentBranch()).toBe('new');
}) });
it('should add commits to checked out branch', function() { it('should add commits to checked out branch', function() {
const str = 'gitGraph:\n' + const str = 'gitGraph:\n' + 'branch new\n' + 'checkout new\n' + 'commit\n' + 'commit\n';
'branch new\n' +
'checkout new\n' +
'commit\n' +
'commit\n'
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(2) expect(Object.keys(commits).length).toBe(2);
expect(parser.yy.getCurrentBranch()).toBe('new') expect(parser.yy.getCurrentBranch()).toBe('new');
const branchCommit = parser.yy.getBranches()['new'] const branchCommit = parser.yy.getBranches()['new'];
expect(branchCommit).not.toBeNull() expect(branchCommit).not.toBeNull();
expect(commits[branchCommit].parent).not.toBeNull() expect(commits[branchCommit].parent).not.toBeNull();
}) });
it('should handle commit with args', function() { it('should handle commit with args', function() {
const str = 'gitGraph:\n' + const str = 'gitGraph:\n' + 'commit "a commit"\n';
'commit "a commit"\n'
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(1) expect(Object.keys(commits).length).toBe(1);
const key = Object.keys(commits)[0] const key = Object.keys(commits)[0];
expect(commits[key].message).toBe('a commit') expect(commits[key].message).toBe('a commit');
expect(parser.yy.getCurrentBranch()).toBe('master') expect(parser.yy.getCurrentBranch()).toBe('master');
}) });
it('it should reset a branch', function() { it('it should reset a branch', function() {
const str = 'gitGraph:\n' + const str =
'gitGraph:\n' +
'commit\n' + 'commit\n' +
'commit\n' + 'commit\n' +
'branch newbranch\n' + 'branch newbranch\n' +
'checkout newbranch\n' + 'checkout newbranch\n' +
'commit\n' + 'commit\n' +
'reset master\n' 'reset master\n';
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(3) expect(Object.keys(commits).length).toBe(3);
expect(parser.yy.getCurrentBranch()).toBe('newbranch') expect(parser.yy.getCurrentBranch()).toBe('newbranch');
expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['master']) expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['master']);
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['newbranch']) expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['newbranch']);
}) });
it('reset can take an argument', function() { it('reset can take an argument', function() {
const str = 'gitGraph:\n' + const str =
'gitGraph:\n' +
'commit\n' + 'commit\n' +
'commit\n' + 'commit\n' +
'branch newbranch\n' + 'branch newbranch\n' +
'checkout newbranch\n' + 'checkout newbranch\n' +
'commit\n' + 'commit\n' +
'reset master^\n' 'reset master^\n';
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(3) expect(Object.keys(commits).length).toBe(3);
expect(parser.yy.getCurrentBranch()).toBe('newbranch') expect(parser.yy.getCurrentBranch()).toBe('newbranch');
const master = commits[parser.yy.getBranches()['master']] const master = commits[parser.yy.getBranches()['master']];
expect(parser.yy.getHead().id).toEqual(master.parent) expect(parser.yy.getHead().id).toEqual(master.parent);
}) });
it('it should handle fast forwardable merges', function() { it('it should handle fast forwardable merges', function() {
const str = 'gitGraph:\n' + const str =
'gitGraph:\n' +
'commit\n' + 'commit\n' +
'branch newbranch\n' + 'branch newbranch\n' +
'checkout newbranch\n' + 'checkout newbranch\n' +
'commit\n' + 'commit\n' +
'commit\n' + 'commit\n' +
'checkout master\n' + 'checkout master\n' +
'merge newbranch\n' 'merge newbranch\n';
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(3) expect(Object.keys(commits).length).toBe(3);
expect(parser.yy.getCurrentBranch()).toBe('master') expect(parser.yy.getCurrentBranch()).toBe('master');
expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['master']) expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['master']);
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['newbranch']) expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['newbranch']);
}) });
it('it should handle cases when merge is a noop', function() { it('it should handle cases when merge is a noop', function() {
const str = 'gitGraph:\n' + const str =
'gitGraph:\n' +
'commit\n' + 'commit\n' +
'branch newbranch\n' + 'branch newbranch\n' +
'checkout newbranch\n' + 'checkout newbranch\n' +
'commit\n' + 'commit\n' +
'commit\n' + 'commit\n' +
'merge master\n' 'merge master\n';
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(3) expect(Object.keys(commits).length).toBe(3);
expect(parser.yy.getCurrentBranch()).toBe('newbranch') expect(parser.yy.getCurrentBranch()).toBe('newbranch');
expect(parser.yy.getBranches()['newbranch']).not.toEqual(parser.yy.getBranches()['master']) expect(parser.yy.getBranches()['newbranch']).not.toEqual(parser.yy.getBranches()['master']);
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['newbranch']) expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['newbranch']);
}) });
it('it should handle merge with 2 parents', function() { it('it should handle merge with 2 parents', function() {
const str = 'gitGraph:\n' + const str =
'gitGraph:\n' +
'commit\n' + 'commit\n' +
'branch newbranch\n' + 'branch newbranch\n' +
'checkout newbranch\n' + 'checkout newbranch\n' +
@ -203,19 +188,20 @@ describe('when parsing a gitGraph', function () {
'commit\n' + 'commit\n' +
'checkout master\n' + 'checkout master\n' +
'commit\n' + 'commit\n' +
'merge newbranch\n' 'merge newbranch\n';
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(5) expect(Object.keys(commits).length).toBe(5);
expect(parser.yy.getCurrentBranch()).toBe('master') expect(parser.yy.getCurrentBranch()).toBe('master');
expect(parser.yy.getBranches()['newbranch']).not.toEqual(parser.yy.getBranches()['master']) expect(parser.yy.getBranches()['newbranch']).not.toEqual(parser.yy.getBranches()['master']);
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['master']) expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['master']);
}) });
it('it should handle ff merge when history walk has two parents (merge commit)', function() { it('it should handle ff merge when history walk has two parents (merge commit)', function() {
const str = 'gitGraph:\n' + const str =
'gitGraph:\n' +
'commit\n' + 'commit\n' +
'branch newbranch\n' + 'branch newbranch\n' +
'checkout newbranch\n' + 'checkout newbranch\n' +
@ -226,16 +212,16 @@ describe('when parsing a gitGraph', function () {
'merge newbranch\n' + 'merge newbranch\n' +
'commit\n' + 'commit\n' +
'checkout newbranch\n' + 'checkout newbranch\n' +
'merge master\n' 'merge master\n';
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(6) expect(Object.keys(commits).length).toBe(6);
expect(parser.yy.getCurrentBranch()).toBe('newbranch') expect(parser.yy.getCurrentBranch()).toBe('newbranch');
expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['master']) expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['master']);
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['master']) expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['master']);
parser.yy.prettyPrint() parser.yy.prettyPrint();
}) });
}) });

View File

@ -1,13 +1,13 @@
import * as d3 from 'd3' import * as d3 from 'd3';
import _ from 'lodash' import _ from 'lodash';
import db from './gitGraphAst' import db from './gitGraphAst';
import gitGraphParser from './parser/gitGraph' import gitGraphParser from './parser/gitGraph';
import { logger } from '../../logger' import { logger } from '../../logger';
import { interpolateToCurve } from '../../utils' import { interpolateToCurve } from '../../utils';
let allCommitsDict = {} let allCommitsDict = {};
let branchNum let branchNum;
let config = { let config = {
nodeSpacing: 150, nodeSpacing: 150,
nodeFillColor: 'yellow', nodeFillColor: 'yellow',
@ -25,11 +25,11 @@ let config = {
x: -25, x: -25,
y: 0 y: 0
} }
} };
let apiConfig = {} let apiConfig = {};
export const setConf = function(c) { export const setConf = function(c) {
apiConfig = c apiConfig = c;
} };
function svgCreateDefs(svg) { function svgCreateDefs(svg) {
svg svg
@ -39,8 +39,9 @@ function svgCreateDefs (svg) {
.append('circle') .append('circle')
.attr('r', config.nodeRadius) .attr('r', config.nodeRadius)
.attr('cx', 0) .attr('cx', 0)
.attr('cy', 0) .attr('cy', 0);
svg.select('#def-commit') svg
.select('#def-commit')
.append('foreignObject') .append('foreignObject')
.attr('width', config.nodeLabel.width) .attr('width', config.nodeLabel.width)
.attr('height', config.nodeLabel.height) .attr('height', config.nodeLabel.height)
@ -49,239 +50,293 @@ function svgCreateDefs (svg) {
.attr('class', 'node-label') .attr('class', 'node-label')
.attr('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility') .attr('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility')
.append('p') .append('p')
.html('') .html('');
} }
function svgDrawLine(svg, points, colorIdx, interpolate) { function svgDrawLine(svg, points, colorIdx, interpolate) {
const curve = interpolateToCurve(interpolate, d3.curveBasis) const curve = interpolateToCurve(interpolate, d3.curveBasis);
const color = config.branchColors[colorIdx % config.branchColors.length] const color = config.branchColors[colorIdx % config.branchColors.length];
const lineGen = d3.line() const lineGen = d3
.line()
.x(function(d) { .x(function(d) {
return Math.round(d.x) return Math.round(d.x);
}) })
.y(function(d) { .y(function(d) {
return Math.round(d.y) return Math.round(d.y);
}) })
.curve(curve) .curve(curve);
svg svg
.append('svg:path') .append('svg:path')
.attr('d', lineGen(points)) .attr('d', lineGen(points))
.style('stroke', color) .style('stroke', color)
.style('stroke-width', config.lineStrokeWidth) .style('stroke-width', config.lineStrokeWidth)
.style('fill', 'none') .style('fill', 'none');
} }
// Pass in the element and its pre-transform coords // Pass in the element and its pre-transform coords
function getElementCoords(element, coords) { function getElementCoords(element, coords) {
coords = coords || element.node().getBBox() coords = coords || element.node().getBBox();
const ctm = element.node().getCTM() const ctm = element.node().getCTM();
const xn = ctm.e + coords.x * ctm.a const xn = ctm.e + coords.x * ctm.a;
const yn = ctm.f + coords.y * ctm.d const yn = ctm.f + coords.y * ctm.d;
return { return {
left: xn, left: xn,
top: yn, top: yn,
width: coords.width, width: coords.width,
height: coords.height height: coords.height
} };
} }
function svgDrawLineForCommits(svg, fromId, toId, direction, color) { function svgDrawLineForCommits(svg, fromId, toId, direction, color) {
logger.debug('svgDrawLineForCommits: ', fromId, toId) logger.debug('svgDrawLineForCommits: ', fromId, toId);
const fromBbox = getElementCoords(svg.select('#node-' + fromId + ' circle')) const fromBbox = getElementCoords(svg.select('#node-' + fromId + ' circle'));
const toBbox = getElementCoords(svg.select('#node-' + toId + ' circle')) const toBbox = getElementCoords(svg.select('#node-' + toId + ' circle'));
switch (direction) { switch (direction) {
case 'LR': case 'LR':
// (toBbox) // (toBbox)
// +-------- // +--------
// + (fromBbox) // + (fromBbox)
if (fromBbox.left - toBbox.left > config.nodeSpacing) { if (fromBbox.left - toBbox.left > config.nodeSpacing) {
const lineStart = { x: fromBbox.left - config.nodeSpacing, y: toBbox.top + toBbox.height / 2 } const lineStart = {
const lineEnd = { x: toBbox.left + toBbox.width, y: toBbox.top + toBbox.height / 2 } x: fromBbox.left - config.nodeSpacing,
svgDrawLine(svg, [lineStart, lineEnd], color, 'linear') y: toBbox.top + toBbox.height / 2
svgDrawLine(svg, [ };
const lineEnd = { x: toBbox.left + toBbox.width, y: toBbox.top + toBbox.height / 2 };
svgDrawLine(svg, [lineStart, lineEnd], color, 'linear');
svgDrawLine(
svg,
[
{ x: fromBbox.left, y: fromBbox.top + fromBbox.height / 2 }, { x: fromBbox.left, y: fromBbox.top + fromBbox.height / 2 },
{ x: fromBbox.left - config.nodeSpacing / 2, y: fromBbox.top + fromBbox.height / 2 }, { x: fromBbox.left - config.nodeSpacing / 2, y: fromBbox.top + fromBbox.height / 2 },
{ x: fromBbox.left - config.nodeSpacing / 2, y: lineStart.y }, { x: fromBbox.left - config.nodeSpacing / 2, y: lineStart.y },
lineStart], color) lineStart
],
color
);
} else { } else {
svgDrawLine(svg, [{ svgDrawLine(
'x': fromBbox.left, svg,
'y': fromBbox.top + fromBbox.height / 2 [
}, { {
'x': fromBbox.left - config.nodeSpacing / 2, x: fromBbox.left,
'y': fromBbox.top + fromBbox.height / 2 y: fromBbox.top + fromBbox.height / 2
}, { },
'x': fromBbox.left - config.nodeSpacing / 2, {
'y': toBbox.top + toBbox.height / 2 x: fromBbox.left - config.nodeSpacing / 2,
}, { y: fromBbox.top + fromBbox.height / 2
'x': toBbox.left + toBbox.width, },
'y': toBbox.top + toBbox.height / 2 {
}], color) x: fromBbox.left - config.nodeSpacing / 2,
y: toBbox.top + toBbox.height / 2
},
{
x: toBbox.left + toBbox.width,
y: toBbox.top + toBbox.height / 2
} }
break ],
color
);
}
break;
case 'BT': case 'BT':
// + (fromBbox) // + (fromBbox)
// | // |
// | // |
// + (toBbox) // + (toBbox)
if (toBbox.top - fromBbox.top > config.nodeSpacing) { if (toBbox.top - fromBbox.top > config.nodeSpacing) {
const lineStart = { x: toBbox.left + toBbox.width / 2, y: fromBbox.top + fromBbox.height + config.nodeSpacing } const lineStart = {
const lineEnd = { x: toBbox.left + toBbox.width / 2, y: toBbox.top } x: toBbox.left + toBbox.width / 2,
svgDrawLine(svg, [lineStart, lineEnd], color, 'linear') y: fromBbox.top + fromBbox.height + config.nodeSpacing
svgDrawLine(svg, [ };
const lineEnd = { x: toBbox.left + toBbox.width / 2, y: toBbox.top };
svgDrawLine(svg, [lineStart, lineEnd], color, 'linear');
svgDrawLine(
svg,
[
{ x: fromBbox.left + fromBbox.width / 2, y: fromBbox.top + fromBbox.height }, { x: fromBbox.left + fromBbox.width / 2, y: fromBbox.top + fromBbox.height },
{ x: fromBbox.left + fromBbox.width / 2, y: fromBbox.top + fromBbox.height + config.nodeSpacing / 2 }, {
x: fromBbox.left + fromBbox.width / 2,
y: fromBbox.top + fromBbox.height + config.nodeSpacing / 2
},
{ x: toBbox.left + toBbox.width / 2, y: lineStart.y - config.nodeSpacing / 2 }, { x: toBbox.left + toBbox.width / 2, y: lineStart.y - config.nodeSpacing / 2 },
lineStart], color) lineStart
],
color
);
} else { } else {
svgDrawLine(svg, [{ svgDrawLine(
'x': fromBbox.left + fromBbox.width / 2, svg,
'y': fromBbox.top + fromBbox.height [
}, { {
'x': fromBbox.left + fromBbox.width / 2, x: fromBbox.left + fromBbox.width / 2,
'y': fromBbox.top + config.nodeSpacing / 2 y: fromBbox.top + fromBbox.height
}, { },
'x': toBbox.left + toBbox.width / 2, {
'y': toBbox.top - config.nodeSpacing / 2 x: fromBbox.left + fromBbox.width / 2,
}, { y: fromBbox.top + config.nodeSpacing / 2
'x': toBbox.left + toBbox.width / 2, },
'y': toBbox.top {
}], color) x: toBbox.left + toBbox.width / 2,
y: toBbox.top - config.nodeSpacing / 2
},
{
x: toBbox.left + toBbox.width / 2,
y: toBbox.top
} }
break ],
color
);
}
break;
} }
} }
function cloneNode(svg, selector) { function cloneNode(svg, selector) {
return svg.select(selector).node().cloneNode(true) return svg
.select(selector)
.node()
.cloneNode(true);
} }
function renderCommitHistory(svg, commitid, branches, direction) { function renderCommitHistory(svg, commitid, branches, direction) {
let commit let commit;
const numCommits = Object.keys(allCommitsDict).length const numCommits = Object.keys(allCommitsDict).length;
if (typeof commitid === 'string') { if (typeof commitid === 'string') {
do { do {
commit = allCommitsDict[commitid] commit = allCommitsDict[commitid];
logger.debug('in renderCommitHistory', commit.id, commit.seq) logger.debug('in renderCommitHistory', commit.id, commit.seq);
if (svg.select('#node-' + commitid).size() > 0) { if (svg.select('#node-' + commitid).size() > 0) {
return return;
} }
svg svg
.append(function() { .append(function() {
return cloneNode(svg, '#def-commit') return cloneNode(svg, '#def-commit');
}) })
.attr('class', 'commit') .attr('class', 'commit')
.attr('id', function() { .attr('id', function() {
return 'node-' + commit.id return 'node-' + commit.id;
}) })
.attr('transform', function() { .attr('transform', function() {
switch (direction) { switch (direction) {
case 'LR': case 'LR':
return 'translate(' + (commit.seq * config.nodeSpacing + config.leftMargin) + ', ' + return (
(branchNum * config.branchOffset) + ')' 'translate(' +
(commit.seq * config.nodeSpacing + config.leftMargin) +
', ' +
branchNum * config.branchOffset +
')'
);
case 'BT': case 'BT':
return 'translate(' + (branchNum * config.branchOffset + config.leftMargin) + ', ' + return (
((numCommits - commit.seq) * config.nodeSpacing) + ')' 'translate(' +
(branchNum * config.branchOffset + config.leftMargin) +
', ' +
(numCommits - commit.seq) * config.nodeSpacing +
')'
);
} }
}) })
.attr('fill', config.nodeFillColor) .attr('fill', config.nodeFillColor)
.attr('stroke', config.nodeStrokeColor) .attr('stroke', config.nodeStrokeColor)
.attr('stroke-width', config.nodeStrokeWidth) .attr('stroke-width', config.nodeStrokeWidth);
let branch let branch;
for (let branchName in branches) { for (let branchName in branches) {
if (branches[branchName].commit === commit) { if (branches[branchName].commit === commit) {
branch = branches[branchName] branch = branches[branchName];
break break;
} }
} }
if (branch) { if (branch) {
logger.debug('found branch ', branch.name) logger.debug('found branch ', branch.name);
svg.select('#node-' + commit.id + ' p') svg
.select('#node-' + commit.id + ' p')
.append('xhtml:span') .append('xhtml:span')
.attr('class', 'branch-label') .attr('class', 'branch-label')
.text(branch.name + ', ') .text(branch.name + ', ');
} }
svg.select('#node-' + commit.id + ' p') svg
.select('#node-' + commit.id + ' p')
.append('xhtml:span') .append('xhtml:span')
.attr('class', 'commit-id') .attr('class', 'commit-id')
.text(commit.id) .text(commit.id);
if (commit.message !== '' && direction === 'BT') { if (commit.message !== '' && direction === 'BT') {
svg.select('#node-' + commit.id + ' p') svg
.select('#node-' + commit.id + ' p')
.append('xhtml:span') .append('xhtml:span')
.attr('class', 'commit-msg') .attr('class', 'commit-msg')
.text(', ' + commit.message) .text(', ' + commit.message);
} }
commitid = commit.parent commitid = commit.parent;
} while (commitid && allCommitsDict[commitid]) } while (commitid && allCommitsDict[commitid]);
} }
if (Array.isArray(commitid)) { if (Array.isArray(commitid)) {
logger.debug('found merge commmit', commitid) logger.debug('found merge commmit', commitid);
renderCommitHistory(svg, commitid[0], branches, direction) renderCommitHistory(svg, commitid[0], branches, direction);
branchNum++ branchNum++;
renderCommitHistory(svg, commitid[1], branches, direction) renderCommitHistory(svg, commitid[1], branches, direction);
branchNum-- branchNum--;
} }
} }
function renderLines(svg, commit, direction, branchColor) { function renderLines(svg, commit, direction, branchColor) {
branchColor = branchColor || 0 branchColor = branchColor || 0;
while (commit.seq > 0 && !commit.lineDrawn) { while (commit.seq > 0 && !commit.lineDrawn) {
if (typeof commit.parent === 'string') { if (typeof commit.parent === 'string') {
svgDrawLineForCommits(svg, commit.id, commit.parent, direction, branchColor) svgDrawLineForCommits(svg, commit.id, commit.parent, direction, branchColor);
commit.lineDrawn = true commit.lineDrawn = true;
commit = allCommitsDict[commit.parent] commit = allCommitsDict[commit.parent];
} else if (Array.isArray(commit.parent)) { } else if (Array.isArray(commit.parent)) {
svgDrawLineForCommits(svg, commit.id, commit.parent[0], direction, branchColor) svgDrawLineForCommits(svg, commit.id, commit.parent[0], direction, branchColor);
svgDrawLineForCommits(svg, commit.id, commit.parent[1], direction, branchColor + 1) svgDrawLineForCommits(svg, commit.id, commit.parent[1], direction, branchColor + 1);
renderLines(svg, allCommitsDict[commit.parent[1]], direction, branchColor + 1) renderLines(svg, allCommitsDict[commit.parent[1]], direction, branchColor + 1);
commit.lineDrawn = true commit.lineDrawn = true;
commit = allCommitsDict[commit.parent[0]] commit = allCommitsDict[commit.parent[0]];
} }
} }
} }
export const draw = function(txt, id, ver) { export const draw = function(txt, id, ver) {
try { try {
const parser = gitGraphParser.parser const parser = gitGraphParser.parser;
parser.yy = db parser.yy = db;
logger.debug('in gitgraph renderer', txt, id, ver) logger.debug('in gitgraph renderer', txt, id, ver);
// Parse the graph definition // Parse the graph definition
parser.parse(txt + '\n') parser.parse(txt + '\n');
config = _.assign(config, apiConfig, db.getOptions()) config = _.assign(config, apiConfig, db.getOptions());
logger.debug('effective options', config) logger.debug('effective options', config);
const direction = db.getDirection() const direction = db.getDirection();
allCommitsDict = db.getCommits() allCommitsDict = db.getCommits();
const branches = db.getBranchesAsObjArray() const branches = db.getBranchesAsObjArray();
if (direction === 'BT') { if (direction === 'BT') {
config.nodeLabel.x = branches.length * config.branchOffset config.nodeLabel.x = branches.length * config.branchOffset;
config.nodeLabel.width = '100%' config.nodeLabel.width = '100%';
config.nodeLabel.y = -1 * 2 * config.nodeRadius config.nodeLabel.y = -1 * 2 * config.nodeRadius;
} }
const svg = d3.select(`[id="${id}"]`) const svg = d3.select(`[id="${id}"]`);
svgCreateDefs(svg) svgCreateDefs(svg);
branchNum = 1 branchNum = 1;
for (let branch in branches) { for (let branch in branches) {
const v = branches[branch] const v = branches[branch];
renderCommitHistory(svg, v.commit.id, branches, direction) renderCommitHistory(svg, v.commit.id, branches, direction);
renderLines(svg, v.commit, direction) renderLines(svg, v.commit, direction);
branchNum++ branchNum++;
} }
svg.attr('height', function() { svg.attr('height', function() {
if (direction === 'BT') return Object.keys(allCommitsDict).length * config.nodeSpacing if (direction === 'BT') return Object.keys(allCommitsDict).length * config.nodeSpacing;
return (branches.length + 1) * config.branchOffset return (branches.length + 1) * config.branchOffset;
}) });
} catch (e) { } catch (e) {
logger.error('Error while rendering gitgraph') logger.error('Error while rendering gitgraph');
logger.error(e.message) logger.error(e.message);
}
} }
};
export default { export default {
setConf, setConf,
draw draw
} };

View File

@ -1,15 +1,15 @@
/* eslint-env jasmine */ /* eslint-env jasmine */
describe('when parsing an info graph it', function() { describe('when parsing an info graph it', function() {
var ex var ex;
beforeEach(function() { beforeEach(function() {
ex = require('./parser/info').parser ex = require('./parser/info').parser;
ex.yy = require('./infoDb') ex.yy = require('./infoDb');
}) });
it('should handle an info definition', function() { it('should handle an info definition', function() {
var str = `info var str = `info
showInfo` showInfo`;
ex.parse(str) ex.parse(str);
}) });
}) });

View File

@ -1,27 +1,27 @@
/** /**
* Created by knut on 15-01-14. * Created by knut on 15-01-14.
*/ */
import { logger } from '../../logger' import { logger } from '../../logger';
var message = '' var message = '';
var info = false var info = false;
export const setMessage = txt => { export const setMessage = txt => {
logger.debug('Setting message to: ' + txt) logger.debug('Setting message to: ' + txt);
message = txt message = txt;
} };
export const getMessage = () => { export const getMessage = () => {
return message return message;
} };
export const setInfo = inf => { export const setInfo = inf => {
info = inf info = inf;
} };
export const getInfo = () => { export const getInfo = () => {
return info return info;
} };
// export const parseError = (err, hash) => { // export const parseError = (err, hash) => {
// global.mermaidAPI.parseError(err, hash) // global.mermaidAPI.parseError(err, hash)
@ -33,4 +33,4 @@ export default {
setInfo, setInfo,
getInfo getInfo
// parseError // parseError
} };

View File

@ -1,20 +1,19 @@
/** /**
* Created by knut on 14-12-11. * Created by knut on 14-12-11.
*/ */
import * as d3 from 'd3' import * as d3 from 'd3';
import db from './infoDb' import db from './infoDb';
import infoParser from './parser/info' import infoParser from './parser/info';
import { logger } from '../../logger' import { logger } from '../../logger';
const conf = { const conf = {};
}
export const setConf = function(cnf) { export const setConf = function(cnf) {
const keys = Object.keys(cnf) const keys = Object.keys(cnf);
keys.forEach(function(key) { keys.forEach(function(key) {
conf[key] = cnf[key] conf[key] = cnf[key];
}) });
} };
/** /**
* Draws a an info picture in the tag with id: id based on the graph definition in text. * Draws a an info picture in the tag with id: id based on the graph definition in text.
@ -23,16 +22,16 @@ export const setConf = function (cnf) {
*/ */
export const draw = (txt, id, ver) => { export const draw = (txt, id, ver) => {
try { try {
const parser = infoParser.parser const parser = infoParser.parser;
parser.yy = db parser.yy = db;
logger.debug('Renering info diagram\n' + txt) logger.debug('Renering info diagram\n' + txt);
// Parse the graph definition // Parse the graph definition
parser.parse(txt) parser.parse(txt);
logger.debug('Parsed info diagram') logger.debug('Parsed info diagram');
// Fetch the default direction, use TD if none was found // Fetch the default direction, use TD if none was found
const svg = d3.select('#' + id) const svg = d3.select('#' + id);
const g = svg.append('g') const g = svg.append('g');
g.append('text') // text label for the x axis g.append('text') // text label for the x axis
.attr('x', 100) .attr('x', 100)
@ -40,18 +39,18 @@ export const draw = (txt, id, ver) => {
.attr('class', 'version') .attr('class', 'version')
.attr('font-size', '32px') .attr('font-size', '32px')
.style('text-anchor', 'middle') .style('text-anchor', 'middle')
.text('v ' + ver) .text('v ' + ver);
svg.attr('height', 100) svg.attr('height', 100);
svg.attr('width', 400) svg.attr('width', 400);
// svg.attr('viewBox', '0 0 300 150'); // svg.attr('viewBox', '0 0 300 150');
} catch (e) { } catch (e) {
logger.error('Error while rendering info diagram') logger.error('Error while rendering info diagram');
logger.error(e.message) logger.error(e.message);
}
} }
};
export default { export default {
setConf, setConf,
draw draw
} };

View File

@ -1,37 +1,36 @@
/* eslint-env jasmine */ /* eslint-env jasmine */
import pieDb from '../pieDb' import pieDb from '../pieDb';
import pie from './pie' import pie from './pie';
import { setConfig } from '../../../config' import { setConfig } from '../../../config';
setConfig({ setConfig({
securityLevel: 'strict' securityLevel: 'strict'
}) });
describe('when parsing pie', function() { describe('when parsing pie', function() {
beforeEach(function() { beforeEach(function() {
pie.parser.yy = pieDb pie.parser.yy = pieDb;
pie.parser.yy.clear() pie.parser.yy.clear();
}) });
it('should handle simple pie', function() { it('should handle simple pie', function() {
const res = pie.parser.parse('pie \n"ash" : 60\n"bat" : 40\n') const res = pie.parser.parse('pie \n"ash" : 60\n"bat" : 40\n');
const sections = pieDb.getSections() const sections = pieDb.getSections();
console.log('sections: ', sections) console.log('sections: ', sections);
const section1 = sections['ash'] const section1 = sections['ash'];
expect(section1).toBe(60) expect(section1).toBe(60);
}) });
it('should handle simple pie with positive decimal', function() { it('should handle simple pie with positive decimal', function() {
const res = pie.parser.parse('pie \n"ash" : 60.67\n"bat" : 40\n') const res = pie.parser.parse('pie \n"ash" : 60.67\n"bat" : 40\n');
const sections = pieDb.getSections() const sections = pieDb.getSections();
console.log('sections: ', sections) console.log('sections: ', sections);
const section1 = sections['ash'] const section1 = sections['ash'];
expect(section1).toBe(60.67) expect(section1).toBe(60.67);
}) });
it('should handle simple pie with negative decimal', function() { it('should handle simple pie with negative decimal', function() {
expect(() => { expect(() => {
pie.parser.parse('pie \n"ash" : 60.67\n"bat" : 40..12\n'); pie.parser.parse('pie \n"ash" : 60.67\n"bat" : 40..12\n');
}).toThrowError(); }).toThrowError();
}) });
}) });

View File

@ -1,40 +1,40 @@
/** /**
* *
*/ */
import { logger } from '../../logger' import { logger } from '../../logger';
let sections = {} let sections = {};
let title = '' let title = '';
const addSection = function(id, value) { const addSection = function(id, value) {
if (typeof sections[id] === 'undefined') { if (typeof sections[id] === 'undefined') {
sections[id] = value sections[id] = value;
logger.debug('Added new section :', id) logger.debug('Added new section :', id);
// console.log('Added new section:', id, value) // console.log('Added new section:', id, value)
} }
} };
const getSections = () => sections const getSections = () => sections;
const setTitle = function(txt) { const setTitle = function(txt) {
title = txt title = txt;
} };
const getTitle = function() { const getTitle = function() {
return title return title;
} };
const cleanupValue = function(value) { const cleanupValue = function(value) {
if (value.substring(0, 1) === ':') { if (value.substring(0, 1) === ':') {
value = value.substring(1).trim() value = value.substring(1).trim();
return Number(value.trim()) return Number(value.trim());
} else { } else {
return Number(value.trim()) return Number(value.trim());
}
} }
};
const clear = function() { const clear = function() {
sections = {} sections = {};
title = '' title = '';
} };
// export const parseError = (err, hash) => { // export const parseError = (err, hash) => {
// global.mermaidAPI.parseError(err, hash) // global.mermaidAPI.parseError(err, hash)
// } // }
@ -47,4 +47,4 @@ export default {
setTitle, setTitle,
getTitle getTitle
// parseError // parseError
} };

View File

@ -1,83 +1,87 @@
/** /**
* Created by AshishJ on 11-09-2019. * Created by AshishJ on 11-09-2019.
*/ */
import * as d3 from 'd3' import * as d3 from 'd3';
import pieData from './pieDb' import pieData from './pieDb';
import pieParser from './parser/pie' import pieParser from './parser/pie';
import { logger } from '../../logger' import { logger } from '../../logger';
const conf = { const conf = {};
}
export const setConf = function(cnf) { export const setConf = function(cnf) {
const keys = Object.keys(cnf) const keys = Object.keys(cnf);
keys.forEach(function(key) { keys.forEach(function(key) {
conf[key] = cnf[key] conf[key] = cnf[key];
}) });
} };
/** /**
* Draws a Pie Chart with the data given in text. * Draws a Pie Chart with the data given in text.
* @param text * @param text
* @param id * @param id
*/ */
let w let w;
export const draw = (txt, id, ver) => { export const draw = (txt, id, ver) => {
try { try {
const parser = pieParser.parser const parser = pieParser.parser;
parser.yy = pieData parser.yy = pieData;
logger.debug('Rendering info diagram\n' + txt) logger.debug('Rendering info diagram\n' + txt);
// Parse the Pie Chart definition // Parse the Pie Chart definition
parser.yy.clear() parser.yy.clear();
parser.parse(txt) parser.parse(txt);
logger.debug('Parsed info diagram') logger.debug('Parsed info diagram');
const elem = document.getElementById(id) const elem = document.getElementById(id);
w = elem.parentElement.offsetWidth w = elem.parentElement.offsetWidth;
if (typeof w === 'undefined') { if (typeof w === 'undefined') {
w = 1200 w = 1200;
} }
if (typeof conf.useWidth !== 'undefined') { if (typeof conf.useWidth !== 'undefined') {
w = conf.useWidth w = conf.useWidth;
} }
const h = 450 const h = 450;
elem.setAttribute('height', '100%') elem.setAttribute('height', '100%');
// Set viewBox // Set viewBox
elem.setAttribute('viewBox', '0 0 ' + w + ' ' + h) elem.setAttribute('viewBox', '0 0 ' + w + ' ' + h);
// Fetch the default direction, use TD if none was found // Fetch the default direction, use TD if none was found
var width = w// 450 var width = w; // 450
var height = 450 var height = 450;
var margin = 40 var margin = 40;
var radius = Math.min(width, height) / 2 - margin var radius = Math.min(width, height) / 2 - margin;
var svg = d3.select('#' + id).append('svg') var svg = d3
.select('#' + id)
.append('svg')
.attr('width', width) .attr('width', width)
.attr('height', height) .attr('height', height)
.append('g') .append('g')
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')') .attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
var data = pieData.getSections() var data = pieData.getSections();
logger.info(data) logger.info(data);
// set the color scale // set the color scale
var color = d3.scaleOrdinal() var color = d3
.scaleOrdinal()
.domain(data) .domain(data)
.range(d3.schemeSet2) .range(d3.schemeSet2);
// Compute the position of each group on the pie: // Compute the position of each group on the pie:
var pie = d3.pie() var pie = d3.pie().value(function(d) {
.value(function (d) { return d.value }) return d.value;
var dataReady = pie(d3.entries(data)) });
var dataReady = pie(d3.entries(data));
// Now I know that group A goes from 0 degrees to x degrees and so on. // Now I know that group A goes from 0 degrees to x degrees and so on.
// shape helper to build arcs: // shape helper to build arcs:
var arcGenerator = d3.arc() var arcGenerator = d3
.arc()
.innerRadius(0) .innerRadius(0)
.outerRadius(radius) .outerRadius(radius);
// Build the pie chart: Basically, each part of the pie is a path that we build using the arc function. // Build the pie chart: Basically, each part of the pie is a path that we build using the arc function.
svg svg
@ -86,10 +90,12 @@ export const draw = (txt, id, ver) => {
.enter() .enter()
.append('path') .append('path')
.attr('d', arcGenerator) .attr('d', arcGenerator)
.attr('fill', function (d) { return (color(d.data.key)) }) .attr('fill', function(d) {
return color(d.data.key);
})
.attr('stroke', 'black') .attr('stroke', 'black')
.style('stroke-width', '2px') .style('stroke-width', '2px')
.style('opacity', 0.7) .style('opacity', 0.7);
// Now add the annotation. Use the centroid method to get the best coordinates // Now add the annotation. Use the centroid method to get the best coordinates
svg svg
@ -97,23 +103,28 @@ export const draw = (txt, id, ver) => {
.data(dataReady) .data(dataReady)
.enter() .enter()
.append('text') .append('text')
.text(function (d) { return d.data.key }) .text(function(d) {
.attr('transform', function (d) { return 'translate(' + arcGenerator.centroid(d) + ')' }) return d.data.key;
})
.attr('transform', function(d) {
return 'translate(' + arcGenerator.centroid(d) + ')';
})
.style('text-anchor', 'middle') .style('text-anchor', 'middle')
.style('font-size', 17) .style('font-size', 17);
svg.append('text') svg
.append('text')
.text(parser.yy.getTitle()) .text(parser.yy.getTitle())
.attr('x', 0) .attr('x', 0)
.attr('y', -(h - 50) / 2) .attr('y', -(h - 50) / 2)
.attr('class', 'pieTitleText') .attr('class', 'pieTitleText');
} catch (e) { } catch (e) {
logger.error('Error while rendering info diagram') logger.error('Error while rendering info diagram');
logger.error(e.message) logger.error(e.message);
}
} }
};
export default { export default {
setConf, setConf,
draw draw
} };

View File

@ -1,51 +1,53 @@
import { logger } from '../../logger' import { logger } from '../../logger';
let actors = {} let actors = {};
let messages = [] let messages = [];
const notes = [] const notes = [];
let title = '' let title = '';
export const addActor = function(id, name, description) { export const addActor = function(id, name, description) {
// Don't allow description nulling // Don't allow description nulling
const old = actors[id] const old = actors[id];
if (old && name === old.name && description == null) return if (old && name === old.name && description == null) return;
// Don't allow null descriptions, either // Don't allow null descriptions, either
if (description == null) description = name if (description == null) description = name;
actors[id] = { name: name, description: description } actors[id] = { name: name, description: description };
} };
export const addMessage = function(idFrom, idTo, message, answer) { export const addMessage = function(idFrom, idTo, message, answer) {
messages.push({ from: idFrom, to: idTo, message: message, answer: answer }) messages.push({ from: idFrom, to: idTo, message: message, answer: answer });
} };
export const addSignal = function(idFrom, idTo, message, messageType) { export const addSignal = function(idFrom, idTo, message, messageType) {
logger.debug('Adding message from=' + idFrom + ' to=' + idTo + ' message=' + message + ' type=' + messageType) logger.debug(
messages.push({ from: idFrom, to: idTo, message: message, type: messageType }) 'Adding message from=' + idFrom + ' to=' + idTo + ' message=' + message + ' type=' + messageType
} );
messages.push({ from: idFrom, to: idTo, message: message, type: messageType });
};
export const getMessages = function() { export const getMessages = function() {
return messages return messages;
} };
export const getActors = function() { export const getActors = function() {
return actors return actors;
} };
export const getActor = function(id) { export const getActor = function(id) {
return actors[id] return actors[id];
} };
export const getActorKeys = function() { export const getActorKeys = function() {
return Object.keys(actors) return Object.keys(actors);
} };
export const getTitle = function() { export const getTitle = function() {
return title return title;
} };
export const clear = function() { export const clear = function() {
actors = {} actors = {};
messages = [] messages = [];
} };
export const LINETYPE = { export const LINETYPE = {
SOLID: 0, SOLID: 0,
@ -69,97 +71,103 @@ export const LINETYPE = {
PAR_END: 21, PAR_END: 21,
RECT_START: 22, RECT_START: 22,
RECT_END: 23 RECT_END: 23
} };
export const ARROWTYPE = { export const ARROWTYPE = {
FILLED: 0, FILLED: 0,
OPEN: 1 OPEN: 1
} };
export const PLACEMENT = { export const PLACEMENT = {
LEFTOF: 0, LEFTOF: 0,
RIGHTOF: 1, RIGHTOF: 1,
OVER: 2 OVER: 2
} };
export const addNote = function(actor, placement, message) { export const addNote = function(actor, placement, message) {
const note = { actor: actor, placement: placement, message: message } const note = { actor: actor, placement: placement, message: message };
// Coerce actor into a [to, from, ...] array // Coerce actor into a [to, from, ...] array
const actors = [].concat(actor, actor) const actors = [].concat(actor, actor);
notes.push(note) notes.push(note);
messages.push({ from: actors[0], to: actors[1], message: message, type: LINETYPE.NOTE, placement: placement }) messages.push({
} from: actors[0],
to: actors[1],
message: message,
type: LINETYPE.NOTE,
placement: placement
});
};
export const setTitle = function(titleText) { export const setTitle = function(titleText) {
title = titleText title = titleText;
} };
export const apply = function(param) { export const apply = function(param) {
if (param instanceof Array) { if (param instanceof Array) {
param.forEach(function(item) { param.forEach(function(item) {
apply(item) apply(item);
}) });
} else { } else {
switch (param.type) { switch (param.type) {
case 'addActor': case 'addActor':
addActor(param.actor, param.actor, param.description) addActor(param.actor, param.actor, param.description);
break break;
case 'activeStart': case 'activeStart':
addSignal(param.actor, undefined, undefined, param.signalType) addSignal(param.actor, undefined, undefined, param.signalType);
break break;
case 'activeEnd': case 'activeEnd':
addSignal(param.actor, undefined, undefined, param.signalType) addSignal(param.actor, undefined, undefined, param.signalType);
break break;
case 'addNote': case 'addNote':
addNote(param.actor, param.placement, param.text) addNote(param.actor, param.placement, param.text);
break break;
case 'addMessage': case 'addMessage':
addSignal(param.from, param.to, param.msg, param.signalType) addSignal(param.from, param.to, param.msg, param.signalType);
break break;
case 'loopStart': case 'loopStart':
addSignal(undefined, undefined, param.loopText, param.signalType) addSignal(undefined, undefined, param.loopText, param.signalType);
break break;
case 'loopEnd': case 'loopEnd':
addSignal(undefined, undefined, undefined, param.signalType) addSignal(undefined, undefined, undefined, param.signalType);
break break;
case 'rectStart': case 'rectStart':
addSignal(undefined, undefined, param.color, param.signalType) addSignal(undefined, undefined, param.color, param.signalType);
break break;
case 'rectEnd': case 'rectEnd':
addSignal(undefined, undefined, undefined, param.signalType) addSignal(undefined, undefined, undefined, param.signalType);
break break;
case 'optStart': case 'optStart':
addSignal(undefined, undefined, param.optText, param.signalType) addSignal(undefined, undefined, param.optText, param.signalType);
break break;
case 'optEnd': case 'optEnd':
addSignal(undefined, undefined, undefined, param.signalType) addSignal(undefined, undefined, undefined, param.signalType);
break break;
case 'altStart': case 'altStart':
addSignal(undefined, undefined, param.altText, param.signalType) addSignal(undefined, undefined, param.altText, param.signalType);
break break;
case 'else': case 'else':
addSignal(undefined, undefined, param.altText, param.signalType) addSignal(undefined, undefined, param.altText, param.signalType);
break break;
case 'altEnd': case 'altEnd':
addSignal(undefined, undefined, undefined, param.signalType) addSignal(undefined, undefined, undefined, param.signalType);
break break;
case 'setTitle': case 'setTitle':
setTitle(param.text) setTitle(param.text);
break break;
case 'parStart': case 'parStart':
addSignal(undefined, undefined, param.parText, param.signalType) addSignal(undefined, undefined, param.parText, param.signalType);
break break;
case 'and': case 'and':
addSignal(undefined, undefined, param.parText, param.signalType) addSignal(undefined, undefined, param.parText, param.signalType);
break break;
case 'parEnd': case 'parEnd':
addSignal(undefined, undefined, undefined, param.signalType) addSignal(undefined, undefined, undefined, param.signalType);
break break;
}
} }
} }
};
export default { export default {
addActor, addActor,
@ -177,4 +185,4 @@ export default {
addNote, addNote,
setTitle, setTitle,
apply apply
} };

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,13 @@
import * as d3 from 'd3' import * as d3 from 'd3';
import svgDraw from './svgDraw' import svgDraw from './svgDraw';
import { logger } from '../../logger' import { logger } from '../../logger';
import { parser } from './parser/sequenceDiagram' import { parser } from './parser/sequenceDiagram';
import sequenceDb from './sequenceDb' import sequenceDb from './sequenceDb';
parser.yy = sequenceDb parser.yy = sequenceDb;
const conf = { const conf = {
diagramMarginX: 50, diagramMarginX: 50,
diagramMarginY: 30, diagramMarginY: 30,
// Margin between actors // Margin between actors
@ -38,7 +37,7 @@ const conf = {
textPlacement: 'tspan', textPlacement: 'tspan',
showSequenceNumbers: false showSequenceNumbers: false
} };
export const bounds = { export const bounds = {
data: { data: {
@ -52,68 +51,68 @@ export const bounds = {
sequenceItems: [], sequenceItems: [],
activations: [], activations: [],
init: function() { init: function() {
this.sequenceItems = [] this.sequenceItems = [];
this.activations = [] this.activations = [];
this.data = { this.data = {
startx: undefined, startx: undefined,
stopx: undefined, stopx: undefined,
starty: undefined, starty: undefined,
stopy: undefined stopy: undefined
} };
this.verticalPos = 0 this.verticalPos = 0;
}, },
updateVal: function(obj, key, val, fun) { updateVal: function(obj, key, val, fun) {
if (typeof obj[key] === 'undefined') { if (typeof obj[key] === 'undefined') {
obj[key] = val obj[key] = val;
} else { } else {
obj[key] = fun(val, obj[key]) obj[key] = fun(val, obj[key]);
} }
}, },
updateBounds: function(startx, starty, stopx, stopy) { updateBounds: function(startx, starty, stopx, stopy) {
const _self = this const _self = this;
let cnt = 0 let cnt = 0;
function updateFn(type) { function updateFn(type) {
return function updateItemBounds(item) { return function updateItemBounds(item) {
cnt++ cnt++;
// The loop sequenceItems is a stack so the biggest margins in the beginning of the sequenceItems // The loop sequenceItems is a stack so the biggest margins in the beginning of the sequenceItems
const n = _self.sequenceItems.length - cnt + 1 const n = _self.sequenceItems.length - cnt + 1;
_self.updateVal(item, 'starty', starty - n * conf.boxMargin, Math.min) _self.updateVal(item, 'starty', starty - n * conf.boxMargin, Math.min);
_self.updateVal(item, 'stopy', stopy + n * conf.boxMargin, Math.max) _self.updateVal(item, 'stopy', stopy + n * conf.boxMargin, Math.max);
_self.updateVal(bounds.data, 'startx', startx - n * conf.boxMargin, Math.min) _self.updateVal(bounds.data, 'startx', startx - n * conf.boxMargin, Math.min);
_self.updateVal(bounds.data, 'stopx', stopx + n * conf.boxMargin, Math.max) _self.updateVal(bounds.data, 'stopx', stopx + n * conf.boxMargin, Math.max);
if (!(type === 'activation')) { if (!(type === 'activation')) {
_self.updateVal(item, 'startx', startx - n * conf.boxMargin, Math.min) _self.updateVal(item, 'startx', startx - n * conf.boxMargin, Math.min);
_self.updateVal(item, 'stopx', stopx + n * conf.boxMargin, Math.max) _self.updateVal(item, 'stopx', stopx + n * conf.boxMargin, Math.max);
_self.updateVal(bounds.data, 'starty', starty - n * conf.boxMargin, Math.min) _self.updateVal(bounds.data, 'starty', starty - n * conf.boxMargin, Math.min);
_self.updateVal(bounds.data, 'stopy', stopy + n * conf.boxMargin, Math.max) _self.updateVal(bounds.data, 'stopy', stopy + n * conf.boxMargin, Math.max);
}
} }
};
} }
this.sequenceItems.forEach(updateFn()) this.sequenceItems.forEach(updateFn());
this.activations.forEach(updateFn('activation')) this.activations.forEach(updateFn('activation'));
}, },
insert: function(startx, starty, stopx, stopy) { insert: function(startx, starty, stopx, stopy) {
const _startx = Math.min(startx, stopx) const _startx = Math.min(startx, stopx);
const _stopx = Math.max(startx, stopx) const _stopx = Math.max(startx, stopx);
const _starty = Math.min(starty, stopy) const _starty = Math.min(starty, stopy);
const _stopy = Math.max(starty, stopy) const _stopy = Math.max(starty, stopy);
this.updateVal(bounds.data, 'startx', _startx, Math.min) this.updateVal(bounds.data, 'startx', _startx, Math.min);
this.updateVal(bounds.data, 'starty', _starty, Math.min) this.updateVal(bounds.data, 'starty', _starty, Math.min);
this.updateVal(bounds.data, 'stopx', _stopx, Math.max) this.updateVal(bounds.data, 'stopx', _stopx, Math.max);
this.updateVal(bounds.data, 'stopy', _stopy, Math.max) this.updateVal(bounds.data, 'stopy', _stopy, Math.max);
this.updateBounds(_startx, _starty, _stopx, _stopy) this.updateBounds(_startx, _starty, _stopx, _stopy);
}, },
newActivation: function(message, diagram) { newActivation: function(message, diagram) {
const actorRect = parser.yy.getActors()[message.from.actor] const actorRect = parser.yy.getActors()[message.from.actor];
const stackedSize = actorActivations(message.from.actor).length const stackedSize = actorActivations(message.from.actor).length;
const x = actorRect.x + conf.width / 2 + (stackedSize - 1) * conf.activationWidth / 2 const x = actorRect.x + conf.width / 2 + ((stackedSize - 1) * conf.activationWidth) / 2;
this.activations.push({ this.activations.push({
startx: x, startx: x,
starty: this.verticalPos + 2, starty: this.verticalPos + 2,
@ -121,59 +120,68 @@ export const bounds = {
stopy: undefined, stopy: undefined,
actor: message.from.actor, actor: message.from.actor,
anchored: svgDraw.anchorElement(diagram) anchored: svgDraw.anchorElement(diagram)
}) });
}, },
endActivation: function(message) { endActivation: function(message) {
// find most recent activation for given actor // find most recent activation for given actor
const lastActorActivationIdx = this.activations const lastActorActivationIdx = this.activations
.map(function (activation) { return activation.actor }) .map(function(activation) {
.lastIndexOf(message.from.actor) return activation.actor;
const activation = this.activations.splice(lastActorActivationIdx, 1)[0] })
return activation .lastIndexOf(message.from.actor);
const activation = this.activations.splice(lastActorActivationIdx, 1)[0];
return activation;
}, },
newLoop: function(title, fill) { newLoop: function(title, fill) {
this.sequenceItems.push({ startx: undefined, starty: this.verticalPos, stopx: undefined, stopy: undefined, title: title, fill: fill }) this.sequenceItems.push({
startx: undefined,
starty: this.verticalPos,
stopx: undefined,
stopy: undefined,
title: title,
fill: fill
});
}, },
endLoop: function() { endLoop: function() {
const loop = this.sequenceItems.pop() const loop = this.sequenceItems.pop();
return loop return loop;
}, },
addSectionToLoop: function(message) { addSectionToLoop: function(message) {
const loop = this.sequenceItems.pop() const loop = this.sequenceItems.pop();
loop.sections = loop.sections || [] loop.sections = loop.sections || [];
loop.sectionTitles = loop.sectionTitles || [] loop.sectionTitles = loop.sectionTitles || [];
loop.sections.push(bounds.getVerticalPos()) loop.sections.push(bounds.getVerticalPos());
loop.sectionTitles.push(message) loop.sectionTitles.push(message);
this.sequenceItems.push(loop) this.sequenceItems.push(loop);
}, },
bumpVerticalPos: function(bump) { bumpVerticalPos: function(bump) {
this.verticalPos = this.verticalPos + bump this.verticalPos = this.verticalPos + bump;
this.data.stopy = this.verticalPos this.data.stopy = this.verticalPos;
}, },
getVerticalPos: function() { getVerticalPos: function() {
return this.verticalPos return this.verticalPos;
}, },
getBounds: function() { getBounds: function() {
return this.data return this.data;
}
} }
};
const _drawLongText = (text, x, y, g, width) => { const _drawLongText = (text, x, y, g, width) => {
let textHeight = 0 let textHeight = 0;
const lines = text.split(/<br\/?>/ig) const lines = text.split(/<br\/?>/gi);
for (const line of lines) { for (const line of lines) {
const textObj = svgDraw.getTextObj() const textObj = svgDraw.getTextObj();
textObj.x = x textObj.x = x;
textObj.y = y + textHeight textObj.y = y + textHeight;
textObj.textMargin = conf.noteMargin textObj.textMargin = conf.noteMargin;
textObj.dy = '1em' textObj.dy = '1em';
textObj.text = line textObj.text = line;
textObj.class = 'noteText' textObj.class = 'noteText';
const textElem = svgDraw.drawText(g, textObj, width) const textElem = svgDraw.drawText(g, textObj, width);
textHeight += (textElem._groups || textElem)[0][0].getBBox().height textHeight += (textElem._groups || textElem)[0][0].getBBox().height;
}
return textHeight
} }
return textHeight;
};
/** /**
* Draws an actor in the diagram with the attaced line * Draws an actor in the diagram with the attaced line
@ -182,21 +190,32 @@ const _drawLongText = (text, x, y, g, width) => {
* @param description The text in the box * @param description The text in the box
*/ */
const drawNote = function(elem, startx, verticalPos, msg, forceWidth) { const drawNote = function(elem, startx, verticalPos, msg, forceWidth) {
const rect = svgDraw.getNoteRect() const rect = svgDraw.getNoteRect();
rect.x = startx rect.x = startx;
rect.y = verticalPos rect.y = verticalPos;
rect.width = forceWidth || conf.width rect.width = forceWidth || conf.width;
rect.class = 'note' rect.class = 'note';
let g = elem.append('g') let g = elem.append('g');
const rectElem = svgDraw.drawRect(g, rect) const rectElem = svgDraw.drawRect(g, rect);
const textHeight = _drawLongText(msg.message, startx - 4, verticalPos + 24, g, rect.width - conf.noteMargin) const textHeight = _drawLongText(
msg.message,
startx - 4,
verticalPos + 24,
g,
rect.width - conf.noteMargin
);
bounds.insert(startx, verticalPos, startx + rect.width, verticalPos + 2 * conf.noteMargin + textHeight) bounds.insert(
rectElem.attr('height', textHeight + 2 * conf.noteMargin) startx,
bounds.bumpVerticalPos(textHeight + 2 * conf.noteMargin) verticalPos,
} startx + rect.width,
verticalPos + 2 * conf.noteMargin + textHeight
);
rectElem.attr('height', textHeight + 2 * conf.noteMargin);
bounds.bumpVerticalPos(textHeight + 2 * conf.noteMargin);
};
/** /**
* Draws a message * Draws a message
@ -208,69 +227,103 @@ const drawNote = function (elem, startx, verticalPos, msg, forceWidth) {
* @param msg * @param msg
*/ */
const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceIndex) { const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceIndex) {
const g = elem.append('g') const g = elem.append('g');
const txtCenter = startx + (stopx - startx) / 2 const txtCenter = startx + (stopx - startx) / 2;
const textElem = g.append('text') // text label for the x axis const textElem = g
.append('text') // text label for the x axis
.attr('x', txtCenter) .attr('x', txtCenter)
.attr('y', verticalPos - 7) .attr('y', verticalPos - 7)
.style('text-anchor', 'middle') .style('text-anchor', 'middle')
.attr('class', 'messageText') .attr('class', 'messageText')
.text(msg.message) .text(msg.message);
let textWidth = (textElem._groups || textElem)[0][0].getBBox().width let textWidth = (textElem._groups || textElem)[0][0].getBBox().width;
let line let line;
if (startx === stopx) { if (startx === stopx) {
if (conf.rightAngles) { if (conf.rightAngles) {
line = g.append('path').attr('d', `M ${startx},${verticalPos} H ${startx + (conf.width / 2)} V ${verticalPos + 25} H ${startx}`) line = g
.append('path')
.attr(
'd',
`M ${startx},${verticalPos} H ${startx + conf.width / 2} V ${verticalPos +
25} H ${startx}`
);
} else { } else {
line = g.append('path') line = g
.attr('d', 'M ' + startx + ',' + verticalPos + ' C ' + (startx + 60) + ',' + (verticalPos - 10) + ' ' + (startx + 60) + ',' + .append('path')
(verticalPos + 30) + ' ' + startx + ',' + (verticalPos + 20)) .attr(
'd',
'M ' +
startx +
',' +
verticalPos +
' C ' +
(startx + 60) +
',' +
(verticalPos - 10) +
' ' +
(startx + 60) +
',' +
(verticalPos + 30) +
' ' +
startx +
',' +
(verticalPos + 20)
);
} }
bounds.bumpVerticalPos(30) bounds.bumpVerticalPos(30);
const dx = Math.max(textWidth / 2, 100) const dx = Math.max(textWidth / 2, 100);
bounds.insert(startx - dx, bounds.getVerticalPos() - 10, stopx + dx, bounds.getVerticalPos()) bounds.insert(startx - dx, bounds.getVerticalPos() - 10, stopx + dx, bounds.getVerticalPos());
} else { } else {
line = g.append('line') line = g.append('line');
line.attr('x1', startx) line.attr('x1', startx);
line.attr('y1', verticalPos) line.attr('y1', verticalPos);
line.attr('x2', stopx) line.attr('x2', stopx);
line.attr('y2', verticalPos) line.attr('y2', verticalPos);
bounds.insert(startx, bounds.getVerticalPos() - 10, stopx, bounds.getVerticalPos()) bounds.insert(startx, bounds.getVerticalPos() - 10, stopx, bounds.getVerticalPos());
} }
// Make an SVG Container // Make an SVG Container
// Draw the line // Draw the line
if (msg.type === parser.yy.LINETYPE.DOTTED || msg.type === parser.yy.LINETYPE.DOTTED_CROSS || msg.type === parser.yy.LINETYPE.DOTTED_OPEN) { if (
line.style('stroke-dasharray', ('3, 3')) msg.type === parser.yy.LINETYPE.DOTTED ||
line.attr('class', 'messageLine1') msg.type === parser.yy.LINETYPE.DOTTED_CROSS ||
msg.type === parser.yy.LINETYPE.DOTTED_OPEN
) {
line.style('stroke-dasharray', '3, 3');
line.attr('class', 'messageLine1');
} else { } else {
line.attr('class', 'messageLine0') line.attr('class', 'messageLine0');
} }
let url = '' let url = '';
if (conf.arrowMarkerAbsolute) { if (conf.arrowMarkerAbsolute) {
url = window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search url =
url = url.replace(/\(/g, '\\(') window.location.protocol +
url = url.replace(/\)/g, '\\)') '//' +
window.location.host +
window.location.pathname +
window.location.search;
url = url.replace(/\(/g, '\\(');
url = url.replace(/\)/g, '\\)');
} }
line.attr('stroke-width', 2) line.attr('stroke-width', 2);
line.attr('stroke', 'black') line.attr('stroke', 'black');
line.style('fill', 'none') // remove any fill colour line.style('fill', 'none'); // remove any fill colour
if (msg.type === parser.yy.LINETYPE.SOLID || msg.type === parser.yy.LINETYPE.DOTTED) { if (msg.type === parser.yy.LINETYPE.SOLID || msg.type === parser.yy.LINETYPE.DOTTED) {
line.attr('marker-end', 'url(' + url + '#arrowhead)') line.attr('marker-end', 'url(' + url + '#arrowhead)');
} }
if (msg.type === parser.yy.LINETYPE.SOLID_CROSS || msg.type === parser.yy.LINETYPE.DOTTED_CROSS) { if (msg.type === parser.yy.LINETYPE.SOLID_CROSS || msg.type === parser.yy.LINETYPE.DOTTED_CROSS) {
line.attr('marker-end', 'url(' + url + '#crosshead)') line.attr('marker-end', 'url(' + url + '#crosshead)');
} }
// add node number // add node number
if (conf.showSequenceNumbers) { if (conf.showSequenceNumbers) {
line.attr('marker-start', 'url(' + url + '#sequencenumber)') line.attr('marker-start', 'url(' + url + '#sequencenumber)');
g.append('text') g.append('text')
.attr('x', startx) .attr('x', startx)
.attr('y', verticalPos + 4) .attr('y', verticalPos + 4)
@ -279,53 +332,57 @@ const drawMessage = function (elem, startx, stopx, verticalPos, msg, sequenceInd
.attr('text-anchor', 'middle') .attr('text-anchor', 'middle')
.attr('textLength', '16px') .attr('textLength', '16px')
.attr('class', 'sequenceNumber') .attr('class', 'sequenceNumber')
.text(sequenceIndex) .text(sequenceIndex);
}
} }
};
export const drawActors = function(diagram, actors, actorKeys, verticalPos) { export const drawActors = function(diagram, actors, actorKeys, verticalPos) {
// Draw the actors // Draw the actors
for (let i = 0; i < actorKeys.length; i++) { for (let i = 0; i < actorKeys.length; i++) {
const key = actorKeys[i] const key = actorKeys[i];
// Add some rendering data to the object // Add some rendering data to the object
actors[key].x = i * conf.actorMargin + i * conf.width actors[key].x = i * conf.actorMargin + i * conf.width;
actors[key].y = verticalPos actors[key].y = verticalPos;
actors[key].width = conf.diagramMarginX actors[key].width = conf.diagramMarginX;
actors[key].height = conf.diagramMarginY actors[key].height = conf.diagramMarginY;
// Draw the box with the attached line // Draw the box with the attached line
svgDraw.drawActor(diagram, actors[key].x, verticalPos, actors[key].description, conf) svgDraw.drawActor(diagram, actors[key].x, verticalPos, actors[key].description, conf);
bounds.insert(actors[key].x, verticalPos, actors[key].x + conf.width, conf.height) bounds.insert(actors[key].x, verticalPos, actors[key].x + conf.width, conf.height);
} }
// Add a margin between the actor boxes and the first arrow // Add a margin between the actor boxes and the first arrow
bounds.bumpVerticalPos(conf.height) bounds.bumpVerticalPos(conf.height);
} };
export const setConf = function(cnf) { export const setConf = function(cnf) {
const keys = Object.keys(cnf) const keys = Object.keys(cnf);
keys.forEach(function(key) { keys.forEach(function(key) {
conf[key] = cnf[key] conf[key] = cnf[key];
}) });
} };
const actorActivations = function(actor) { const actorActivations = function(actor) {
return bounds.activations.filter(function(activation) { return bounds.activations.filter(function(activation) {
return activation.actor === actor return activation.actor === actor;
}) });
} };
const actorFlowVerticaBounds = function(actor) { const actorFlowVerticaBounds = function(actor) {
// handle multiple stacked activations for same actor // handle multiple stacked activations for same actor
const actors = parser.yy.getActors() const actors = parser.yy.getActors();
const activations = actorActivations(actor) const activations = actorActivations(actor);
const left = activations.reduce(function (acc, activation) { return Math.min(acc, activation.startx) }, actors[actor].x + conf.width / 2) const left = activations.reduce(function(acc, activation) {
const right = activations.reduce(function (acc, activation) { return Math.max(acc, activation.stopx) }, actors[actor].x + conf.width / 2) return Math.min(acc, activation.startx);
return [left, right] }, actors[actor].x + conf.width / 2);
} const right = activations.reduce(function(acc, activation) {
return Math.max(acc, activation.stopx);
}, actors[actor].x + conf.width / 2);
return [left, right];
};
/** /**
* Draws a flowchart in the tag with id: id based on the graph definition in text. * Draws a flowchart in the tag with id: id based on the graph definition in text.
@ -333,209 +390,248 @@ const actorFlowVerticaBounds = function (actor) {
* @param id * @param id
*/ */
export const draw = function(text, id) { export const draw = function(text, id) {
parser.yy.clear() parser.yy.clear();
parser.parse(text + '\n') parser.parse(text + '\n');
bounds.init() bounds.init();
const diagram = d3.select(`[id="${id}"]`) const diagram = d3.select(`[id="${id}"]`);
let startx let startx;
let stopx let stopx;
let forceWidth let forceWidth;
// Fetch data from the parsing // Fetch data from the parsing
const actors = parser.yy.getActors() const actors = parser.yy.getActors();
const actorKeys = parser.yy.getActorKeys() const actorKeys = parser.yy.getActorKeys();
const messages = parser.yy.getMessages() const messages = parser.yy.getMessages();
const title = parser.yy.getTitle() const title = parser.yy.getTitle();
drawActors(diagram, actors, actorKeys, 0) drawActors(diagram, actors, actorKeys, 0);
// The arrow head definition is attached to the svg once // The arrow head definition is attached to the svg once
svgDraw.insertArrowHead(diagram) svgDraw.insertArrowHead(diagram);
svgDraw.insertArrowCrossHead(diagram) svgDraw.insertArrowCrossHead(diagram);
svgDraw.insertSequenceNumber(diagram) svgDraw.insertSequenceNumber(diagram);
function activeEnd(msg, verticalPos) { function activeEnd(msg, verticalPos) {
const activationData = bounds.endActivation(msg) const activationData = bounds.endActivation(msg);
if (activationData.starty + 18 > verticalPos) { if (activationData.starty + 18 > verticalPos) {
activationData.starty = verticalPos - 6 activationData.starty = verticalPos - 6;
verticalPos += 12 verticalPos += 12;
} }
svgDraw.drawActivation(diagram, activationData, verticalPos, conf, actorActivations(msg.from.actor).length) svgDraw.drawActivation(
diagram,
activationData,
verticalPos,
conf,
actorActivations(msg.from.actor).length
);
bounds.insert(activationData.startx, verticalPos - 10, activationData.stopx, verticalPos) bounds.insert(activationData.startx, verticalPos - 10, activationData.stopx, verticalPos);
} }
// const lastMsg // const lastMsg
// Draw the messages/signals // Draw the messages/signals
let sequenceIndex = 1 let sequenceIndex = 1;
messages.forEach(function(msg) { messages.forEach(function(msg) {
let loopData let loopData;
switch (msg.type) { switch (msg.type) {
case parser.yy.LINETYPE.NOTE: case parser.yy.LINETYPE.NOTE:
bounds.bumpVerticalPos(conf.boxMargin) bounds.bumpVerticalPos(conf.boxMargin);
startx = actors[msg.from].x startx = actors[msg.from].x;
stopx = actors[msg.to].x stopx = actors[msg.to].x;
if (msg.placement === parser.yy.PLACEMENT.RIGHTOF) { if (msg.placement === parser.yy.PLACEMENT.RIGHTOF) {
drawNote(diagram, startx + (conf.width + conf.actorMargin) / 2, bounds.getVerticalPos(), msg) drawNote(
diagram,
startx + (conf.width + conf.actorMargin) / 2,
bounds.getVerticalPos(),
msg
);
} else if (msg.placement === parser.yy.PLACEMENT.LEFTOF) { } else if (msg.placement === parser.yy.PLACEMENT.LEFTOF) {
drawNote(diagram, startx - (conf.width + conf.actorMargin) / 2, bounds.getVerticalPos(), msg) drawNote(
diagram,
startx - (conf.width + conf.actorMargin) / 2,
bounds.getVerticalPos(),
msg
);
} else if (msg.to === msg.from) { } else if (msg.to === msg.from) {
// Single-actor over // Single-actor over
drawNote(diagram, startx, bounds.getVerticalPos(), msg) drawNote(diagram, startx, bounds.getVerticalPos(), msg);
} else { } else {
// Multi-actor over // Multi-actor over
forceWidth = Math.abs(startx - stopx) + conf.actorMargin forceWidth = Math.abs(startx - stopx) + conf.actorMargin;
drawNote(diagram, (startx + stopx + conf.width - forceWidth) / 2, bounds.getVerticalPos(), msg, drawNote(
forceWidth) diagram,
(startx + stopx + conf.width - forceWidth) / 2,
bounds.getVerticalPos(),
msg,
forceWidth
);
} }
break break;
case parser.yy.LINETYPE.ACTIVE_START: case parser.yy.LINETYPE.ACTIVE_START:
bounds.newActivation(msg, diagram) bounds.newActivation(msg, diagram);
break break;
case parser.yy.LINETYPE.ACTIVE_END: case parser.yy.LINETYPE.ACTIVE_END:
activeEnd(msg, bounds.getVerticalPos()) activeEnd(msg, bounds.getVerticalPos());
break break;
case parser.yy.LINETYPE.LOOP_START: case parser.yy.LINETYPE.LOOP_START:
bounds.bumpVerticalPos(conf.boxMargin) bounds.bumpVerticalPos(conf.boxMargin);
bounds.newLoop(msg.message) bounds.newLoop(msg.message);
bounds.bumpVerticalPos(conf.boxMargin + conf.boxTextMargin) bounds.bumpVerticalPos(conf.boxMargin + conf.boxTextMargin);
break break;
case parser.yy.LINETYPE.LOOP_END: case parser.yy.LINETYPE.LOOP_END:
loopData = bounds.endLoop() loopData = bounds.endLoop();
svgDraw.drawLoop(diagram, loopData, 'loop', conf) svgDraw.drawLoop(diagram, loopData, 'loop', conf);
bounds.bumpVerticalPos(conf.boxMargin) bounds.bumpVerticalPos(conf.boxMargin);
break break;
case parser.yy.LINETYPE.RECT_START: case parser.yy.LINETYPE.RECT_START:
bounds.bumpVerticalPos(conf.boxMargin) bounds.bumpVerticalPos(conf.boxMargin);
bounds.newLoop(undefined, msg.message) bounds.newLoop(undefined, msg.message);
bounds.bumpVerticalPos(conf.boxMargin) bounds.bumpVerticalPos(conf.boxMargin);
break break;
case parser.yy.LINETYPE.RECT_END: case parser.yy.LINETYPE.RECT_END:
const rectData = bounds.endLoop() const rectData = bounds.endLoop();
svgDraw.drawBackgroundRect(diagram, rectData) svgDraw.drawBackgroundRect(diagram, rectData);
bounds.bumpVerticalPos(conf.boxMargin) bounds.bumpVerticalPos(conf.boxMargin);
break break;
case parser.yy.LINETYPE.OPT_START: case parser.yy.LINETYPE.OPT_START:
bounds.bumpVerticalPos(conf.boxMargin) bounds.bumpVerticalPos(conf.boxMargin);
bounds.newLoop(msg.message) bounds.newLoop(msg.message);
bounds.bumpVerticalPos(conf.boxMargin + conf.boxTextMargin) bounds.bumpVerticalPos(conf.boxMargin + conf.boxTextMargin);
break break;
case parser.yy.LINETYPE.OPT_END: case parser.yy.LINETYPE.OPT_END:
loopData = bounds.endLoop() loopData = bounds.endLoop();
svgDraw.drawLoop(diagram, loopData, 'opt', conf) svgDraw.drawLoop(diagram, loopData, 'opt', conf);
bounds.bumpVerticalPos(conf.boxMargin) bounds.bumpVerticalPos(conf.boxMargin);
break break;
case parser.yy.LINETYPE.ALT_START: case parser.yy.LINETYPE.ALT_START:
bounds.bumpVerticalPos(conf.boxMargin) bounds.bumpVerticalPos(conf.boxMargin);
bounds.newLoop(msg.message) bounds.newLoop(msg.message);
bounds.bumpVerticalPos(conf.boxMargin + conf.boxTextMargin) bounds.bumpVerticalPos(conf.boxMargin + conf.boxTextMargin);
break break;
case parser.yy.LINETYPE.ALT_ELSE: case parser.yy.LINETYPE.ALT_ELSE:
bounds.bumpVerticalPos(conf.boxMargin) bounds.bumpVerticalPos(conf.boxMargin);
loopData = bounds.addSectionToLoop(msg.message) loopData = bounds.addSectionToLoop(msg.message);
bounds.bumpVerticalPos(conf.boxMargin) bounds.bumpVerticalPos(conf.boxMargin);
break break;
case parser.yy.LINETYPE.ALT_END: case parser.yy.LINETYPE.ALT_END:
loopData = bounds.endLoop() loopData = bounds.endLoop();
svgDraw.drawLoop(diagram, loopData, 'alt', conf) svgDraw.drawLoop(diagram, loopData, 'alt', conf);
bounds.bumpVerticalPos(conf.boxMargin) bounds.bumpVerticalPos(conf.boxMargin);
break break;
case parser.yy.LINETYPE.PAR_START: case parser.yy.LINETYPE.PAR_START:
bounds.bumpVerticalPos(conf.boxMargin) bounds.bumpVerticalPos(conf.boxMargin);
bounds.newLoop(msg.message) bounds.newLoop(msg.message);
bounds.bumpVerticalPos(conf.boxMargin + conf.boxTextMargin) bounds.bumpVerticalPos(conf.boxMargin + conf.boxTextMargin);
break break;
case parser.yy.LINETYPE.PAR_AND: case parser.yy.LINETYPE.PAR_AND:
bounds.bumpVerticalPos(conf.boxMargin) bounds.bumpVerticalPos(conf.boxMargin);
loopData = bounds.addSectionToLoop(msg.message) loopData = bounds.addSectionToLoop(msg.message);
bounds.bumpVerticalPos(conf.boxMargin) bounds.bumpVerticalPos(conf.boxMargin);
break break;
case parser.yy.LINETYPE.PAR_END: case parser.yy.LINETYPE.PAR_END:
loopData = bounds.endLoop() loopData = bounds.endLoop();
svgDraw.drawLoop(diagram, loopData, 'par', conf) svgDraw.drawLoop(diagram, loopData, 'par', conf);
bounds.bumpVerticalPos(conf.boxMargin) bounds.bumpVerticalPos(conf.boxMargin);
break break;
default: default:
try { try {
// lastMsg = msg // lastMsg = msg
bounds.bumpVerticalPos(conf.messageMargin) bounds.bumpVerticalPos(conf.messageMargin);
const fromBounds = actorFlowVerticaBounds(msg.from) const fromBounds = actorFlowVerticaBounds(msg.from);
const toBounds = actorFlowVerticaBounds(msg.to) const toBounds = actorFlowVerticaBounds(msg.to);
const fromIdx = fromBounds[0] <= toBounds[0] ? 1 : 0 const fromIdx = fromBounds[0] <= toBounds[0] ? 1 : 0;
const toIdx = fromBounds[0] < toBounds[0] ? 0 : 1 const toIdx = fromBounds[0] < toBounds[0] ? 0 : 1;
startx = fromBounds[fromIdx] startx = fromBounds[fromIdx];
stopx = toBounds[toIdx] stopx = toBounds[toIdx];
const verticalPos = bounds.getVerticalPos() const verticalPos = bounds.getVerticalPos();
drawMessage(diagram, startx, stopx, verticalPos, msg, sequenceIndex) drawMessage(diagram, startx, stopx, verticalPos, msg, sequenceIndex);
const allBounds = fromBounds.concat(toBounds) const allBounds = fromBounds.concat(toBounds);
bounds.insert(Math.min.apply(null, allBounds), verticalPos, Math.max.apply(null, allBounds), verticalPos) bounds.insert(
Math.min.apply(null, allBounds),
verticalPos,
Math.max.apply(null, allBounds),
verticalPos
);
} catch (e) { } catch (e) {
logger.error('error while drawing message', e) logger.error('error while drawing message', e);
} }
} }
// Increment sequence counter if msg.type is a line (and not another event like activation or note, etc) // Increment sequence counter if msg.type is a line (and not another event like activation or note, etc)
if ([ if (
[
parser.yy.LINETYPE.SOLID_OPEN, parser.yy.LINETYPE.SOLID_OPEN,
parser.yy.LINETYPE.DOTTED_OPEN, parser.yy.LINETYPE.DOTTED_OPEN,
parser.yy.LINETYPE.SOLID, parser.yy.LINETYPE.SOLID,
parser.yy.LINETYPE.DOTTED, parser.yy.LINETYPE.DOTTED,
parser.yy.LINETYPE.SOLID_CROSS, parser.yy.LINETYPE.SOLID_CROSS,
parser.yy.LINETYPE.DOTTED_CROSS parser.yy.LINETYPE.DOTTED_CROSS
].includes(msg.type)) { ].includes(msg.type)
sequenceIndex++ ) {
sequenceIndex++;
} }
}) });
if (conf.mirrorActors) { if (conf.mirrorActors) {
// Draw actors below diagram // Draw actors below diagram
bounds.bumpVerticalPos(conf.boxMargin * 2) bounds.bumpVerticalPos(conf.boxMargin * 2);
drawActors(diagram, actors, actorKeys, bounds.getVerticalPos()) drawActors(diagram, actors, actorKeys, bounds.getVerticalPos());
} }
const box = bounds.getBounds() const box = bounds.getBounds();
// Adjust line height of actor lines now that the height of the diagram is known // Adjust line height of actor lines now that the height of the diagram is known
logger.debug('For line height fix Querying: #' + id + ' .actor-line') logger.debug('For line height fix Querying: #' + id + ' .actor-line');
const actorLines = d3.selectAll('#' + id + ' .actor-line') const actorLines = d3.selectAll('#' + id + ' .actor-line');
actorLines.attr('y2', box.stopy) actorLines.attr('y2', box.stopy);
let height = box.stopy - box.starty + 2 * conf.diagramMarginY let height = box.stopy - box.starty + 2 * conf.diagramMarginY;
if (conf.mirrorActors) { if (conf.mirrorActors) {
height = height - conf.boxMargin + conf.bottomMarginAdj height = height - conf.boxMargin + conf.bottomMarginAdj;
} }
const width = (box.stopx - box.startx) + (2 * conf.diagramMarginX) const width = box.stopx - box.startx + 2 * conf.diagramMarginX;
if (title) { if (title) {
diagram.append('text') diagram
.append('text')
.text(title) .text(title)
.attr('x', ((box.stopx - box.startx) / 2) - (2 * conf.diagramMarginX)) .attr('x', (box.stopx - box.startx) / 2 - 2 * conf.diagramMarginX)
.attr('y', -25) .attr('y', -25);
} }
if (conf.useMaxWidth) { if (conf.useMaxWidth) {
diagram.attr('height', '100%') diagram.attr('height', '100%');
diagram.attr('width', '100%') diagram.attr('width', '100%');
diagram.attr('style', 'max-width:' + (width) + 'px;') diagram.attr('style', 'max-width:' + width + 'px;');
} else { } else {
diagram.attr('height', height) diagram.attr('height', height);
diagram.attr('width', width) diagram.attr('width', width);
}
const extraVertForTitle = title ? 40 : 0
diagram.attr('viewBox', (box.startx - conf.diagramMarginX) + ' -' + (conf.diagramMarginY + extraVertForTitle) + ' ' + width + ' ' + (height + extraVertForTitle))
} }
const extraVertForTitle = title ? 40 : 0;
diagram.attr(
'viewBox',
box.startx -
conf.diagramMarginX +
' -' +
(conf.diagramMarginY + extraVertForTitle) +
' ' +
width +
' ' +
(height + extraVertForTitle)
);
};
export default { export default {
bounds, bounds,
drawActors, drawActors,
setConf, setConf,
draw draw
} };

View File

@ -1,60 +1,76 @@
export const drawRect = function(elem, rectData) { export const drawRect = function(elem, rectData) {
const rectElem = elem.append('rect') const rectElem = elem.append('rect');
rectElem.attr('x', rectData.x) rectElem.attr('x', rectData.x);
rectElem.attr('y', rectData.y) rectElem.attr('y', rectData.y);
rectElem.attr('fill', rectData.fill) rectElem.attr('fill', rectData.fill);
rectElem.attr('stroke', rectData.stroke) rectElem.attr('stroke', rectData.stroke);
rectElem.attr('width', rectData.width) rectElem.attr('width', rectData.width);
rectElem.attr('height', rectData.height) rectElem.attr('height', rectData.height);
rectElem.attr('rx', rectData.rx) rectElem.attr('rx', rectData.rx);
rectElem.attr('ry', rectData.ry) rectElem.attr('ry', rectData.ry);
if (typeof rectData.class !== 'undefined') { if (typeof rectData.class !== 'undefined') {
rectElem.attr('class', rectData.class) rectElem.attr('class', rectData.class);
} }
return rectElem return rectElem;
} };
export const drawText = function(elem, textData, width) { export const drawText = function(elem, textData, width) {
// Remove and ignore br:s // Remove and ignore br:s
const nText = textData.text.replace(/<br\/?>/ig, ' ') const nText = textData.text.replace(/<br\/?>/gi, ' ');
const textElem = elem.append('text') const textElem = elem.append('text');
textElem.attr('x', textData.x) textElem.attr('x', textData.x);
textElem.attr('y', textData.y) textElem.attr('y', textData.y);
textElem.style('text-anchor', textData.anchor) textElem.style('text-anchor', textData.anchor);
textElem.attr('fill', textData.fill) textElem.attr('fill', textData.fill);
if (typeof textData.class !== 'undefined') { if (typeof textData.class !== 'undefined') {
textElem.attr('class', textData.class) textElem.attr('class', textData.class);
} }
const span = textElem.append('tspan') const span = textElem.append('tspan');
span.attr('x', textData.x + textData.textMargin * 2) span.attr('x', textData.x + textData.textMargin * 2);
span.attr('fill', textData.fill) span.attr('fill', textData.fill);
span.text(nText) span.text(nText);
return textElem return textElem;
} };
export const drawLabel = function(elem, txtObject) { export const drawLabel = function(elem, txtObject) {
function genPoints(x, y, width, height, cut) { function genPoints(x, y, width, height, cut) {
return x + ',' + y + ' ' + return (
(x + width) + ',' + y + ' ' + x +
(x + width) + ',' + (y + height - cut) + ' ' + ',' +
(x + width - cut * 1.2) + ',' + (y + height) + ' ' + y +
(x) + ',' + (y + height) ' ' +
(x + width) +
',' +
y +
' ' +
(x + width) +
',' +
(y + height - cut) +
' ' +
(x + width - cut * 1.2) +
',' +
(y + height) +
' ' +
x +
',' +
(y + height)
);
} }
const polygon = elem.append('polygon') const polygon = elem.append('polygon');
polygon.attr('points', genPoints(txtObject.x, txtObject.y, 50, 20, 7)) polygon.attr('points', genPoints(txtObject.x, txtObject.y, 50, 20, 7));
polygon.attr('class', 'labelBox') polygon.attr('class', 'labelBox');
txtObject.y = txtObject.y + txtObject.labelMargin txtObject.y = txtObject.y + txtObject.labelMargin;
txtObject.x = txtObject.x + 0.5 * txtObject.labelMargin txtObject.x = txtObject.x + 0.5 * txtObject.labelMargin;
drawText(elem, txtObject) drawText(elem, txtObject);
} };
let actorCnt = -1 let actorCnt = -1;
/** /**
* Draws an actor in the diagram with the attaced line * Draws an actor in the diagram with the attaced line
* @param center - The center of the the actor * @param center - The center of the the actor
@ -62,10 +78,10 @@ let actorCnt = -1
* @param description The text in the box * @param description The text in the box
*/ */
export const drawActor = function(elem, left, verticalPos, description, conf) { export const drawActor = function(elem, left, verticalPos, description, conf) {
const center = left + (conf.width / 2) const center = left + conf.width / 2;
const g = elem.append('g') const g = elem.append('g');
if (verticalPos === 0) { if (verticalPos === 0) {
actorCnt++ actorCnt++;
g.append('line') g.append('line')
.attr('id', 'actor' + actorCnt) .attr('id', 'actor' + actorCnt)
.attr('x1', center) .attr('x1', center)
@ -74,27 +90,35 @@ export const drawActor = function (elem, left, verticalPos, description, conf) {
.attr('y2', 2000) .attr('y2', 2000)
.attr('class', 'actor-line') .attr('class', 'actor-line')
.attr('stroke-width', '0.5px') .attr('stroke-width', '0.5px')
.attr('stroke', '#999') .attr('stroke', '#999');
} }
const rect = getNoteRect() const rect = getNoteRect();
rect.x = left rect.x = left;
rect.y = verticalPos rect.y = verticalPos;
rect.fill = '#eaeaea' rect.fill = '#eaeaea';
rect.width = conf.width rect.width = conf.width;
rect.height = conf.height rect.height = conf.height;
rect.class = 'actor' rect.class = 'actor';
rect.rx = 3 rect.rx = 3;
rect.ry = 3 rect.ry = 3;
drawRect(g, rect) drawRect(g, rect);
_drawTextCandidateFunc(conf)(description, g, _drawTextCandidateFunc(conf)(
rect.x, rect.y, rect.width, rect.height, { 'class': 'actor' }, conf) description,
} g,
rect.x,
rect.y,
rect.width,
rect.height,
{ class: 'actor' },
conf
);
};
export const anchorElement = function(elem) { export const anchorElement = function(elem) {
return elem.append('g') return elem.append('g');
} };
/** /**
* Draws an actor in the diagram with the attaced line * Draws an actor in the diagram with the attaced line
* @param elem - element to append activation rect * @param elem - element to append activation rect
@ -102,15 +126,15 @@ export const anchorElement = function (elem) {
* @param verticalPos - precise y cooridnate of bottom activation box edge * @param verticalPos - precise y cooridnate of bottom activation box edge
*/ */
export const drawActivation = function(elem, bounds, verticalPos, conf, actorActivations) { export const drawActivation = function(elem, bounds, verticalPos, conf, actorActivations) {
const rect = getNoteRect() const rect = getNoteRect();
const g = bounds.anchored const g = bounds.anchored;
rect.x = bounds.startx rect.x = bounds.startx;
rect.y = bounds.starty rect.y = bounds.starty;
rect.class = 'activation' + (actorActivations % 3) // Will evaluate to 0, 1 or 2 rect.class = 'activation' + (actorActivations % 3); // Will evaluate to 0, 1 or 2
rect.width = bounds.stopx - bounds.startx rect.width = bounds.stopx - bounds.startx;
rect.height = verticalPos - bounds.starty rect.height = verticalPos - bounds.starty;
drawRect(g, rect) drawRect(g, rect);
} };
/** /**
* Draws an actor in the diagram with the attaced line * Draws an actor in the diagram with the attaced line
@ -119,53 +143,54 @@ export const drawActivation = function (elem, bounds, verticalPos, conf, actorAc
* @param description The text in the box * @param description The text in the box
*/ */
export const drawLoop = function(elem, bounds, labelText, conf) { export const drawLoop = function(elem, bounds, labelText, conf) {
const g = elem.append('g') const g = elem.append('g');
const drawLoopLine = function(startx, starty, stopx, stopy) { const drawLoopLine = function(startx, starty, stopx, stopy) {
return g.append('line') return g
.append('line')
.attr('x1', startx) .attr('x1', startx)
.attr('y1', starty) .attr('y1', starty)
.attr('x2', stopx) .attr('x2', stopx)
.attr('y2', stopy) .attr('y2', stopy)
.attr('class', 'loopLine') .attr('class', 'loopLine');
} };
drawLoopLine(bounds.startx, bounds.starty, bounds.stopx, bounds.starty) drawLoopLine(bounds.startx, bounds.starty, bounds.stopx, bounds.starty);
drawLoopLine(bounds.stopx, bounds.starty, bounds.stopx, bounds.stopy) drawLoopLine(bounds.stopx, bounds.starty, bounds.stopx, bounds.stopy);
drawLoopLine(bounds.startx, bounds.stopy, bounds.stopx, bounds.stopy) drawLoopLine(bounds.startx, bounds.stopy, bounds.stopx, bounds.stopy);
drawLoopLine(bounds.startx, bounds.starty, bounds.startx, bounds.stopy) drawLoopLine(bounds.startx, bounds.starty, bounds.startx, bounds.stopy);
if (typeof bounds.sections !== 'undefined') { if (typeof bounds.sections !== 'undefined') {
bounds.sections.forEach(function(item) { bounds.sections.forEach(function(item) {
drawLoopLine(bounds.startx, item, bounds.stopx, item).style('stroke-dasharray', '3, 3') drawLoopLine(bounds.startx, item, bounds.stopx, item).style('stroke-dasharray', '3, 3');
}) });
} }
let txt = getTextObj() let txt = getTextObj();
txt.text = labelText txt.text = labelText;
txt.x = bounds.startx txt.x = bounds.startx;
txt.y = bounds.starty txt.y = bounds.starty;
txt.labelMargin = 1.5 * 10 // This is the small box that says "loop" txt.labelMargin = 1.5 * 10; // This is the small box that says "loop"
txt.class = 'labelText' // Its size & position are fixed. txt.class = 'labelText'; // Its size & position are fixed.
drawLabel(g, txt) drawLabel(g, txt);
txt = getTextObj() txt = getTextObj();
txt.text = '[ ' + bounds.title + ' ]' txt.text = '[ ' + bounds.title + ' ]';
txt.x = bounds.startx + (bounds.stopx - bounds.startx) / 2 txt.x = bounds.startx + (bounds.stopx - bounds.startx) / 2;
txt.y = bounds.starty + 1.5 * conf.boxMargin txt.y = bounds.starty + 1.5 * conf.boxMargin;
txt.anchor = 'middle' txt.anchor = 'middle';
txt.class = 'loopText' txt.class = 'loopText';
drawText(g, txt) drawText(g, txt);
if (typeof bounds.sectionTitles !== 'undefined') { if (typeof bounds.sectionTitles !== 'undefined') {
bounds.sectionTitles.forEach(function(item, idx) { bounds.sectionTitles.forEach(function(item, idx) {
if (item !== '') { if (item !== '') {
txt.text = '[ ' + item + ' ]' txt.text = '[ ' + item + ' ]';
txt.y = bounds.sections[idx] + 1.5 * conf.boxMargin txt.y = bounds.sections[idx] + 1.5 * conf.boxMargin;
drawText(g, txt) drawText(g, txt);
}
})
} }
});
} }
};
/** /**
* Draws a background rectangle * Draws a background rectangle
@ -179,14 +204,16 @@ export const drawBackgroundRect = function (elem, bounds) {
height: bounds.stopy - bounds.starty, height: bounds.stopy - bounds.starty,
fill: bounds.fill, fill: bounds.fill,
class: 'rect' class: 'rect'
}) });
rectElem.lower() rectElem.lower();
} };
/** /**
* Setup arrow head and define the marker. The result is appended to the svg. * Setup arrow head and define the marker. The result is appended to the svg.
*/ */
export const insertArrowHead = function(elem) { export const insertArrowHead = function(elem) {
elem.append('defs').append('marker') elem
.append('defs')
.append('marker')
.attr('id', 'arrowhead') .attr('id', 'arrowhead')
.attr('refX', 5) .attr('refX', 5)
.attr('refY', 2) .attr('refY', 2)
@ -194,13 +221,15 @@ export const insertArrowHead = function (elem) {
.attr('markerHeight', 4) .attr('markerHeight', 4)
.attr('orient', 'auto') .attr('orient', 'auto')
.append('path') .append('path')
.attr('d', 'M 0,0 V 4 L6,2 Z') // this is actual shape for arrowhead .attr('d', 'M 0,0 V 4 L6,2 Z'); // this is actual shape for arrowhead
} };
/** /**
* Setup node number. The result is appended to the svg. * Setup node number. The result is appended to the svg.
*/ */
export const insertSequenceNumber = function(elem) { export const insertSequenceNumber = function(elem) {
elem.append('defs').append('marker') elem
.append('defs')
.append('marker')
.attr('id', 'sequencenumber') .attr('id', 'sequencenumber')
.attr('refX', 15) .attr('refX', 15)
.attr('refY', 15) .attr('refY', 15)
@ -210,45 +239,48 @@ export const insertSequenceNumber = function (elem) {
.append('circle') .append('circle')
.attr('cx', 15) .attr('cx', 15)
.attr('cy', 15) .attr('cy', 15)
.attr('r', 6) .attr('r', 6);
// .style("fill", '#f00'); // .style("fill", '#f00');
} };
/** /**
* Setup arrow head and define the marker. The result is appended to the svg. * Setup arrow head and define the marker. The result is appended to the svg.
*/ */
export const insertArrowCrossHead = function(elem) { export const insertArrowCrossHead = function(elem) {
const defs = elem.append('defs') const defs = elem.append('defs');
const marker = defs.append('marker') const marker = defs
.append('marker')
.attr('id', 'crosshead') .attr('id', 'crosshead')
.attr('markerWidth', 15) .attr('markerWidth', 15)
.attr('markerHeight', 8) .attr('markerHeight', 8)
.attr('orient', 'auto') .attr('orient', 'auto')
.attr('refX', 16) .attr('refX', 16)
.attr('refY', 4) .attr('refY', 4);
// The arrow // The arrow
marker.append('path') marker
.append('path')
.attr('fill', 'black') .attr('fill', 'black')
.attr('stroke', '#000000') .attr('stroke', '#000000')
.style('stroke-dasharray', ('0, 0')) .style('stroke-dasharray', '0, 0')
.attr('stroke-width', '1px') .attr('stroke-width', '1px')
.attr('d', 'M 9,2 V 6 L16,4 Z') .attr('d', 'M 9,2 V 6 L16,4 Z');
// The cross // The cross
marker.append('path') marker
.append('path')
.attr('fill', 'none') .attr('fill', 'none')
.attr('stroke', '#000000') .attr('stroke', '#000000')
.style('stroke-dasharray', ('0, 0')) .style('stroke-dasharray', '0, 0')
.attr('stroke-width', '1px') .attr('stroke-width', '1px')
.attr('d', 'M 0,1 L 6,7 M 6,1 L 0,7') .attr('d', 'M 0,1 L 6,7 M 6,1 L 0,7');
// this is actual shape for arrowhead // this is actual shape for arrowhead
} };
export const getTextObj = function() { export const getTextObj = function() {
const txt = { const txt = {
x: 0, x: 0,
y: 0, y: 0,
'fill': undefined, fill: undefined,
'text-anchor': 'start', 'text-anchor': 'start',
style: '#666', style: '#666',
width: 100, width: 100,
@ -256,9 +288,9 @@ export const getTextObj = function () {
textMargin: 0, textMargin: 0,
rx: 0, rx: 0,
ry: 0 ry: 0
} };
return txt return txt;
} };
export const getNoteRect = function() { export const getNoteRect = function() {
const rect = { const rect = {
@ -271,72 +303,87 @@ export const getNoteRect = function () {
height: 100, height: 100,
rx: 0, rx: 0,
ry: 0 ry: 0
} };
return rect return rect;
} };
const _drawTextCandidateFunc = (function() { const _drawTextCandidateFunc = (function() {
function byText(content, g, x, y, width, height, textAttrs) { function byText(content, g, x, y, width, height, textAttrs) {
const text = g.append('text') const text = g
.attr('x', x + width / 2).attr('y', y + height / 2 + 5) .append('text')
.attr('x', x + width / 2)
.attr('y', y + height / 2 + 5)
.style('text-anchor', 'middle') .style('text-anchor', 'middle')
.text(content) .text(content);
_setTextAttrs(text, textAttrs) _setTextAttrs(text, textAttrs);
} }
function byTspan(content, g, x, y, width, height, textAttrs, conf) { function byTspan(content, g, x, y, width, height, textAttrs, conf) {
const { actorFontSize, actorFontFamily } = conf const { actorFontSize, actorFontFamily } = conf;
const lines = content.split(/<br\/?>/ig) const lines = content.split(/<br\/?>/gi);
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const dy = (i * actorFontSize) - (actorFontSize * (lines.length - 1) / 2) const dy = i * actorFontSize - (actorFontSize * (lines.length - 1)) / 2;
const text = g.append('text') const text = g
.attr('x', x + width / 2).attr('y', y) .append('text')
.attr('x', x + width / 2)
.attr('y', y)
.style('text-anchor', 'middle') .style('text-anchor', 'middle')
.style('font-size', actorFontSize) .style('font-size', actorFontSize)
.style('font-family', actorFontFamily) .style('font-family', actorFontFamily);
text.append('tspan') text
.attr('x', x + width / 2).attr('dy', dy) .append('tspan')
.text(lines[i]) .attr('x', x + width / 2)
.attr('dy', dy)
.text(lines[i]);
text.attr('y', y + height / 2.0) text
.attr('y', y + height / 2.0)
.attr('dominant-baseline', 'central') .attr('dominant-baseline', 'central')
.attr('alignment-baseline', 'central') .attr('alignment-baseline', 'central');
_setTextAttrs(text, textAttrs) _setTextAttrs(text, textAttrs);
} }
} }
function byFo(content, g, x, y, width, height, textAttrs, conf) { function byFo(content, g, x, y, width, height, textAttrs, conf) {
const s = g.append('switch') const s = g.append('switch');
const f = s.append('foreignObject') const f = s
.attr('x', x).attr('y', y) .append('foreignObject')
.attr('width', width).attr('height', height) .attr('x', x)
.attr('y', y)
.attr('width', width)
.attr('height', height);
const text = f.append('div').style('display', 'table') const text = f
.style('height', '100%').style('width', '100%') .append('div')
.style('display', 'table')
.style('height', '100%')
.style('width', '100%');
text.append('div').style('display', 'table-cell') text
.style('text-align', 'center').style('vertical-align', 'middle') .append('div')
.text(content) .style('display', 'table-cell')
.style('text-align', 'center')
.style('vertical-align', 'middle')
.text(content);
byTspan(content, s, x, y, width, height, textAttrs, conf) byTspan(content, s, x, y, width, height, textAttrs, conf);
_setTextAttrs(text, textAttrs) _setTextAttrs(text, textAttrs);
} }
function _setTextAttrs(toText, fromTextAttrsDict) { function _setTextAttrs(toText, fromTextAttrsDict) {
for (const key in fromTextAttrsDict) { for (const key in fromTextAttrsDict) {
if (fromTextAttrsDict.hasOwnProperty(key)) { if (fromTextAttrsDict.hasOwnProperty(key)) {
toText.attr(key, fromTextAttrsDict[key]) toText.attr(key, fromTextAttrsDict[key]);
} }
} }
} }
return function(conf) { return function(conf) {
return conf.textPlacement === 'fo' ? byFo : ( return conf.textPlacement === 'fo' ? byFo : conf.textPlacement === 'old' ? byText : byTspan;
conf.textPlacement === 'old' ? byText : byTspan) };
} })();
})()
export default { export default {
drawRect, drawRect,
@ -352,4 +399,4 @@ export default {
insertArrowCrossHead, insertArrowCrossHead,
getTextObj, getTextObj,
getNoteRect getNoteRect
} };

View File

@ -1,11 +1,11 @@
/* eslint-env jasmine */ /* eslint-env jasmine */
const svgDraw = require('./svgDraw') const svgDraw = require('./svgDraw');
const { MockD3 } = require('d3') const { MockD3 } = require('d3');
describe('svgDraw', function() { describe('svgDraw', function() {
describe('drawRect', function() { describe('drawRect', function() {
it('it should append a rectangle', function() { it('it should append a rectangle', function() {
const svg = MockD3('svg') const svg = MockD3('svg');
svgDraw.drawRect(svg, { svgDraw.drawRect(svg, {
x: 10, x: 10,
y: 10, y: 10,
@ -16,22 +16,22 @@ describe('svgDraw', function () {
rx: '10', rx: '10',
ry: '10', ry: '10',
class: 'unitTestRectangleClass' class: 'unitTestRectangleClass'
}) });
expect(svg.__children.length).toBe(1) expect(svg.__children.length).toBe(1);
const rect = svg.__children[0] const rect = svg.__children[0];
expect(rect.__name).toBe('rect') expect(rect.__name).toBe('rect');
expect(rect.attr).toHaveBeenCalledWith('x', 10) expect(rect.attr).toHaveBeenCalledWith('x', 10);
expect(rect.attr).toHaveBeenCalledWith('y', 10) expect(rect.attr).toHaveBeenCalledWith('y', 10);
expect(rect.attr).toHaveBeenCalledWith('fill', '#ccc') expect(rect.attr).toHaveBeenCalledWith('fill', '#ccc');
expect(rect.attr).toHaveBeenCalledWith('stroke', 'red') expect(rect.attr).toHaveBeenCalledWith('stroke', 'red');
expect(rect.attr).toHaveBeenCalledWith('width', '20') expect(rect.attr).toHaveBeenCalledWith('width', '20');
expect(rect.attr).toHaveBeenCalledWith('height', '20') expect(rect.attr).toHaveBeenCalledWith('height', '20');
expect(rect.attr).toHaveBeenCalledWith('rx', '10') expect(rect.attr).toHaveBeenCalledWith('rx', '10');
expect(rect.attr).toHaveBeenCalledWith('ry', '10') expect(rect.attr).toHaveBeenCalledWith('ry', '10');
expect(rect.attr).toHaveBeenCalledWith('class', 'unitTestRectangleClass') expect(rect.attr).toHaveBeenCalledWith('class', 'unitTestRectangleClass');
}) });
it('it should not add the class attribute if a class isn`t provided', () => { it('it should not add the class attribute if a class isn`t provided', () => {
const svg = MockD3('svg') const svg = MockD3('svg');
svgDraw.drawRect(svg, { svgDraw.drawRect(svg, {
x: 10, x: 10,
y: 10, y: 10,
@ -41,17 +41,17 @@ describe('svgDraw', function () {
height: '20', height: '20',
rx: '10', rx: '10',
ry: '10' ry: '10'
}) });
expect(svg.__children.length).toBe(1) expect(svg.__children.length).toBe(1);
const rect = svg.__children[0] const rect = svg.__children[0];
expect(rect.__name).toBe('rect') expect(rect.__name).toBe('rect');
expect(rect.attr).toHaveBeenCalledWith('fill', '#ccc') expect(rect.attr).toHaveBeenCalledWith('fill', '#ccc');
expect(rect.attr).not.toHaveBeenCalledWith('class', expect.anything()) expect(rect.attr).not.toHaveBeenCalledWith('class', expect.anything());
}) });
}) });
describe('drawBackgroundRect', function() { describe('drawBackgroundRect', function() {
it('it should append a rect before the previous element within a given bound', function() { it('it should append a rect before the previous element within a given bound', function() {
const svg = MockD3('svg') const svg = MockD3('svg');
const boundingRect = { const boundingRect = {
startx: 50, startx: 50,
starty: 200, starty: 200,
@ -59,18 +59,18 @@ describe('svgDraw', function () {
stopy: 260, stopy: 260,
title: undefined, title: undefined,
fill: '#ccc' fill: '#ccc'
} };
svgDraw.drawBackgroundRect(svg, boundingRect) svgDraw.drawBackgroundRect(svg, boundingRect);
expect(svg.__children.length).toBe(1) expect(svg.__children.length).toBe(1);
const rect = svg.__children[0] const rect = svg.__children[0];
expect(rect.__name).toBe('rect') expect(rect.__name).toBe('rect');
expect(rect.attr).toHaveBeenCalledWith('x', 50) expect(rect.attr).toHaveBeenCalledWith('x', 50);
expect(rect.attr).toHaveBeenCalledWith('y', 200) expect(rect.attr).toHaveBeenCalledWith('y', 200);
expect(rect.attr).toHaveBeenCalledWith('width', 100) expect(rect.attr).toHaveBeenCalledWith('width', 100);
expect(rect.attr).toHaveBeenCalledWith('height', 60) expect(rect.attr).toHaveBeenCalledWith('height', 60);
expect(rect.attr).toHaveBeenCalledWith('fill', '#ccc') expect(rect.attr).toHaveBeenCalledWith('fill', '#ccc');
expect(rect.attr).toHaveBeenCalledWith('class', 'rect') expect(rect.attr).toHaveBeenCalledWith('class', 'rect');
expect(rect.lower).toHaveBeenCalled() expect(rect.lower).toHaveBeenCalled();
}) });
}) });
}) });

View File

@ -1,4 +1,4 @@
import moment from 'moment-mini' import moment from 'moment-mini';
export const LEVELS = { export const LEVELS = {
debug: 1, debug: 1,
@ -6,7 +6,7 @@ export const LEVELS = {
warn: 3, warn: 3,
error: 4, error: 4,
fatal: 5 fatal: 5
} };
export const logger = { export const logger = {
debug: () => {}, debug: () => {},
@ -14,32 +14,32 @@ export const logger = {
warn: () => {}, warn: () => {},
error: () => {}, error: () => {},
fatal: () => {} fatal: () => {}
} };
export const setLogLevel = function(level) { export const setLogLevel = function(level) {
logger.debug = () => {} logger.debug = () => {};
logger.info = () => {} logger.info = () => {};
logger.warn = () => {} logger.warn = () => {};
logger.error = () => {} logger.error = () => {};
logger.fatal = () => {} logger.fatal = () => {};
if (level <= LEVELS.fatal) { if (level <= LEVELS.fatal) {
logger.fatal = console.log.bind(console, '\x1b[35m', format('FATAL')) logger.fatal = console.log.bind(console, '\x1b[35m', format('FATAL'));
} }
if (level <= LEVELS.error) { if (level <= LEVELS.error) {
logger.error = console.log.bind(console, '\x1b[31m', format('ERROR')) logger.error = console.log.bind(console, '\x1b[31m', format('ERROR'));
} }
if (level <= LEVELS.warn) { if (level <= LEVELS.warn) {
logger.warn = console.log.bind(console, `\x1b[33m`, format('WARN')) logger.warn = console.log.bind(console, `\x1b[33m`, format('WARN'));
} }
if (level <= LEVELS.info) { if (level <= LEVELS.info) {
logger.info = console.log.bind(console, '\x1b[34m', format('INFO')) logger.info = console.log.bind(console, '\x1b[34m', format('INFO'));
} }
if (level <= LEVELS.debug) { if (level <= LEVELS.debug) {
logger.debug = console.log.bind(console, '\x1b[32m', format('DEBUG')) logger.debug = console.log.bind(console, '\x1b[32m', format('DEBUG'));
}
} }
};
const format = (level) => { const format = level => {
const time = moment().format('HH:mm:ss.SSS') const time = moment().format('HH:mm:ss.SSS');
return `${time} : ${level} : ` return `${time} : ${level} : `;
} };

View File

@ -2,10 +2,10 @@
* Web page integration module for the mermaid framework. It uses the mermaidAPI for mermaid functionality and to render * Web page integration module for the mermaid framework. It uses the mermaidAPI for mermaid functionality and to render
* the diagrams to svg code. * the diagrams to svg code.
*/ */
import he from 'he' import he from 'he';
import mermaidAPI from './mermaidAPI' import mermaidAPI from './mermaidAPI';
import { logger } from './logger' import { logger } from './logger';
/** /**
* ## init * ## init
@ -29,92 +29,104 @@ import { logger } from './logger'
* @param nodes a css selector or an array of nodes * @param nodes a css selector or an array of nodes
*/ */
const init = function() { const init = function() {
const conf = mermaidAPI.getConfig() const conf = mermaidAPI.getConfig();
logger.debug('Starting rendering diagrams') logger.debug('Starting rendering diagrams');
let nodes let nodes;
if (arguments.length >= 2) { if (arguments.length >= 2) {
/*! sequence config was passed as #1 */ /*! sequence config was passed as #1 */
if (typeof arguments[0] !== 'undefined') { if (typeof arguments[0] !== 'undefined') {
mermaid.sequenceConfig = arguments[0] mermaid.sequenceConfig = arguments[0];
} }
nodes = arguments[1] nodes = arguments[1];
} else { } else {
nodes = arguments[0] nodes = arguments[0];
} }
// if last argument is a function this is the callback function // if last argument is a function this is the callback function
let callback let callback;
if (typeof arguments[arguments.length - 1] === 'function') { if (typeof arguments[arguments.length - 1] === 'function') {
callback = arguments[arguments.length - 1] callback = arguments[arguments.length - 1];
logger.debug('Callback function found') logger.debug('Callback function found');
} else { } else {
if (typeof conf.mermaid !== 'undefined') { if (typeof conf.mermaid !== 'undefined') {
if (typeof conf.mermaid.callback === 'function') { if (typeof conf.mermaid.callback === 'function') {
callback = conf.mermaid.callback callback = conf.mermaid.callback;
logger.debug('Callback function found') logger.debug('Callback function found');
} else { } else {
logger.debug('No Callback function found') logger.debug('No Callback function found');
} }
} }
} }
nodes = nodes === undefined ? document.querySelectorAll('.mermaid') nodes =
: typeof nodes === 'string' ? document.querySelectorAll(nodes) nodes === undefined
: nodes instanceof window.Node ? [nodes] ? document.querySelectorAll('.mermaid')
: nodes // Last case - sequence config was passed pick next : typeof nodes === 'string'
? document.querySelectorAll(nodes)
: nodes instanceof window.Node
? [nodes]
: nodes; // Last case - sequence config was passed pick next
logger.debug('Start On Load before: ' + mermaid.startOnLoad) logger.debug('Start On Load before: ' + mermaid.startOnLoad);
if (typeof mermaid.startOnLoad !== 'undefined') { if (typeof mermaid.startOnLoad !== 'undefined') {
logger.debug('Start On Load inner: ' + mermaid.startOnLoad) logger.debug('Start On Load inner: ' + mermaid.startOnLoad);
mermaidAPI.initialize({ startOnLoad: mermaid.startOnLoad }) mermaidAPI.initialize({ startOnLoad: mermaid.startOnLoad });
} }
if (typeof mermaid.ganttConfig !== 'undefined') { if (typeof mermaid.ganttConfig !== 'undefined') {
mermaidAPI.initialize({ gantt: mermaid.ganttConfig }) mermaidAPI.initialize({ gantt: mermaid.ganttConfig });
} }
let txt let txt;
for (let i = 0; i < nodes.length; i++) { for (let i = 0; i < nodes.length; i++) {
const element = nodes[i] const element = nodes[i];
/*! Check if previously processed */ /*! Check if previously processed */
if (!element.getAttribute('data-processed')) { if (!element.getAttribute('data-processed')) {
element.setAttribute('data-processed', true) element.setAttribute('data-processed', true);
} else { } else {
continue continue;
} }
const id = `mermaid-${Date.now()}` const id = `mermaid-${Date.now()}`;
// Fetch the graph definition including tags // Fetch the graph definition including tags
txt = element.innerHTML txt = element.innerHTML;
// transforms the html to pure text // transforms the html to pure text
txt = he.decode(txt).trim().replace(/<br>/ig, '<br/>') txt = he
.decode(txt)
.trim()
.replace(/<br>/gi, '<br/>');
mermaidAPI.render(id, txt, (svgCode, bindFunctions) => { mermaidAPI.render(
element.innerHTML = svgCode id,
txt,
(svgCode, bindFunctions) => {
element.innerHTML = svgCode;
if (typeof callback !== 'undefined') { if (typeof callback !== 'undefined') {
callback(id) callback(id);
}
if (bindFunctions) bindFunctions(element)
}, element)
} }
if (bindFunctions) bindFunctions(element);
},
element
);
} }
};
const initialize = function(config) { const initialize = function(config) {
logger.debug('Initializing mermaid ') logger.debug('Initializing mermaid ');
if (typeof config.mermaid !== 'undefined') { if (typeof config.mermaid !== 'undefined') {
if (typeof config.mermaid.startOnLoad !== 'undefined') { if (typeof config.mermaid.startOnLoad !== 'undefined') {
mermaid.startOnLoad = config.mermaid.startOnLoad mermaid.startOnLoad = config.mermaid.startOnLoad;
} }
if (typeof config.mermaid.htmlLabels !== 'undefined') { if (typeof config.mermaid.htmlLabels !== 'undefined') {
mermaid.htmlLabels = config.mermaid.htmlLabels mermaid.htmlLabels = config.mermaid.htmlLabels;
} }
} }
mermaidAPI.initialize(config) mermaidAPI.initialize(config);
} };
/** /**
* ##contentLoaded * ##contentLoaded
@ -122,32 +134,36 @@ const initialize = function (config) {
* 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 config let config;
if (mermaid.startOnLoad) { if (mermaid.startOnLoad) {
// No config found, do check API config // No config found, do check API config
config = mermaidAPI.getConfig() config = mermaidAPI.getConfig();
if (config.startOnLoad) { if (config.startOnLoad) {
mermaid.init() mermaid.init();
} }
} else { } else {
if (typeof mermaid.startOnLoad === 'undefined') { if (typeof mermaid.startOnLoad === 'undefined') {
logger.debug('In start, no config') logger.debug('In start, no config');
config = mermaidAPI.getConfig() config = mermaidAPI.getConfig();
if (config.startOnLoad) { if (config.startOnLoad) {
mermaid.init() mermaid.init();
}
} }
} }
} }
};
if (typeof document !== 'undefined') { if (typeof document !== 'undefined') {
/*! /*!
* Wait for document loaded before starting the execution * Wait for document loaded before starting the execution
*/ */
window.addEventListener('load', function () { window.addEventListener(
contentLoaded() 'load',
}, false) function() {
contentLoaded();
},
false
);
} }
const mermaid = { const mermaid = {
@ -162,6 +178,6 @@ const mermaid = {
initialize, initialize,
contentLoaded contentLoaded
} };
export default mermaid export default mermaid;

View File

@ -1,195 +1,200 @@
/* eslint-env jasmine */ /* eslint-env jasmine */
import mermaid from './mermaid' import mermaid from './mermaid';
import flowDb from './diagrams/flowchart/flowDb' import flowDb from './diagrams/flowchart/flowDb';
import flowParser from './diagrams/flowchart/parser/flow' import flowParser from './diagrams/flowchart/parser/flow';
import flowRenderer from './diagrams/flowchart/flowRenderer' import flowRenderer from './diagrams/flowchart/flowRenderer';
describe('when using mermaid and ', function() { describe('when using mermaid and ', function() {
describe('when detecting chart type ', function() { describe('when detecting chart type ', function() {
it('should not start rendering with mermaid.startOnLoad set to false', function() { it('should not start rendering with mermaid.startOnLoad set to false', function() {
mermaid.startOnLoad = false mermaid.startOnLoad = false;
document.body.innerHTML = '<div class="mermaid">graph TD;\na;</div>' document.body.innerHTML = '<div class="mermaid">graph TD;\na;</div>';
spyOn(mermaid, 'init') spyOn(mermaid, 'init');
mermaid.contentLoaded() mermaid.contentLoaded();
expect(mermaid.init).not.toHaveBeenCalled() expect(mermaid.init).not.toHaveBeenCalled();
}) });
it('should start rendering with both startOnLoad set', function() { it('should start rendering with both startOnLoad set', function() {
mermaid.startOnLoad = true mermaid.startOnLoad = true;
document.body.innerHTML = '<div class="mermaid">graph TD;\na;</div>' document.body.innerHTML = '<div class="mermaid">graph TD;\na;</div>';
spyOn(mermaid, 'init') spyOn(mermaid, 'init');
mermaid.contentLoaded() mermaid.contentLoaded();
expect(mermaid.init).toHaveBeenCalled() expect(mermaid.init).toHaveBeenCalled();
}) });
it('should start rendering with mermaid.startOnLoad', function() { it('should start rendering with mermaid.startOnLoad', function() {
mermaid.startOnLoad = true mermaid.startOnLoad = true;
document.body.innerHTML = '<div class="mermaid">graph TD;\na;</div>' document.body.innerHTML = '<div class="mermaid">graph TD;\na;</div>';
spyOn(mermaid, 'init') spyOn(mermaid, 'init');
mermaid.contentLoaded() mermaid.contentLoaded();
expect(mermaid.init).toHaveBeenCalled() expect(mermaid.init).toHaveBeenCalled();
}) });
it('should start rendering as a default with no changes performed', function() { it('should start rendering as a default with no changes performed', function() {
document.body.innerHTML = '<div class="mermaid">graph TD;\na;</div>' document.body.innerHTML = '<div class="mermaid">graph TD;\na;</div>';
spyOn(mermaid, 'init') spyOn(mermaid, 'init');
mermaid.contentLoaded() mermaid.contentLoaded();
expect(mermaid.init).toHaveBeenCalled() expect(mermaid.init).toHaveBeenCalled();
}) });
}) });
describe('when calling addEdges ', function() { describe('when calling addEdges ', function() {
beforeEach(function() { beforeEach(function() {
flowParser.parser.yy = flowDb flowParser.parser.yy = flowDb;
flowDb.clear() flowDb.clear();
}) });
it('it should handle edges with text', function() { it('it should handle edges with text', function() {
flowParser.parser.parse('graph TD;A-->|text ex|B;') flowParser.parser.parse('graph TD;A-->|text ex|B;');
flowParser.parser.yy.getVertices() flowParser.parser.yy.getVertices();
const edges = flowParser.parser.yy.getEdges() const edges = flowParser.parser.yy.getEdges();
const mockG = { const mockG = {
setEdge: function(start, end, options) { setEdge: function(start, end, options) {
expect(start).toBe('A') expect(start).toBe('A');
expect(end).toBe('B') expect(end).toBe('B');
expect(options.arrowhead).toBe('normal') expect(options.arrowhead).toBe('normal');
expect(options.label.match('text ex')).toBeTruthy() expect(options.label.match('text ex')).toBeTruthy();
}
} }
};
flowRenderer.addEdges(edges, mockG) flowRenderer.addEdges(edges, mockG);
}) });
it('should handle edges without text', function() { it('should handle edges without text', function() {
flowParser.parser.parse('graph TD;A-->B;') flowParser.parser.parse('graph TD;A-->B;');
flowParser.parser.yy.getVertices() flowParser.parser.yy.getVertices();
const edges = flowParser.parser.yy.getEdges() const edges = flowParser.parser.yy.getEdges();
const mockG = { const mockG = {
setEdge: function(start, end, options) { setEdge: function(start, end, options) {
expect(start).toBe('A') expect(start).toBe('A');
expect(end).toBe('B') expect(end).toBe('B');
expect(options.arrowhead).toBe('normal') expect(options.arrowhead).toBe('normal');
}
} }
};
flowRenderer.addEdges(edges, mockG) flowRenderer.addEdges(edges, mockG);
}) });
it('should handle open-ended edges', function() { it('should handle open-ended edges', function() {
flowParser.parser.parse('graph TD;A---B;') flowParser.parser.parse('graph TD;A---B;');
flowParser.parser.yy.getVertices() flowParser.parser.yy.getVertices();
const edges = flowParser.parser.yy.getEdges() const edges = flowParser.parser.yy.getEdges();
const mockG = { const mockG = {
setEdge: function(start, end, options) { setEdge: function(start, end, options) {
expect(start).toBe('A') expect(start).toBe('A');
expect(end).toBe('B') expect(end).toBe('B');
expect(options.arrowhead).toBe('none') expect(options.arrowhead).toBe('none');
}
} }
};
flowRenderer.addEdges(edges, mockG) flowRenderer.addEdges(edges, mockG);
}) });
it('should handle edges with styles defined', function() { it('should handle edges with styles defined', function() {
flowParser.parser.parse('graph TD;A---B; linkStyle 0 stroke:val1,stroke-width:val2;') flowParser.parser.parse('graph TD;A---B; linkStyle 0 stroke:val1,stroke-width:val2;');
flowParser.parser.yy.getVertices() flowParser.parser.yy.getVertices();
const edges = flowParser.parser.yy.getEdges() const edges = flowParser.parser.yy.getEdges();
const mockG = { const mockG = {
setEdge: function(start, end, options) { setEdge: function(start, end, options) {
expect(start).toBe('A') expect(start).toBe('A');
expect(end).toBe('B') expect(end).toBe('B');
expect(options.arrowhead).toBe('none') expect(options.arrowhead).toBe('none');
expect(options.style).toBe('stroke:val1;stroke-width:val2;fill:none;') expect(options.style).toBe('stroke:val1;stroke-width:val2;fill:none;');
}
} }
};
flowRenderer.addEdges(edges, mockG) flowRenderer.addEdges(edges, mockG);
}) });
it('should handle edges with interpolation defined', function() { it('should handle edges with interpolation defined', function() {
flowParser.parser.parse('graph TD;A---B; linkStyle 0 interpolate basis') flowParser.parser.parse('graph TD;A---B; linkStyle 0 interpolate basis');
flowParser.parser.yy.getVertices() flowParser.parser.yy.getVertices();
const edges = flowParser.parser.yy.getEdges() const edges = flowParser.parser.yy.getEdges();
const mockG = { const mockG = {
setEdge: function(start, end, options) { setEdge: function(start, end, options) {
expect(start).toBe('A') expect(start).toBe('A');
expect(end).toBe('B') expect(end).toBe('B');
expect(options.arrowhead).toBe('none') expect(options.arrowhead).toBe('none');
expect(options.curve).toBe('basis') // mocked as string expect(options.curve).toBe('basis'); // mocked as string
}
} }
};
flowRenderer.addEdges(edges, mockG) flowRenderer.addEdges(edges, mockG);
}) });
it('should handle edges with text and styles defined', function() { it('should handle edges with text and styles defined', function() {
flowParser.parser.parse('graph TD;A---|the text|B; linkStyle 0 stroke:val1,stroke-width:val2;') flowParser.parser.parse(
flowParser.parser.yy.getVertices() 'graph TD;A---|the text|B; linkStyle 0 stroke:val1,stroke-width:val2;'
const edges = flowParser.parser.yy.getEdges() );
flowParser.parser.yy.getVertices();
const edges = flowParser.parser.yy.getEdges();
const mockG = { const mockG = {
setEdge: function(start, end, options) { setEdge: function(start, end, options) {
expect(start).toBe('A') expect(start).toBe('A');
expect(end).toBe('B') expect(end).toBe('B');
expect(options.arrowhead).toBe('none') expect(options.arrowhead).toBe('none');
expect(options.label.match('the text')).toBeTruthy() expect(options.label.match('the text')).toBeTruthy();
expect(options.style).toBe('stroke:val1;stroke-width:val2;fill:none;') expect(options.style).toBe('stroke:val1;stroke-width:val2;fill:none;');
}
} }
};
flowRenderer.addEdges(edges, mockG) flowRenderer.addEdges(edges, mockG);
}) });
it('should set fill to "none" by default when handling edges', function() { it('should set fill to "none" by default when handling edges', function() {
flowParser.parser.parse('graph TD;A---B; linkStyle 0 stroke:val1,stroke-width:val2;') flowParser.parser.parse('graph TD;A---B; linkStyle 0 stroke:val1,stroke-width:val2;');
flowParser.parser.yy.getVertices() flowParser.parser.yy.getVertices();
const edges = flowParser.parser.yy.getEdges() const edges = flowParser.parser.yy.getEdges();
const mockG = { const mockG = {
setEdge: function(start, end, options) { setEdge: function(start, end, options) {
expect(start).toBe('A') expect(start).toBe('A');
expect(end).toBe('B') expect(end).toBe('B');
expect(options.arrowhead).toBe('none') expect(options.arrowhead).toBe('none');
expect(options.style).toBe('stroke:val1;stroke-width:val2;fill:none;') expect(options.style).toBe('stroke:val1;stroke-width:val2;fill:none;');
}
} }
};
flowRenderer.addEdges(edges, mockG) flowRenderer.addEdges(edges, mockG);
}) });
it('should not set fill to none if fill is set in linkStyle', function() { it('should not set fill to none if fill is set in linkStyle', function() {
flowParser.parser.parse('graph TD;A---B; linkStyle 0 stroke:val1,stroke-width:val2,fill:blue;') flowParser.parser.parse(
flowParser.parser.yy.getVertices() 'graph TD;A---B; linkStyle 0 stroke:val1,stroke-width:val2,fill:blue;'
const edges = flowParser.parser.yy.getEdges() );
flowParser.parser.yy.getVertices();
const edges = flowParser.parser.yy.getEdges();
const mockG = { const mockG = {
setEdge: function(start, end, options) { setEdge: function(start, end, options) {
expect(start).toBe('A') expect(start).toBe('A');
expect(end).toBe('B') expect(end).toBe('B');
expect(options.arrowhead).toBe('none') expect(options.arrowhead).toBe('none');
expect(options.style).toBe('stroke:val1;stroke-width:val2;fill:blue;') expect(options.style).toBe('stroke:val1;stroke-width:val2;fill:blue;');
}
} }
};
flowRenderer.addEdges(edges, mockG) flowRenderer.addEdges(edges, mockG);
}) });
}) });
describe('checking validity of input ', function() { describe('checking validity of input ', function() {
it('it should throw for an invalid definiton', function() { it('it should throw for an invalid definiton', function() {
expect(() => mermaid.parse('this is not a mermaid diagram definition')).toThrow() expect(() => mermaid.parse('this is not a mermaid diagram definition')).toThrow();
}) });
it('it should not throw for a valid flow definition', function() { it('it should not throw for a valid flow definition', function() {
expect(() => mermaid.parse('graph TD;A--x|text including URL space|B;')).not.toThrow() expect(() => mermaid.parse('graph TD;A--x|text including URL space|B;')).not.toThrow();
}) });
it('it should throw for an invalid flow definition', function() { it('it should throw for an invalid flow definition', function() {
expect(() => mermaid.parse('graph TQ;A--x|text including URL space|B;')).toThrow() expect(() => mermaid.parse('graph TQ;A--x|text including URL space|B;')).toThrow();
}) });
it('it should not throw for a valid sequenceDiagram definition', function() { it('it should not throw for a valid sequenceDiagram definition', function() {
const text = 'sequenceDiagram\n' + const text =
'sequenceDiagram\n' +
'Alice->Bob: Hello Bob, how are you?\n\n' + 'Alice->Bob: Hello Bob, how are you?\n\n' +
'%% Comment\n' + '%% Comment\n' +
'Note right of Bob: Bob thinks\n' + 'Note right of Bob: Bob thinks\n' +
@ -197,12 +202,13 @@ describe('when using mermaid and ', function () {
'Bob-->Alice: I am good thanks!\n' + 'Bob-->Alice: I am good thanks!\n' +
'else isSick\n' + 'else isSick\n' +
'Bob-->Alice: Feel sick...\n' + 'Bob-->Alice: Feel sick...\n' +
'end' 'end';
expect(() => mermaid.parse(text)).not.toThrow() expect(() => mermaid.parse(text)).not.toThrow();
}) });
it('it should throw for an invalid sequenceDiagram definition', function() { it('it should throw for an invalid sequenceDiagram definition', function() {
const text = 'sequenceDiagram\n' + const text =
'sequenceDiagram\n' +
'Alice:->Bob: Hello Bob, how are you?\n\n' + 'Alice:->Bob: Hello Bob, how are you?\n\n' +
'%% Comment\n' + '%% Comment\n' +
'Note right of Bob: Bob thinks\n' + 'Note right of Bob: Bob thinks\n' +
@ -210,8 +216,8 @@ describe('when using mermaid and ', function () {
'Bob-->Alice: I am good thanks!\n' + 'Bob-->Alice: I am good thanks!\n' +
'else isSick\n' + 'else isSick\n' +
'Bob-->Alice: Feel sick...\n' + 'Bob-->Alice: Feel sick...\n' +
'end' 'end';
expect(() => mermaid.parse(text)).toThrow() expect(() => mermaid.parse(text)).toThrow();
}) });
}) });
}) });

View File

@ -10,37 +10,37 @@
* *
* @name mermaidAPI * @name mermaidAPI
*/ */
import * as d3 from 'd3' import * as d3 from 'd3';
import scope from 'scope-css' import scope from 'scope-css';
import pkg from '../package.json' import pkg from '../package.json';
import { setConfig, getConfig } from './config' import { setConfig, getConfig } from './config';
import { logger, setLogLevel } from './logger' import { logger, setLogLevel } from './logger';
import utils from './utils' import utils from './utils';
import flowRenderer from './diagrams/flowchart/flowRenderer' import flowRenderer from './diagrams/flowchart/flowRenderer';
import flowParser from './diagrams/flowchart/parser/flow' import flowParser from './diagrams/flowchart/parser/flow';
import flowDb from './diagrams/flowchart/flowDb' import flowDb from './diagrams/flowchart/flowDb';
import sequenceRenderer from './diagrams/sequence/sequenceRenderer' import sequenceRenderer from './diagrams/sequence/sequenceRenderer';
import sequenceParser from './diagrams/sequence/parser/sequenceDiagram' import sequenceParser from './diagrams/sequence/parser/sequenceDiagram';
import sequenceDb from './diagrams/sequence/sequenceDb' import sequenceDb from './diagrams/sequence/sequenceDb';
import ganttRenderer from './diagrams/gantt/ganttRenderer' import ganttRenderer from './diagrams/gantt/ganttRenderer';
import ganttParser from './diagrams/gantt/parser/gantt' import ganttParser from './diagrams/gantt/parser/gantt';
import ganttDb from './diagrams/gantt/ganttDb' import ganttDb from './diagrams/gantt/ganttDb';
import classRenderer from './diagrams/class/classRenderer' import classRenderer from './diagrams/class/classRenderer';
import classParser from './diagrams/class/parser/classDiagram' import classParser from './diagrams/class/parser/classDiagram';
import classDb from './diagrams/class/classDb' import classDb from './diagrams/class/classDb';
import gitGraphRenderer from './diagrams/git/gitGraphRenderer' import gitGraphRenderer from './diagrams/git/gitGraphRenderer';
import gitGraphParser from './diagrams/git/parser/gitGraph' import gitGraphParser from './diagrams/git/parser/gitGraph';
import gitGraphAst from './diagrams/git/gitGraphAst' import gitGraphAst from './diagrams/git/gitGraphAst';
import infoRenderer from './diagrams/info/infoRenderer' import infoRenderer from './diagrams/info/infoRenderer';
import infoParser from './diagrams/info/parser/info' import infoParser from './diagrams/info/parser/info';
import infoDb from './diagrams/info/infoDb' import infoDb from './diagrams/info/infoDb';
import pieRenderer from './diagrams/pie/pieRenderer' import pieRenderer from './diagrams/pie/pieRenderer';
import pieParser from './diagrams/pie/parser/pie' import pieParser from './diagrams/pie/parser/pie';
import pieDb from './diagrams/pie/pieDb' import pieDb from './diagrams/pie/pieDb';
const themes = {} const themes = {};
for (const themeName of ['default', 'forest', 'dark', 'neutral']) { for (const themeName of ['default', 'forest', 'dark', 'neutral']) {
themes[themeName] = require(`./themes/${themeName}/index.scss`) themes[themeName] = require(`./themes/${themeName}/index.scss`);
} }
/** /**
@ -75,7 +75,6 @@ for (const themeName of ['default', 'forest', 'dark', 'neutral']) {
* @name Configuration * @name Configuration
*/ */
const config = { const config = {
/** theme , the CSS style sheet /** theme , the CSS style sheet
* *
* **theme** - Choose one of the built-in themes: * **theme** - Choose one of the built-in themes:
@ -149,7 +148,6 @@ const config = {
* The object containing configurations specific for sequence diagrams * The object containing configurations specific for sequence diagrams
*/ */
sequence: { sequence: {
/** /**
* margin to the right and left of the sequence diagram. * margin to the right and left of the sequence diagram.
* **Default value 50**. * **Default value 50**.
@ -234,7 +232,6 @@ const config = {
* **Default value false**. * **Default value false**.
*/ */
showSequenceNumbers: false showSequenceNumbers: false
}, },
/** /**
@ -303,100 +300,100 @@ const config = {
}, },
class: {}, class: {},
git: {} git: {}
} };
setLogLevel(config.logLevel) setLogLevel(config.logLevel);
setConfig(config) setConfig(config);
function parse(text) { function parse(text) {
const graphType = utils.detectType(text) const graphType = utils.detectType(text);
let parser let parser;
logger.debug('Type ' + graphType) logger.debug('Type ' + graphType);
switch (graphType) { switch (graphType) {
case 'git': case 'git':
parser = gitGraphParser parser = gitGraphParser;
parser.parser.yy = gitGraphAst parser.parser.yy = gitGraphAst;
break break;
case 'flowchart': case 'flowchart':
parser = flowParser parser = flowParser;
parser.parser.yy = flowDb parser.parser.yy = flowDb;
break break;
case 'sequence': case 'sequence':
parser = sequenceParser parser = sequenceParser;
parser.parser.yy = sequenceDb parser.parser.yy = sequenceDb;
break break;
case 'gantt': case 'gantt':
parser = ganttParser parser = ganttParser;
parser.parser.yy = ganttDb parser.parser.yy = ganttDb;
break break;
case 'class': case 'class':
parser = classParser parser = classParser;
parser.parser.yy = classDb parser.parser.yy = classDb;
break break;
case 'info': case 'info':
logger.debug('info info info') logger.debug('info info info');
console.warn('In API', pkg.version) console.warn('In API', pkg.version);
parser = infoParser parser = infoParser;
parser.parser.yy = infoDb parser.parser.yy = infoDb;
break break;
case 'pie': case 'pie':
logger.debug('pie') logger.debug('pie');
parser = pieParser parser = pieParser;
parser.parser.yy = pieDb parser.parser.yy = pieDb;
break break;
} }
parser.parser.yy.parseError = (str, hash) => { parser.parser.yy.parseError = (str, hash) => {
const error = { str, hash } const error = { str, hash };
throw error throw error;
} };
parser.parse(text) parser.parse(text);
} }
export const encodeEntities = function(text) { export const encodeEntities = function(text) {
let txt = text let txt = text;
txt = txt.replace(/style.*:\S*#.*;/g, function(s) { txt = txt.replace(/style.*:\S*#.*;/g, function(s) {
const innerTxt = s.substring(0, s.length - 1) const innerTxt = s.substring(0, s.length - 1);
return innerTxt return innerTxt;
}) });
txt = txt.replace(/classDef.*:\S*#.*;/g, function(s) { txt = txt.replace(/classDef.*:\S*#.*;/g, function(s) {
const innerTxt = s.substring(0, s.length - 1) const innerTxt = s.substring(0, s.length - 1);
return innerTxt return innerTxt;
}) });
txt = txt.replace(/#\w+;/g, function(s) { txt = txt.replace(/#\w+;/g, function(s) {
const innerTxt = s.substring(1, s.length - 1) const innerTxt = s.substring(1, s.length - 1);
const isInt = /^\+?\d+$/.test(innerTxt) const isInt = /^\+?\d+$/.test(innerTxt);
if (isInt) { if (isInt) {
return 'fl°°' + innerTxt + '¶ß' return 'fl°°' + innerTxt + '¶ß';
} else { } else {
return 'fl°' + innerTxt + '¶ß' return 'fl°' + innerTxt + '¶ß';
} }
}) });
return txt return txt;
} };
export const decodeEntities = function(text) { export const decodeEntities = function(text) {
let txt = text let txt = text;
txt = txt.replace(/fl°°/g, function() { txt = txt.replace(/fl°°/g, function() {
return '&#' return '&#';
}) });
txt = txt.replace(/fl°/g, function() { txt = txt.replace(/fl°/g, function() {
return '&' return '&';
}) });
txt = txt.replace(/¶ß/g, function() { txt = txt.replace(/¶ß/g, function() {
return ';' return ';';
}) });
return txt return txt;
} };
/** /**
* Function that renders an svg with a graph from a chart definition. Usage example below. * Function that renders an svg with a graph from a chart definition. Usage example below.
* *
@ -421,181 +418,207 @@ export const decodeEntities = function (text) {
*/ */
const render = function(id, txt, cb, container) { const render = function(id, txt, cb, container) {
if (typeof container !== 'undefined') { if (typeof container !== 'undefined') {
container.innerHTML = '' container.innerHTML = '';
d3.select(container).append('div') d3.select(container)
.append('div')
.attr('id', 'd' + id) .attr('id', 'd' + id)
.append('svg') .append('svg')
.attr('id', id) .attr('id', id)
.attr('width', '100%') .attr('width', '100%')
.attr('xmlns', 'http://www.w3.org/2000/svg') .attr('xmlns', 'http://www.w3.org/2000/svg')
.append('g') .append('g');
} else { } else {
const element = document.querySelector('#' + 'd' + id) const element = document.querySelector('#' + 'd' + id);
if (element) { if (element) {
element.innerHTML = '' element.innerHTML = '';
} }
d3.select('body').append('div') d3.select('body')
.append('div')
.attr('id', 'd' + id) .attr('id', 'd' + id)
.append('svg') .append('svg')
.attr('id', id) .attr('id', id)
.attr('width', '100%') .attr('width', '100%')
.attr('xmlns', 'http://www.w3.org/2000/svg') .attr('xmlns', 'http://www.w3.org/2000/svg')
.append('g') .append('g');
} }
window.txt = txt window.txt = txt;
txt = encodeEntities(txt) txt = encodeEntities(txt);
const element = d3.select('#d' + id).node() const element = d3.select('#d' + id).node();
const graphType = utils.detectType(txt) const graphType = utils.detectType(txt);
// insert inline style into svg // insert inline style into svg
const svg = element.firstChild const svg = element.firstChild;
const firstChild = svg.firstChild const firstChild = svg.firstChild;
// pre-defined theme // pre-defined theme
let style = themes[config.theme] let style = themes[config.theme];
if (style === undefined) { if (style === undefined) {
style = '' style = '';
} }
// user provided theme CSS // user provided theme CSS
if (config.themeCSS !== undefined) { if (config.themeCSS !== undefined) {
style += `\n${config.themeCSS}` style += `\n${config.themeCSS}`;
} }
// classDef // classDef
if (graphType === 'flowchart') { if (graphType === 'flowchart') {
const classes = flowRenderer.getClasses(txt) const classes = flowRenderer.getClasses(txt);
for (const className in classes) { for (const className in classes) {
style += `\n.${className} > * { ${classes[className].styles.join(' !important; ')} !important; }` style += `\n.${className} > * { ${classes[className].styles.join(
' !important; '
)} !important; }`;
} }
} }
const style1 = document.createElement('style') const style1 = document.createElement('style');
style1.innerHTML = scope(style, `#${id}`) style1.innerHTML = scope(style, `#${id}`);
svg.insertBefore(style1, firstChild) svg.insertBefore(style1, firstChild);
const style2 = document.createElement('style') const style2 = document.createElement('style');
const cs = window.getComputedStyle(svg) const cs = window.getComputedStyle(svg);
style2.innerHTML = `#${id} { style2.innerHTML = `#${id} {
color: ${cs.color}; color: ${cs.color};
font: ${cs.font}; font: ${cs.font};
}` }`;
svg.insertBefore(style2, firstChild) svg.insertBefore(style2, firstChild);
switch (graphType) { switch (graphType) {
case 'git': case 'git':
config.flowchart.arrowMarkerAbsolute = config.arrowMarkerAbsolute config.flowchart.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
gitGraphRenderer.setConf(config.git) gitGraphRenderer.setConf(config.git);
gitGraphRenderer.draw(txt, id, false) gitGraphRenderer.draw(txt, id, false);
break break;
case 'flowchart': case 'flowchart':
config.flowchart.arrowMarkerAbsolute = config.arrowMarkerAbsolute config.flowchart.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
flowRenderer.setConf(config.flowchart) flowRenderer.setConf(config.flowchart);
flowRenderer.draw(txt, id, false) flowRenderer.draw(txt, id, false);
break break;
case 'sequence': case 'sequence':
config.sequence.arrowMarkerAbsolute = config.arrowMarkerAbsolute config.sequence.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
if (config.sequenceDiagram) { // backwards compatibility if (config.sequenceDiagram) {
sequenceRenderer.setConf(Object.assign(config.sequence, config.sequenceDiagram)) // backwards compatibility
console.error('`mermaid config.sequenceDiagram` has been renamed to `config.sequence`. Please update your mermaid config.') sequenceRenderer.setConf(Object.assign(config.sequence, config.sequenceDiagram));
console.error(
'`mermaid config.sequenceDiagram` has been renamed to `config.sequence`. Please update your mermaid config.'
);
} else { } else {
sequenceRenderer.setConf(config.sequence) sequenceRenderer.setConf(config.sequence);
} }
sequenceRenderer.draw(txt, id) sequenceRenderer.draw(txt, id);
break break;
case 'gantt': case 'gantt':
config.gantt.arrowMarkerAbsolute = config.arrowMarkerAbsolute config.gantt.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
ganttRenderer.setConf(config.gantt) ganttRenderer.setConf(config.gantt);
ganttRenderer.draw(txt, id) ganttRenderer.draw(txt, id);
break break;
case 'class': case 'class':
config.class.arrowMarkerAbsolute = config.arrowMarkerAbsolute config.class.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
classRenderer.setConf(config.class) classRenderer.setConf(config.class);
classRenderer.draw(txt, id) classRenderer.draw(txt, id);
break break;
case 'info': case 'info':
config.class.arrowMarkerAbsolute = config.arrowMarkerAbsolute config.class.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
infoRenderer.setConf(config.class) infoRenderer.setConf(config.class);
infoRenderer.draw(txt, id, pkg.version) infoRenderer.draw(txt, id, pkg.version);
break break;
case 'pie': case 'pie':
config.class.arrowMarkerAbsolute = config.arrowMarkerAbsolute config.class.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
pieRenderer.setConf(config.class) pieRenderer.setConf(config.class);
pieRenderer.draw(txt, id, pkg.version) pieRenderer.draw(txt, id, pkg.version);
break break;
} }
d3.select(`[id="${id}"]`).selectAll('foreignobject > *').attr('xmlns', 'http://www.w3.org/1999/xhtml') d3.select(`[id="${id}"]`)
.selectAll('foreignobject > *')
.attr('xmlns', 'http://www.w3.org/1999/xhtml');
let url = '' let url = '';
if (config.arrowMarkerAbsolute) { if (config.arrowMarkerAbsolute) {
url = window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search url =
url = url.replace(/\(/g, '\\(') window.location.protocol +
url = url.replace(/\)/g, '\\)') '//' +
window.location.host +
window.location.pathname +
window.location.search;
url = url.replace(/\(/g, '\\(');
url = url.replace(/\)/g, '\\)');
} }
// Fix for when the base tag is used // Fix for when the base tag is used
let svgCode = d3.select('#d' + id).node().innerHTML.replace(/url\(#arrowhead/g, 'url(' + url + '#arrowhead', 'g') let svgCode = d3
.select('#d' + id)
.node()
.innerHTML.replace(/url\(#arrowhead/g, 'url(' + url + '#arrowhead', 'g');
svgCode = decodeEntities(svgCode) svgCode = decodeEntities(svgCode);
if (typeof cb !== 'undefined') { if (typeof cb !== 'undefined') {
switch (graphType) { switch (graphType) {
case 'flowchart': case 'flowchart':
cb(svgCode, flowDb.bindFunctions) cb(svgCode, flowDb.bindFunctions);
break break;
case 'gantt': case 'gantt':
cb(svgCode, ganttDb.bindFunctions) cb(svgCode, ganttDb.bindFunctions);
break break;
default: default:
cb(svgCode) cb(svgCode);
} }
} else { } else {
logger.debug('CB = undefined!') logger.debug('CB = undefined!');
} }
const node = d3.select('#d' + id).node() const node = d3.select('#d' + id).node();
if (node !== null && typeof node.remove === 'function') { if (node !== null && typeof node.remove === 'function') {
d3.select('#d' + id).node().remove() d3.select('#d' + id)
.node()
.remove();
} }
return svgCode return svgCode;
} };
const setConf = function(cnf) { const setConf = function(cnf) {
// Top level initially mermaid, gflow, sequenceDiagram and gantt // Top level initially mermaid, gflow, sequenceDiagram and gantt
const lvl1Keys = Object.keys(cnf) const lvl1Keys = Object.keys(cnf);
for (let i = 0; i < lvl1Keys.length; i++) { for (let i = 0; i < lvl1Keys.length; i++) {
if (typeof cnf[lvl1Keys[i]] === 'object' && cnf[lvl1Keys[i]] != null) { if (typeof cnf[lvl1Keys[i]] === 'object' && cnf[lvl1Keys[i]] != null) {
const lvl2Keys = Object.keys(cnf[lvl1Keys[i]]) const lvl2Keys = Object.keys(cnf[lvl1Keys[i]]);
for (let j = 0; j < lvl2Keys.length; j++) { for (let j = 0; j < lvl2Keys.length; j++) {
logger.debug('Setting conf ', lvl1Keys[i], '-', lvl2Keys[j]) logger.debug('Setting conf ', lvl1Keys[i], '-', lvl2Keys[j]);
if (typeof config[lvl1Keys[i]] === 'undefined') { if (typeof config[lvl1Keys[i]] === 'undefined') {
config[lvl1Keys[i]] = {} config[lvl1Keys[i]] = {};
} }
logger.debug('Setting config: ' + lvl1Keys[i] + ' ' + lvl2Keys[j] + ' to ' + cnf[lvl1Keys[i]][lvl2Keys[j]]) logger.debug(
config[lvl1Keys[i]][lvl2Keys[j]] = cnf[lvl1Keys[i]][lvl2Keys[j]] 'Setting config: ' +
lvl1Keys[i] +
' ' +
lvl2Keys[j] +
' to ' +
cnf[lvl1Keys[i]][lvl2Keys[j]]
);
config[lvl1Keys[i]][lvl2Keys[j]] = cnf[lvl1Keys[i]][lvl2Keys[j]];
} }
} else { } else {
config[lvl1Keys[i]] = cnf[lvl1Keys[i]] config[lvl1Keys[i]] = cnf[lvl1Keys[i]];
}
} }
} }
};
function initialize(options) { function initialize(options) {
logger.debug('Initializing mermaidAPI ', pkg.version) logger.debug('Initializing mermaidAPI ', pkg.version);
// Update default config with options supplied at initialization // Update default config with options supplied at initialization
if (typeof options === 'object') { if (typeof options === 'object') {
setConf(options) setConf(options);
} }
setConfig(config) setConfig(config);
setLogLevel(config.logLevel) setLogLevel(config.logLevel);
} }
// function getConfig () { // function getConfig () {
@ -608,9 +631,9 @@ const mermaidAPI = {
parse, parse,
initialize, initialize,
getConfig getConfig
} };
export default mermaidAPI export default mermaidAPI;
/** /**
* ## mermaidAPI configuration defaults * ## mermaidAPI configuration defaults
* <pre> * <pre>

View File

@ -1,45 +1,45 @@
/* eslint-env jasmine */ /* eslint-env jasmine */
import mermaidAPI from './mermaidAPI' import mermaidAPI from './mermaidAPI';
describe('when using mermaidAPI and ', function() { describe('when using mermaidAPI and ', function() {
describe('doing initialize ', function() { describe('doing initialize ', function() {
beforeEach(function() { beforeEach(function() {
document.body.innerHTML = '' document.body.innerHTML = '';
}) });
it('should copy a literal into the configuration', function() { it('should copy a literal into the configuration', function() {
const orgConfig = mermaidAPI.getConfig() const orgConfig = mermaidAPI.getConfig();
expect(orgConfig.testLiteral).toBe(undefined) expect(orgConfig.testLiteral).toBe(undefined);
mermaidAPI.initialize({ 'testLiteral': true }) mermaidAPI.initialize({ testLiteral: true });
const config = mermaidAPI.getConfig() const config = mermaidAPI.getConfig();
expect(config.testLiteral).toBe(true) expect(config.testLiteral).toBe(true);
}) });
it('should copy a an object into the configuration', function() { it('should copy a an object into the configuration', function() {
const orgConfig = mermaidAPI.getConfig() const orgConfig = mermaidAPI.getConfig();
expect(orgConfig.testObject).toBe(undefined) expect(orgConfig.testObject).toBe(undefined);
const object = { const object = {
test1: 1, test1: 1,
test2: false test2: false
} };
mermaidAPI.initialize({ 'testObject': object }) mermaidAPI.initialize({ testObject: object });
mermaidAPI.initialize({ 'testObject': { 'test3': true } }) mermaidAPI.initialize({ testObject: { test3: true } });
const config = mermaidAPI.getConfig() const config = mermaidAPI.getConfig();
expect(config.testObject.test1).toBe(1) expect(config.testObject.test1).toBe(1);
expect(config.testObject.test2).toBe(false) expect(config.testObject.test2).toBe(false);
expect(config.testObject.test3).toBe(true) expect(config.testObject.test3).toBe(true);
}) });
}) });
describe('checking validity of input ', function() { describe('checking validity of input ', function() {
it('it should throw for an invalid definiton', function() { it('it should throw for an invalid definiton', function() {
expect(() => mermaidAPI.parse('this is not a mermaid diagram definition')).toThrow() expect(() => mermaidAPI.parse('this is not a mermaid diagram definition')).toThrow();
}) });
it('it should not throw for a valid definiton', function() { it('it should not throw for a valid definiton', function() {
expect(() => mermaidAPI.parse('graph TD;A--x|text including URL space|B;')).not.toThrow() expect(() => mermaidAPI.parse('graph TD;A--x|text including URL space|B;')).not.toThrow();
}) });
}) });
}) });

View File

@ -1,5 +1,5 @@
import * as d3 from 'd3' import * as d3 from 'd3';
import { logger } from './logger' import { logger } from './logger';
/** /**
* @function detectType * @function detectType
@ -19,33 +19,33 @@ import { logger } from './logger'
* @returns {string} A graph definition key * @returns {string} A graph definition key
*/ */
export const detectType = function(text) { export const detectType = function(text) {
text = text.replace(/^\s*%%.*\n/g, '\n') text = text.replace(/^\s*%%.*\n/g, '\n');
logger.debug('Detecting diagram type based on the text ' + text) logger.debug('Detecting diagram type based on the text ' + text);
if (text.match(/^\s*sequenceDiagram/)) { if (text.match(/^\s*sequenceDiagram/)) {
return 'sequence' return 'sequence';
} }
if (text.match(/^\s*gantt/)) { if (text.match(/^\s*gantt/)) {
return 'gantt' return 'gantt';
} }
if (text.match(/^\s*classDiagram/)) { if (text.match(/^\s*classDiagram/)) {
return 'class' return 'class';
} }
if (text.match(/^\s*gitGraph/)) { if (text.match(/^\s*gitGraph/)) {
return 'git' return 'git';
} }
if (text.match(/^\s*info/)) { if (text.match(/^\s*info/)) {
return 'info' return 'info';
} }
if (text.match(/^\s*pie/)) { if (text.match(/^\s*pie/)) {
return 'pie' return 'pie';
} }
return 'flowchart' return 'flowchart';
} };
/** /**
* @function isSubstringInArray * @function isSubstringInArray
@ -56,21 +56,21 @@ export const detectType = function (text) {
**/ **/
export const isSubstringInArray = function(str, arr) { export const isSubstringInArray = function(str, arr) {
for (let i = 0; i < arr.length; i++) { for (let i = 0; i < arr.length; i++) {
if (arr[i].match(str)) return i if (arr[i].match(str)) return i;
}
return -1
} }
return -1;
};
export const interpolateToCurve = (interpolate, defaultCurve) => { export const interpolateToCurve = (interpolate, defaultCurve) => {
if (!interpolate) { if (!interpolate) {
return defaultCurve return defaultCurve;
}
const curveName = `curve${interpolate.charAt(0).toUpperCase() + interpolate.slice(1)}`
return d3[curveName] || defaultCurve
} }
const curveName = `curve${interpolate.charAt(0).toUpperCase() + interpolate.slice(1)}`;
return d3[curveName] || defaultCurve;
};
export default { export default {
detectType, detectType,
isSubstringInArray, isSubstringInArray,
interpolateToCurve interpolateToCurve
} };

View File

@ -1,39 +1,39 @@
/* eslint-env jasmine */ /* eslint-env jasmine */
import utils from './utils' import utils from './utils';
describe('when detecting chart type ', function() { describe('when detecting chart type ', function() {
it('should handle a graph defintion', function() { it('should handle a graph defintion', function() {
const str = 'graph TB\nbfs1:queue' const str = 'graph TB\nbfs1:queue';
const type = utils.detectType(str) const type = utils.detectType(str);
expect(type).toBe('flowchart') expect(type).toBe('flowchart');
}) });
it('should handle a graph defintion with leading spaces', function() { it('should handle a graph defintion with leading spaces', function() {
const str = ' graph TB\nbfs1:queue' const str = ' graph TB\nbfs1:queue';
const type = utils.detectType(str) const type = utils.detectType(str);
expect(type).toBe('flowchart') expect(type).toBe('flowchart');
}) });
it('should handle a graph defintion with leading spaces and newline', function() { it('should handle a graph defintion with leading spaces and newline', function() {
const str = ' \n graph TB\nbfs1:queue' const str = ' \n graph TB\nbfs1:queue';
const type = utils.detectType(str) const type = utils.detectType(str);
expect(type).toBe('flowchart') expect(type).toBe('flowchart');
}) });
it('should handle a graph defintion for gitGraph', function() { it('should handle a graph defintion for gitGraph', function() {
const str = ' \n gitGraph TB:\nbfs1:queue' const str = ' \n gitGraph TB:\nbfs1:queue';
const type = utils.detectType(str) const type = utils.detectType(str);
expect(type).toBe('git') expect(type).toBe('git');
}) });
}) });
describe('when finding substring in array ', function() { describe('when finding substring in array ', function() {
it('should return the array index that contains the substring', function() { it('should return the array index that contains the substring', function() {
const arr = ['stroke:val1', 'fill:val2'] const arr = ['stroke:val1', 'fill:val2'];
const result = utils.isSubstringInArray('fill', arr) const result = utils.isSubstringInArray('fill', arr);
expect(result).toEqual(1) expect(result).toEqual(1);
}) });
it('should return -1 if the substring is not found in the array', function() { it('should return -1 if the substring is not found in the array', function() {
const arr = ['stroke:val1', 'stroke-width:val2'] const arr = ['stroke:val1', 'stroke-width:val2'];
const result = utils.isSubstringInArray('fill', arr) const result = utils.isSubstringInArray('fill', arr);
expect(result).toEqual(-1) expect(result).toEqual(-1);
}) });
}) });

273
yarn.lock
View File

@ -1528,6 +1528,11 @@ acorn-jsx@^5.0.0:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.1.tgz#32a064fd925429216a09b141102bfdd185fae40e" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.1.tgz#32a064fd925429216a09b141102bfdd185fae40e"
acorn-jsx@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.2.tgz#84b68ea44b373c4f8686023a551f61a21b7c4a4f"
integrity sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw==
acorn-walk@^6.0.1: acorn-walk@^6.0.1:
version "6.1.1" version "6.1.1"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.1.tgz#d363b66f5fac5f018ff9c3a1e7b6f8e310cc3913" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.1.tgz#d363b66f5fac5f018ff9c3a1e7b6f8e310cc3913"
@ -1540,6 +1545,11 @@ acorn@^6.0.1, acorn@^6.0.2:
version "6.0.4" version "6.0.4"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.0.4.tgz#77377e7353b72ec5104550aa2d2097a2fd40b754" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.0.4.tgz#77377e7353b72ec5104550aa2d2097a2fd40b754"
acorn@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.0.0.tgz#26b8d1cd9a9b700350b71c0905546f64d1284e7a"
integrity sha512-PaF/MduxijYYt7unVGRuds1vBC9bFxbNf+VWqhOClfdgy7RlVkQqt610ig1/yxTgsDIfW1cWDel5EBbOy3jdtQ==
agent-base@^4.1.0: agent-base@^4.1.0:
version "4.2.1" version "4.2.1"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9"
@ -1575,6 +1585,16 @@ ajv@^6.1.0:
fast-json-stable-stringify "^2.0.0" fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.3.0" json-schema-traverse "^0.3.0"
ajv@^6.10.0, ajv@^6.10.2:
version "6.10.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52"
integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==
dependencies:
fast-deep-equal "^2.0.1"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
amdefine@>=0.0.4: amdefine@>=0.0.4:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
@ -1591,9 +1611,10 @@ ansi-escapes@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.0.0.tgz#ec3e8b4e9f8064fc02c3ac9b65f1c275bda8ef92" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.0.0.tgz#ec3e8b4e9f8064fc02c3ac9b65f1c275bda8ef92"
ansi-escapes@^3.1.0: ansi-escapes@^3.1.0, ansi-escapes@^3.2.0:
version "3.2.0" version "3.2.0"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==
ansi-html@0.0.7, ansi-html@^0.0.7: ansi-html@0.0.7, ansi-html@^0.0.7:
version "0.0.7" version "0.0.7"
@ -2383,6 +2404,11 @@ callsites@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50"
callsites@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
camel-case@3.0.x: camel-case@3.0.x:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73" resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73"
@ -2504,6 +2530,11 @@ chardet@^0.4.0:
version "0.4.2" version "0.4.2"
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2"
chardet@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
check-more-types@2.24.0: check-more-types@2.24.0:
version "2.24.0" version "2.24.0"
resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600"
@ -3394,9 +3425,10 @@ debug@3.2.6, debug@^3.2.5, debug@^3.2.6:
dependencies: dependencies:
ms "^2.1.1" ms "^2.1.1"
debug@4.1.1, debug@^4.1.1: debug@4.1.1, debug@^4.0.1, debug@^4.1.1:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
dependencies: dependencies:
ms "^2.1.1" ms "^2.1.1"
@ -3627,6 +3659,13 @@ doctrine@^2.1.0:
dependencies: dependencies:
esutils "^2.0.2" esutils "^2.0.2"
doctrine@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
dependencies:
esutils "^2.0.2"
documentation@^12.0.1: documentation@^12.0.1:
version "12.0.1" version "12.0.1"
resolved "https://registry.yarnpkg.com/documentation/-/documentation-12.0.1.tgz#4abe263d5415f3ed7ee737829921ef6159e6a335" resolved "https://registry.yarnpkg.com/documentation/-/documentation-12.0.1.tgz#4abe263d5415f3ed7ee737829921ef6159e6a335"
@ -3899,6 +3938,13 @@ escodegen@^1.9.1:
optionalDependencies: optionalDependencies:
source-map "~0.6.1" source-map "~0.6.1"
eslint-config-prettier@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.3.0.tgz#e73b48e59dc49d950843f3eb96d519e2248286a3"
integrity sha512-EWaGjlDAZRzVFveh2Jsglcere2KK5CJBhkNSa1xs3KfMUGdRiT7lG089eqPdvlzWHpAqaekubOsOMu8W8Yk71A==
dependencies:
get-stdin "^6.0.0"
eslint-config-standard-jsx@6.0.2: eslint-config-standard-jsx@6.0.2:
version "6.0.2" version "6.0.2"
resolved "https://registry.yarnpkg.com/eslint-config-standard-jsx/-/eslint-config-standard-jsx-6.0.2.tgz#90c9aa16ac2c4f8970c13fc7efc608bacd02da70" resolved "https://registry.yarnpkg.com/eslint-config-standard-jsx/-/eslint-config-standard-jsx-6.0.2.tgz#90c9aa16ac2c4f8970c13fc7efc608bacd02da70"
@ -3954,6 +4000,13 @@ eslint-plugin-node@~7.0.1:
resolve "^1.8.1" resolve "^1.8.1"
semver "^5.5.0" semver "^5.5.0"
eslint-plugin-prettier@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.0.tgz#8695188f95daa93b0dc54b249347ca3b79c4686d"
integrity sha512-XWX2yVuwVNLOUhQijAkXz+rMPPoCr7WFiAl8ig6I7Xn+pPVhDhzg4DxHpmbeb0iqjO9UronEA3Tb09ChnFVHHA==
dependencies:
prettier-linter-helpers "^1.0.0"
eslint-plugin-promise@~4.0.0: eslint-plugin-promise@~4.0.0:
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.0.1.tgz#2d074b653f35a23d1ba89d8e976a985117d1c6a2" resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.0.1.tgz#2d074b653f35a23d1ba89d8e976a985117d1c6a2"
@ -3979,14 +4032,77 @@ eslint-scope@^4.0.0:
esrecurse "^4.1.0" esrecurse "^4.1.0"
estraverse "^4.1.1" estraverse "^4.1.1"
eslint-scope@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9"
integrity sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==
dependencies:
esrecurse "^4.1.0"
estraverse "^4.1.1"
eslint-utils@^1.3.0, eslint-utils@^1.3.1: eslint-utils@^1.3.0, eslint-utils@^1.3.1:
version "1.3.1" version "1.3.1"
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.3.1.tgz#9a851ba89ee7c460346f97cf8939c7298827e512" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.3.1.tgz#9a851ba89ee7c460346f97cf8939c7298827e512"
eslint-utils@^1.4.2:
version "1.4.2"
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab"
integrity sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==
dependencies:
eslint-visitor-keys "^1.0.0"
eslint-visitor-keys@^1.0.0: eslint-visitor-keys@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
eslint-visitor-keys@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2"
integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==
eslint@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.3.0.tgz#1f1a902f67bfd4c354e7288b81e40654d927eb6a"
integrity sha512-ZvZTKaqDue+N8Y9g0kp6UPZtS4FSY3qARxBs7p4f0H0iof381XHduqVerFWtK8DPtKmemqbqCFENWSQgPR/Gow==
dependencies:
"@babel/code-frame" "^7.0.0"
ajv "^6.10.0"
chalk "^2.1.0"
cross-spawn "^6.0.5"
debug "^4.0.1"
doctrine "^3.0.0"
eslint-scope "^5.0.0"
eslint-utils "^1.4.2"
eslint-visitor-keys "^1.1.0"
espree "^6.1.1"
esquery "^1.0.1"
esutils "^2.0.2"
file-entry-cache "^5.0.1"
functional-red-black-tree "^1.0.1"
glob-parent "^5.0.0"
globals "^11.7.0"
ignore "^4.0.6"
import-fresh "^3.0.0"
imurmurhash "^0.1.4"
inquirer "^6.4.1"
is-glob "^4.0.0"
js-yaml "^3.13.1"
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.3.0"
lodash "^4.17.14"
minimatch "^3.0.4"
mkdirp "^0.5.1"
natural-compare "^1.4.0"
optionator "^0.8.2"
progress "^2.0.0"
regexpp "^2.0.1"
semver "^6.1.2"
strip-ansi "^5.2.0"
strip-json-comments "^3.0.1"
table "^5.2.3"
text-table "^0.2.0"
v8-compile-cache "^2.0.3"
eslint@~5.4.0: eslint@~5.4.0:
version "5.4.0" version "5.4.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.4.0.tgz#d068ec03006bb9e06b429dc85f7e46c1b69fac62" resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.4.0.tgz#d068ec03006bb9e06b429dc85f7e46c1b69fac62"
@ -4038,6 +4154,15 @@ espree@^4.0.0:
acorn-jsx "^5.0.0" acorn-jsx "^5.0.0"
eslint-visitor-keys "^1.0.0" eslint-visitor-keys "^1.0.0"
espree@^6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.1.tgz#7f80e5f7257fc47db450022d723e356daeb1e5de"
integrity sha512-EYbr8XZUhWbYCqQRW0duU5LxzL5bETN6AjKBGy1302qqzPaCH10QbRg3Wvco79Z8x9WbiE8HYB4e75xl6qUYvQ==
dependencies:
acorn "^7.0.0"
acorn-jsx "^5.0.2"
eslint-visitor-keys "^1.1.0"
esprima@1.1.x, esprima@~1.1.1: esprima@1.1.x, esprima@~1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-1.1.1.tgz#5b6f1547f4d102e670e140c509be6771d6aeb549" resolved "https://registry.yarnpkg.com/esprima/-/esprima-1.1.1.tgz#5b6f1547f4d102e670e140c509be6771d6aeb549"
@ -4289,6 +4414,15 @@ external-editor@^2.1.0:
iconv-lite "^0.4.17" iconv-lite "^0.4.17"
tmp "^0.0.33" tmp "^0.0.33"
external-editor@^3.0.3:
version "3.1.0"
resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495"
integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==
dependencies:
chardet "^0.7.0"
iconv-lite "^0.4.24"
tmp "^0.0.33"
extglob@^0.3.1: extglob@^0.3.1:
version "0.3.2" version "0.3.2"
resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
@ -4337,6 +4471,11 @@ fast-deep-equal@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
fast-diff@^1.1.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
fast-glob@^3.0.3: fast-glob@^3.0.3:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.0.4.tgz#d484a41005cb6faeb399b951fd1bd70ddaebb602" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.0.4.tgz#d484a41005cb6faeb399b951fd1bd70ddaebb602"
@ -4420,6 +4559,13 @@ file-entry-cache@^2.0.0:
flat-cache "^1.2.1" flat-cache "^1.2.1"
object-assign "^4.0.1" object-assign "^4.0.1"
file-entry-cache@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c"
integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==
dependencies:
flat-cache "^2.0.1"
filename-regex@^2.0.0: filename-regex@^2.0.0:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"
@ -4537,6 +4683,20 @@ flat-cache@^1.2.1:
rimraf "~2.6.2" rimraf "~2.6.2"
write "^0.2.1" write "^0.2.1"
flat-cache@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0"
integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==
dependencies:
flatted "^2.0.0"
rimraf "2.6.3"
write "1.0.3"
flatted@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08"
integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==
flush-write-stream@^1.0.0, flush-write-stream@^1.0.2: flush-write-stream@^1.0.0, flush-write-stream@^1.0.2:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.3.tgz#c5d586ef38af6097650b49bc41b55fabb19f35bd" resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.3.tgz#c5d586ef38af6097650b49bc41b55fabb19f35bd"
@ -5260,7 +5420,7 @@ hyperlinker@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/hyperlinker/-/hyperlinker-1.0.0.tgz#23dc9e38a206b208ee49bc2d6c8ef47027df0c0e" resolved "https://registry.yarnpkg.com/hyperlinker/-/hyperlinker-1.0.0.tgz#23dc9e38a206b208ee49bc2d6c8ef47027df0c0e"
iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.4: iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.24, iconv-lite@^0.4.4:
version "0.4.24" version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
dependencies: dependencies:
@ -5300,9 +5460,10 @@ ignore@^3.0.9:
version "3.3.10" version "3.3.10"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043"
ignore@^4.0.2: ignore@^4.0.2, ignore@^4.0.6:
version "4.0.6" version "4.0.6"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
ignore@^5.1.1: ignore@^5.1.1:
version "5.1.4" version "5.1.4"
@ -5315,6 +5476,14 @@ import-fresh@^2.0.0:
caller-path "^2.0.0" caller-path "^2.0.0"
resolve-from "^3.0.0" resolve-from "^3.0.0"
import-fresh@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.1.0.tgz#6d33fa1dcef6df930fae003446f33415af905118"
integrity sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ==
dependencies:
parent-module "^1.0.0"
resolve-from "^4.0.0"
import-local@^1.0.0: import-local@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/import-local/-/import-local-1.0.0.tgz#5e4ffdc03f4fe6c009c6729beb29631c2f8227bc" resolved "https://registry.yarnpkg.com/import-local/-/import-local-1.0.0.tgz#5e4ffdc03f4fe6c009c6729beb29631c2f8227bc"
@ -5396,6 +5565,25 @@ inquirer@^5.2.0:
strip-ansi "^4.0.0" strip-ansi "^4.0.0"
through "^2.3.6" through "^2.3.6"
inquirer@^6.4.1:
version "6.5.2"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca"
integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==
dependencies:
ansi-escapes "^3.2.0"
chalk "^2.4.2"
cli-cursor "^2.1.0"
cli-width "^2.0.0"
external-editor "^3.0.3"
figures "^2.0.0"
lodash "^4.17.12"
mute-stream "0.0.7"
run-async "^2.2.0"
rxjs "^6.4.0"
string-width "^2.1.0"
strip-ansi "^5.1.0"
through "^2.3.6"
internal-ip@^4.3.0: internal-ip@^4.3.0:
version "4.3.0" version "4.3.0"
resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907"
@ -6661,9 +6849,10 @@ lodash@^4.0.0, lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4,
version "4.17.13" version "4.17.13"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.13.tgz#0bdc3a6adc873d2f4e0c4bac285df91b64fc7b93" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.13.tgz#0bdc3a6adc873d2f4e0c4bac285df91b64fc7b93"
lodash@^4.17.13: lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14:
version "4.17.15" version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
log-driver@^1.2.7: log-driver@^1.2.7:
version "1.2.7" version "1.2.7"
@ -7699,6 +7888,13 @@ param-case@2.1.x:
dependencies: dependencies:
no-case "^2.2.0" no-case "^2.2.0"
parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
dependencies:
callsites "^3.0.0"
parse-asn1@^5.0.0: parse-asn1@^5.0.0:
version "5.1.1" version "5.1.1"
resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.1.tgz#f6bf293818332bd0dab54efb16087724745e6ca8" resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.1.tgz#f6bf293818332bd0dab54efb16087724745e6ca8"
@ -8062,6 +8258,18 @@ preserve@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
prettier-linter-helpers@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
dependencies:
fast-diff "^1.1.2"
prettier@^1.18.2:
version "1.18.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea"
integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==
pretty-format@^23.6.0: pretty-format@^23.6.0:
version "23.6.0" version "23.6.0"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-23.6.0.tgz#5eaac8eeb6b33b987b7fe6097ea6a8a146ab5760" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-23.6.0.tgz#5eaac8eeb6b33b987b7fe6097ea6a8a146ab5760"
@ -8804,6 +9012,11 @@ resolve-from@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
resolve-from@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
resolve-options@^1.1.0: resolve-options@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/resolve-options/-/resolve-options-1.1.0.tgz#32bb9e39c06d67338dc9378c0d6d6074566ad131" resolved "https://registry.yarnpkg.com/resolve-options/-/resolve-options-1.1.0.tgz#32bb9e39c06d67338dc9378c0d6d6074566ad131"
@ -8856,7 +9069,7 @@ reusify@^1.0.0:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
rimraf@2, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3, rimraf@~2.6.2: rimraf@2, rimraf@2.6.3, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3, rimraf@~2.6.2:
version "2.6.3" version "2.6.3"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
dependencies: dependencies:
@ -8907,6 +9120,13 @@ rxjs@^5.0.0-beta.11, rxjs@^5.5.2:
dependencies: dependencies:
symbol-observable "1.0.1" symbol-observable "1.0.1"
rxjs@^6.4.0:
version "6.5.3"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.3.tgz#510e26317f4db91a7eb1de77d9dd9ba0a4899a3a"
integrity sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==
dependencies:
tslib "^1.9.0"
safe-buffer@5.1.2, safe-buffer@^5.1.1, safe-buffer@^5.1.2: safe-buffer@5.1.2, safe-buffer@^5.1.1, safe-buffer@^5.1.2:
version "5.1.2" version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
@ -9036,6 +9256,11 @@ semver@^6.1.1:
version "6.2.0" version "6.2.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.2.0.tgz#4d813d9590aaf8a9192693d6c85b9344de5901db" resolved "https://registry.yarnpkg.com/semver/-/semver-6.2.0.tgz#4d813d9590aaf8a9192693d6c85b9344de5901db"
semver@^6.1.2:
version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
semver@~5.3.0: semver@~5.3.0:
version "5.3.0" version "5.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
@ -9199,6 +9424,15 @@ slice-ansi@1.0.0:
dependencies: dependencies:
is-fullwidth-code-point "^2.0.0" is-fullwidth-code-point "^2.0.0"
slice-ansi@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"
integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==
dependencies:
ansi-styles "^3.2.0"
astral-regex "^1.0.0"
is-fullwidth-code-point "^2.0.0"
slugify@^1.3.1: slugify@^1.3.1:
version "1.3.4" version "1.3.4"
resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.3.4.tgz#78d2792d7222b55cd9fc81fa018df99af779efeb" resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.3.4.tgz#78d2792d7222b55cd9fc81fa018df99af779efeb"
@ -9648,6 +9882,11 @@ strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
strip-json-comments@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7"
integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==
subarg@^1.0.0: subarg@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2" resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2"
@ -9708,6 +9947,16 @@ table@^4.0.3:
slice-ansi "1.0.0" slice-ansi "1.0.0"
string-width "^2.1.1" string-width "^2.1.1"
table@^5.2.3:
version "5.4.6"
resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==
dependencies:
ajv "^6.10.2"
lodash "^4.17.14"
slice-ansi "^2.1.0"
string-width "^3.0.0"
tapable@^1.0.0: tapable@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.0.0.tgz#cbb639d9002eed9c6b5975eb20598d7936f1f9f2" resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.0.0.tgz#cbb639d9002eed9c6b5975eb20598d7936f1f9f2"
@ -10253,6 +10502,11 @@ v8-compile-cache@^2.0.2:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.2.tgz#a428b28bb26790734c4fc8bc9fa106fccebf6a6c" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.2.tgz#a428b28bb26790734c4fc8bc9fa106fccebf6a6c"
v8-compile-cache@^2.0.3:
version "2.1.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e"
integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==
validate-npm-package-license@^3.0.1: validate-npm-package-license@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc"
@ -10687,6 +10941,13 @@ write-file-atomic@^2.1.0:
imurmurhash "^0.1.4" imurmurhash "^0.1.4"
signal-exit "^3.0.2" signal-exit "^3.0.2"
write@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3"
integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==
dependencies:
mkdirp "^0.5.1"
write@^0.2.1: write@^0.2.1:
version "0.2.1" version "0.2.1"
resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757"