Merge branch 'develop' into kh-user-journey

This commit is contained in:
Knut Sveidqvist
2022-04-12 07:36:33 +02:00
committed by GitHub
73 changed files with 3726 additions and 2119 deletions

View File

@@ -345,8 +345,34 @@ const setDirection = (dir) => {
direction = dir;
};
let title = '';
export const setTitle = function (txt) {
let sanitizedText = sanitizeText(txt, configApi.getConfig());
title = sanitizedText;
};
export const getTitle = function () {
return title;
};
let accDescription = '';
export const setAccDescription = function (txt) {
let sanitizedText = sanitizeText(txt, configApi.getConfig());
accDescription = sanitizedText;
};
export const getAccDescription = function () {
return accDescription;
};
export default {
parseDirective,
setTitle,
getTitle,
getAccDescription,
setAccDescription,
getConfig: () => configApi.getConfig().class,
addClass,
bindFunctions,

View File

@@ -543,6 +543,21 @@ foo()
expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE);
});
it('should have a title and accDescription', function () {
const str =
'classDiagram\n' +
'class Car~T~\n' +
'title My Title\n' +
'accDescription My Description\n';
'Car : -List~Wheel~ wheels\n' +
'Car : +setWheels(List~Wheel~ wheels)\n' +
'Car : +getWheels() List~Wheel~';
parser.parse(str);
expect(parser.yy.getTitle()).toBe('My Title');
expect(parser.yy.getAccDescription()).toBe('My Description');
});
it('should handle relation definitions AGGREGATION and dotted line', function () {
const str = 'classDiagram\n' + 'Class01 o.. Class02';

View File

@@ -11,6 +11,7 @@ import { render } from '../../dagre-wrapper/index.js';
import { curveLinear } from 'd3';
import { interpolateToCurve, getStylesFromArray, configureSvgSize } from '../../utils';
import common from '../common/common';
import addSVGAccessibilityFields from '../../accessibility';
parser.yy = classDb;
@@ -397,6 +398,7 @@ export const draw = function (text, id) {
}
}
addSVGAccessibilityFields(parser.yy, svg, id);
// If node has a link, wrap it in an anchor SVG object.
// const keys = Object.keys(classes);
// keys.forEach(function(key) {

View File

@@ -7,6 +7,7 @@ import { parser } from './parser/classDiagram';
import svgDraw from './svgDraw';
import { configureSvgSize } from '../../utils';
import { getConfig } from '../../config';
import addSVGAccessibilityFields from '../../accessibility';
parser.yy = classDb;
@@ -263,6 +264,7 @@ export const draw = function (text, id) {
const vBox = `${svgBounds.x - padding} ${svgBounds.y - padding} ${width} ${height}`;
log.debug(`viewBox ${vBox}`);
diagram.attr('viewBox', vBox);
addSVGAccessibilityFields(parser.yy, diagram, id);
};
export default {

View File

@@ -29,6 +29,8 @@
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
\%\%(?!\{)*[^\n]*(\r?\n?)+ /* skip comments */
\%\%[^\n]*(\r?\n)* /* skip comments */
"title"\s[^#\n;]+ return 'title';
"accDescription"\s[^#\n;]+ return 'accDescription';
\s*(\r?\n)+ return 'NEWLINE';
\s+ /* skip whitespace */
@@ -185,6 +187,7 @@ Function arguments are optional: 'call <callback_name>()' simply executes 'callb
start
: mermaidDoc
| statments
| direction
| directive start
;
@@ -253,6 +256,9 @@ statement
| cssClassStatement
| directive
| direction
| title {yy.setTitle($1.substring(6));$$=$1.substring(6);}
| accDescription {yy.setAccDescription($1.substring(15));$$=$1.substring(15);}
;
classStatement

View File

@@ -1,6 +1,7 @@
import { log } from '../../logger';
import mermaidAPI from '../../mermaidAPI';
import * as configApi from '../../config';
import common from '../common/common';
let entities = {};
let relationships = [];
@@ -69,7 +70,8 @@ const getRelationships = () => relationships;
// Keep this - TODO: revisit...allow the diagram to have a title
const setTitle = function (txt) {
title = txt;
let sanitizedText = common.sanitizeText(txt, configApi.getConfig());
title = sanitizedText;
};
const getTitle = function () {
@@ -77,7 +79,8 @@ const getTitle = function () {
};
const setAccDescription = function (txt) {
description = txt;
let sanitizedText = common.sanitizeText(txt, configApi.getConfig());
description = sanitizedText;
};
const getAccDescription = function () {

View File

@@ -17,16 +17,36 @@ let tooltips = {};
let subCount = 0;
let firstGraphFlag = true;
let direction;
let title = 'Flow chart';
let description = '';
let version; // As in graph
// Functions to be run after graph rendering
let funs = [];
const sanitizeText = (txt) => common.sanitizeText(txt, config);
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
const setTitle = function (txt) {
title = sanitizeText(txt);
};
const getTitle = function () {
return title;
};
const setAccDescription = function (txt) {
description = sanitizeText(txt);
};
const getAccDescription = function () {
return description;
};
/**
* Function to lookup domId from id in the graph definition.
*
@@ -77,7 +97,7 @@ export const addVertex = function (_id, text, type, style, classes, dir, props =
vertexCounter++;
if (typeof text !== 'undefined') {
config = configApi.getConfig();
txt = common.sanitizeText(text.trim(), config);
txt = sanitizeText(text.trim());
// strip quotes if string starts and ends with a quote
if (txt[0] === '"' && txt[txt.length - 1] === '"') {
@@ -132,7 +152,7 @@ export const addSingleLink = function (_start, _end, type, linktext) {
linktext = type.text;
if (typeof linktext !== 'undefined') {
edge.text = common.sanitizeText(linktext.trim(), config);
edge.text = sanitizeText(linktext.trim());
// strip quotes if string starts and exnds with a quote
if (edge.text[0] === '"' && edge.text[edge.text.length - 1] === '"') {
@@ -255,7 +275,7 @@ export const setClass = function (ids, className) {
const setTooltip = function (ids, tooltip) {
ids.split(',').forEach(function (id) {
if (typeof tooltip !== 'undefined') {
tooltips[version === 'gen-1' ? lookUpDomId(id) : id] = common.sanitizeText(tooltip, config);
tooltips[version === 'gen-1' ? lookUpDomId(id) : id] = sanitizeText(tooltip);
}
});
};
@@ -488,7 +508,7 @@ export const addSubGraph = function (_id, list, _title) {
id = id || 'subGraph' + subCount;
// if (id[0].match(/\d/)) id = lookUpDomId(id);
title = title || '';
title = common.sanitizeText(title, config);
title = sanitizeText(title);
subCount = subCount + 1;
const subGraph = { id: id, nodes: nodeList, title: title.trim(), classes: [], dir };
@@ -736,6 +756,10 @@ const makeUniq = (sg, allSubgraphs) => {
export default {
parseDirective,
defaultConfig: () => configApi.defaultConfig.flowchart,
setTitle,
getTitle,
getAccDescription,
setAccDescription,
addVertex,
lookUpDomId,
addLink,

View File

@@ -10,6 +10,7 @@ import addHtmlLabel from 'dagre-d3/lib/label/add-html-label.js';
import { log } from '../../logger';
import common, { evaluate } from '../common/common';
import { interpolateToCurve, getStylesFromArray, configureSvgSize } from '../../utils';
import addSVGAccessibilityFields from '../../accessibility';
const conf = {};
export const setConf = function (cnf) {
@@ -181,7 +182,7 @@ export const addVertices = function (vert, g, svgId, root, doc) {
};
/**
* Add edges to graph based on parsed graph defninition
* Add edges to graph based on parsed graph definition
*
* @param {object} edges The edges to add to the graph
* @param {object} g The graph object
@@ -380,7 +381,7 @@ export const draw = function (text, id) {
const rankSpacing = conf.rankSpacing || 50;
const securityLevel = getConfig().securityLevel;
// Handle root and ocument for when rendering in sanbox mode
// Handle root and document for when rendering in sandbox mode
let sandboxElement;
if (securityLevel === 'sandbox') {
sandboxElement = select('#i' + id);
@@ -416,7 +417,7 @@ export const draw = function (text, id) {
flowDb.addVertex(subG.id, subG.title, 'group', undefined, subG.classes, subG.dir);
}
// Fetch the verices/nodes and edges/links from the parsed graph definition
// Fetch the vertices/nodes and edges/links from the parsed graph definition
const vert = flowDb.getVertices();
const edges = flowDb.getEdges();
@@ -444,6 +445,9 @@ export const draw = function (text, id) {
const svg = root.select(`[id="${id}"]`);
svg.attr('xmlns:xlink', 'http://www.w3.org/1999/xlink');
// Adds title and description to the flow chart
addSVGAccessibilityFields(parser.yy, svg, id);
// Run the renderer. This is what draws the final graph.
const element = root.select('#' + id + ' g');
render(element, g, ['point', 'circle', 'cross'], 'flowchart', id);

View File

@@ -11,6 +11,7 @@ import { log } from '../../logger';
import common, { evaluate } from '../common/common';
import { interpolateToCurve, getStylesFromArray, configureSvgSize } from '../../utils';
import flowChartShapes from './flowChartShapes';
import addSVGAccessibilityFields from '../../accessibility';
const conf = {};
export const setConf = function (cnf) {
@@ -158,7 +159,7 @@ export const addVertices = function (vert, g, svgId, root, _doc) {
};
/**
* Add edges to graph based on parsed graph defninition
* Add edges to graph based on parsed graph definition
*
* @param {object} edges The edges to add to the graph
* @param {object} g The graph object
@@ -350,7 +351,7 @@ export const draw = function (text, id) {
flowDb.addVertex(subG.id, subG.title, 'group', undefined, subG.classes);
}
// Fetch the verices/nodes and edges/links from the parsed graph definition
// Fetch the vertices/nodes and edges/links from the parsed graph definition
const vert = flowDb.getVertices();
log.warn('Get vertices', vert);
@@ -426,6 +427,9 @@ export const draw = function (text, id) {
log.warn(g);
// Adds title and description to the flow chart
addSVGAccessibilityFields(parser.yy, svg, id);
// Run the renderer. This is what draws the final graph.
const element = root.select('#' + id + ' g');
render(element, g);

View File

@@ -7,6 +7,8 @@
/* lexical grammar */
%lex
%x string
%x title
%x accDescription
%x dir
%x vertex
%x click
@@ -26,6 +28,10 @@
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
\%\%(?!\{)[^\n]* /* skip comments */
[^\}]\%\%[^\n]* /* skip comments */
title { this.begin("title");return 'title'; }
<title>(?!\n|;|#)*[^\n]* { this.popState(); return "title_value"; }
accDescription { this.begin("accDescription");return 'accDescription'; }
<accDescription>(?!\n|;|#)*[^\n]* { this.popState(); return "description_value"; }
["] this.begin("string");
<string>["] this.popState();
<string>[^"]* return "STR";
@@ -337,6 +343,8 @@ statement
| subgraph separator document end
{$$=yy.addSubGraph(undefined,$3,undefined);}
| direction
| title title_value { $$=$2.trim();yy.setTitle($$); }
| accDescription description_value { $$=$2.trim();yy.setAccDescription($$); }
;
separator: NEWLINE | SEMI | EOF ;

View File

@@ -6,13 +6,13 @@ setConfig({
securityLevel: 'strict',
});
describe('when parsing ', function () {
describe('parsing a flow chart', function () {
beforeEach(function () {
flow.parser.yy = flowDb;
flow.parser.yy.clear();
});
it('it should handle a trailing whitespaces after statememnts', function () {
it('should handle a trailing whitespaces after statememnts', function () {
const res = flow.parser.parse('graph TD;\n\n\n %% Comment\n A-->B; \n B-->C;');
const vert = flow.parser.yy.getVertices();
@@ -80,47 +80,47 @@ describe('when parsing ', function () {
flow.parser.yy.clear();
};
it("it should be able to parse a '.'", function () {
it("should be able to parse a '.'", function () {
charTest('.');
charTest('Start 103a.a1');
});
// it('it should be able to parse text containing \'_\'', function () {
// it('should be able to parse text containing \'_\'', function () {
// charTest('_')
// })
it("it should be able to parse a ':'", function () {
it("should be able to parse a ':'", function () {
charTest(':');
});
it("it should be able to parse a ','", function () {
it("should be able to parse a ','", function () {
charTest(',');
});
it("it should be able to parse text containing '-'", function () {
it("should be able to parse text containing '-'", function () {
charTest('a-b');
});
it("it should be able to parse a '+'", function () {
it("should be able to parse a '+'", function () {
charTest('+');
});
it("it should be able to parse a '*'", function () {
it("should be able to parse a '*'", function () {
charTest('*');
});
it("it should be able to parse a '<'", function () {
it("should be able to parse a '<'", function () {
charTest('<', '&lt;');
});
// it("it should be able to parse a '>'", function() {
// it("should be able to parse a '>'", function() {
// charTest('>', '&gt;');
// });
// it("it should be able to parse a '='", function() {
// it("should be able to parse a '='", function() {
// charTest('=', '&equals;');
// });
it("it should be able to parse a '&'", function () {
it("should be able to parse a '&'", function () {
charTest('&');
});
});
@@ -146,6 +146,7 @@ describe('when parsing ', function () {
const classes = flow.parser.yy.getClasses();
expect(vertices['A'].id).toBe('A');
});
it('should be possible to use numbers as labels', function () {
let statement = '';
@@ -155,4 +156,19 @@ describe('when parsing ', function () {
const classes = flow.parser.yy.getClasses();
expect(vertices['1'].id).toBe('1');
});
it('should add title and description to flow chart', function () {
const flowChart = `graph LR
title Big decisions
accDescription Flow chart of the decision making process
A[Hard] -->|Text| B(Round)
B --> C{Decision}
C -->|One| D[Result 1]
C -->|Two| E[Result 2]
`;
flow.parser.parse(flowChart);
expect(flow.parser.yy.getTitle()).toBe('Big decisions');
expect(flow.parser.yy.getAccDescription()).toBe('Flow chart of the decision making process');
});
});

View File

@@ -4,6 +4,7 @@ import { log } from '../../logger';
import * as configApi from '../../config';
import utils from '../../utils';
import mermaidAPI from '../../mermaidAPI';
import common from '../common/common';
let dateFormat = '';
let axisFormat = '';
@@ -12,6 +13,7 @@ let includes = [];
let excludes = [];
let links = {};
let title = '';
let accDescription = '';
let sections = [];
let tasks = [];
let currentSection = '';
@@ -23,6 +25,10 @@ let topAxis = false;
// The serial order of the task in the script
let lastOrder = 0;
const sanitizeText = function (txt) {
return common.sanitizeText(txt, configApi.getConfig());
};
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
@@ -108,13 +114,21 @@ export const getLinks = function () {
};
export const setTitle = function (txt) {
title = txt;
title = sanitizeText(txt);
};
export const getTitle = function () {
return title;
};
export const setAccDescription = function (txt) {
accDescription = sanitizeText(txt);
};
export const getAccDescription = function () {
return accDescription;
};
export const addSection = function (txt) {
currentSection = txt;
sections.push(txt);
@@ -637,6 +651,8 @@ export default {
getTodayMarker,
setTitle,
getTitle,
setAccDescription,
getAccDescription,
addSection,
getSections,
getTasks,

View File

@@ -32,6 +32,7 @@ describe('when using the ganttDb', function () {
fn | expected
${'getTasks'} | ${[]}
${'getTitle'} | ${''}
${'getAccDescription'} | ${''}
${'getDateFormat'} | ${''}
${'getAxisFormat'} | ${''}
${'getTodayMarker'} | ${''}

View File

@@ -15,6 +15,7 @@ import common from '../common/common';
import ganttDb from './ganttDb';
import { getConfig } from '../../config';
import { configureSvgSize } from '../../utils';
import addSVGAccessibilityFields from '../../accessibility';
parser.yy = ganttDb;
export const setConf = function () {
@@ -114,6 +115,8 @@ export const draw = function (text, id) {
.attr('y', conf.titleTopMargin)
.attr('class', 'titleText');
addSVGAccessibilityFields(parser.yy, svg, id);
/**
* @param tasks
* @param pageWidth

View File

@@ -65,22 +65,23 @@ that id.
<click>[\s\n] this.popState();
<click>[^\s\n]* return 'click';
"gantt" return 'gantt';
"dateFormat"\s[^#\n;]+ return 'dateFormat';
"inclusiveEndDates" return 'inclusiveEndDates';
"topAxis" return 'topAxis';
"axisFormat"\s[^#\n;]+ return 'axisFormat';
"includes"\s[^#\n;]+ return 'includes';
"excludes"\s[^#\n;]+ return 'excludes';
"todayMarker"\s[^\n;]+ return 'todayMarker';
\d\d\d\d"-"\d\d"-"\d\d return 'date';
"title"\s[^#\n;]+ return 'title';
"section"\s[^#:\n;]+ return 'section';
[^#:\n;]+ return 'taskTxt';
":"[^#\n;]+ return 'taskData';
":" return ':';
<<EOF>> return 'EOF';
. return 'INVALID';
"gantt" return 'gantt';
"dateFormat"\s[^#\n;]+ return 'dateFormat';
"inclusiveEndDates" return 'inclusiveEndDates';
"topAxis" return 'topAxis';
"axisFormat"\s[^#\n;]+ return 'axisFormat';
"includes"\s[^#\n;]+ return 'includes';
"excludes"\s[^#\n;]+ return 'excludes';
"todayMarker"\s[^\n;]+ return 'todayMarker';
\d\d\d\d"-"\d\d"-"\d\d return 'date';
"title"\s[^#\n;]+ return 'title';
"accDescription"\s[^#\n;]+ return 'accDescription'
"section"\s[^#:\n;]+ return 'section';
[^#:\n;]+ return 'taskTxt';
":"[^#\n;]+ return 'taskData';
":" return ':';
<<EOF>> return 'EOF';
. return 'INVALID';
/lex
@@ -116,6 +117,7 @@ statement
| includes {yy.setIncludes($1.substr(9));$$=$1.substr(9);}
| todayMarker {yy.setTodayMarker($1.substr(12));$$=$1.substr(12);}
| title {yy.setTitle($1.substr(6));$$=$1.substr(6);}
| accDescription {yy.setAccDescription($1.substr(15));$$=$1.substr(15);}
| section {yy.addSection($1.substr(8));$$=$1.substr(8);}
| clickStatement
| taskTxt taskData {yy.addTask($1,$2);$$='task';}

View File

@@ -156,4 +156,21 @@ describe('when parsing a gantt diagram it', function () {
'"test0", test1, test2'
);
});
it('should allow for a title and accDescription', function () {
const expectedTitle = 'Gantt Diagram';
const expectedAccDescription = 'Tasks for Q4';
const ganttString =
'gantt\n' +
`title ${expectedTitle}\n` +
`accDescription ${expectedAccDescription}\n` +
'dateFormat YYYY-MM-DD\n' +
'section Section\n' +
'A task :a1, 2014-01-01, 30d\n';
const output = parser.parse(ganttString);
expect(ganttDb.getTitle()).toBe(expectedTitle);
expect(ganttDb.getAccDescription()).toBe(expectedAccDescription);
});
});

View File

@@ -2,11 +2,15 @@ import { log } from '../../logger';
import { random } from '../../utils';
import mermaidAPI from '../../mermaidAPI';
import * as configApi from '../../config';
import { getConfig } from '../../config';
import common from '../common/common';
let mainBranchName = getConfig().gitGraph.mainBranchName;
let commits = {};
let head = null;
let branches = { main: head };
let curBranch = 'main';
let branches = {};
branches[mainBranchName] = head;
let curBranch = mainBranchName;
let direction = 'LR';
let seq = 0;
@@ -220,7 +224,6 @@ export const merge = function (otherBranch) {
export const checkout = function (branch) {
branch = common.sanitizeText(branch, configApi.getConfig());
console.info(branches);
if (typeof branches[branch] === 'undefined') {
let error = new Error(
'Trying to checkout branch which is not yet created. (Help try using "branch ' + branch + '")'
@@ -238,9 +241,6 @@ export const checkout = function (branch) {
} else {
curBranch = branch;
const id = branches[curBranch];
console.log(id);
console.log('hi');
console.log(commits);
head = commits[id];
}
};
@@ -320,8 +320,10 @@ export const prettyPrint = function () {
export const clear = function () {
commits = {};
head = null;
branches = { main: head };
curBranch = 'main';
let mainBranch = getConfig().gitGraph.mainBranchName;
branches = {};
branches[mainBranch] = null;
curBranch = mainBranch;
seq = 0;
};

View File

@@ -1,18 +1,12 @@
/* eslint-disable */
import { curveBasis, line, select } from 'd3';
import { interpolateToCurve, getStylesFromArray, configureSvgSize } from '../../utils';
import db from './gitGraphAst';
//import * as db from './mockDb';
import gitGraphParser from './parser/gitGraph';
import { log } from '../../logger';
/* eslint-disable */
import { getConfig } from '../../config';
//import * as configApi from '../../config';
let allCommitsDict = {};
let branchNum;
//let conf = configApi.getConfig();
//const commitType = db.commitType;
const commitType = {
NORMAL: 0,
REVERSE: 1,
@@ -29,89 +23,55 @@ const clear = () => {
commitPos = {};
allCommitsDict = {};
maxPos = 0;
lanes = []
lanes = [];
};
// let apiConfig = {};
// export const setConf = function(c) {
// apiConfig = c;
// };
/** @param svg */
function svgCreateDefs(svg) {
const config = getConfig().gitGraph;
svg
.append('defs')
.append('g')
.attr('id', 'def-commit')
.append('circle')
.attr('r', config.nodeRadius)
.attr('cx', 0)
.attr('cy', 0);
svg
.select('#def-commit')
.append('foreignObject')
.attr('width', config.nodeLabel.width)
.attr('height', config.nodeLabel.height)
.attr('x', config.nodeLabel.x)
.attr('y', config.nodeLabel.y)
.attr('class', 'node-label')
.attr('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility')
.append('p')
.html('');
}
/**
* @param svg
* @param points
* @param colorIdx
* @param interpolate
*/
/**
// Pass in the element and its pre-transform coords
* Draws a text, used for labels of the branches
*
* @param element
* @param coords
* @param {string} txt The text
* @returns {SVGElement}
*/
/**
* @param svg
* @param fromId
* @param toId
* @param direction
* @param color
*/
const drawText = (txt) => {
const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
// svgLabel.setAttribute('style', style.replace('color:', 'fill:'));
let rows = [];
let rows = [];
if (typeof txt === 'string') {
rows = txt.split(/\\n|\n|<br\s*\/?>/gi);
} else if (Array.isArray(txt)) {
rows = txt;
} else {
rows = [];
}
for (let j = 0; j < rows.length; j++) {
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
tspan.setAttribute('dy', '1em');
tspan.setAttribute('x', '0');
tspan.setAttribute('class', 'row');
tspan.textContent = rows[j].trim();
svgLabel.appendChild(tspan);
// Handling of new lines in the label
if (typeof txt === 'string') {
rows = txt.split(/\\n|\n|<br\s*\/?>/gi);
} else if (Array.isArray(txt)) {
rows = txt;
} else {
rows = [];
}
/**
* @param svg
* @param selector
*/
for (let j = 0; j < rows.length; j++) {
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
tspan.setAttribute('dy', '1em');
tspan.setAttribute('x', '0');
tspan.setAttribute('class', 'row');
tspan.textContent = rows[j].trim();
svgLabel.appendChild(tspan);
}
/**
* @param svg
* @param selector
*/
return svgLabel;
}
};
/**
* Draws the commits with its symbol and labels. The function has tywo modes, one which only
* calculates the positions and one that does the actual drawing. This for a simple way getting the
* vertical leyering rcorrect in the graph.
*
* @param {any} svg
* @param {any} commits
* @param {any} modifyGraph
*/
const drawCommits = (svg, commits, modifyGraph) => {
const gitGraphConfig = getConfig().gitGraph;
const gBullets = svg.append('g').attr('class', 'commit-bullets');
const gLabels = svg.append('g').attr('class', 'commit-labels');
let pos = 0;
@@ -119,20 +79,20 @@ const drawCommits = (svg, commits, modifyGraph) => {
const keys = Object.keys(commits);
const sortedKeys = keys.sort((a, b) => {
return commits[a].seq - commits[b].seq;
})
});
sortedKeys.forEach((key, index) => {
const commit = commits[key];
const y = branchPos[commit.branch].pos;
const x = pos + 10;
// Don't draw the commits now but calculate the positioning which is used by the branmch lines etc.
const x = pos + 10;
// Don't draw the commits now but calculate the positioning which is used by the branch lines etc.
if (modifyGraph) {
let typeClass;
switch(commit.type) {
switch (commit.type) {
case commitType.NORMAL:
typeClass = 'commit-normal';
break;
case commitType.REVERSE:
case commitType.REVERSE:
typeClass = 'commit-reverse';
break;
case commitType.HIGHLIGHT:
@@ -147,51 +107,77 @@ const drawCommits = (svg, commits, modifyGraph) => {
if (commit.type === commitType.HIGHLIGHT) {
const circle = gBullets.append('rect');
circle.attr('x', x-10);
circle.attr('y', y-10);
circle.attr('x', x - 10);
circle.attr('y', y - 10);
circle.attr('height', 20);
circle.attr('width', 20);
circle.attr('class', 'commit ' + commit.id + ' commit-highlight' + branchPos[commit.branch].index + ' ' + typeClass+'-outer');
gBullets.append('rect')
.attr('x', x-6)
.attr('y', y-6)
.attr('height', 12)
.attr('width', 12)
.attr('class', 'commit ' + commit.id + ' commit' + branchPos[commit.branch].index + ' ' + typeClass+'-inner');
circle.attr(
'class',
'commit ' +
commit.id +
' commit-highlight' +
branchPos[commit.branch].index +
' ' +
typeClass +
'-outer'
);
gBullets
.append('rect')
.attr('x', x - 6)
.attr('y', y - 6)
.attr('height', 12)
.attr('width', 12)
.attr(
'class',
'commit ' +
commit.id +
' commit' +
branchPos[commit.branch].index +
' ' +
typeClass +
'-inner'
);
} else {
const circle = gBullets.append('circle');
circle.attr('cx', x);
circle.attr('cy', y);
circle.attr('r', commit.type === commitType.MERGE ? 9:10);
circle.attr('class', 'commit ' + commit.id + ' commit' + branchPos[commit.branch].index);
if(commit.type === commitType.MERGE) {
circle.attr('r', commit.type === commitType.MERGE ? 9 : 10);
circle.attr('class', 'commit ' + commit.id + ' commit' + branchPos[commit.branch].index);
if (commit.type === commitType.MERGE) {
const circle2 = gBullets.append('circle');
circle2.attr('cx', x);
circle2.attr('cy', y);
circle2.attr('r', 6);
circle2.attr('class', 'commit '+typeClass + ' ' + commit.id + ' commit' + branchPos[commit.branch].index);
circle2.attr(
'class',
'commit ' + typeClass + ' ' + commit.id + ' commit' + branchPos[commit.branch].index
);
}
if(commit.type === commitType.REVERSE) {
if (commit.type === commitType.REVERSE) {
const cross = gBullets.append('path');
cross
.attr('d', `M ${x-5},${y-5}L${x+5},${y+5}M${x-5},${y+5}L${x+5},${y-5}`)
.attr('class', 'commit '+typeClass + ' ' + commit.id + ' commit' + branchPos[commit.branch].index);
.attr('d', `M ${x - 5},${y - 5}L${x + 5},${y + 5}M${x - 5},${y + 5}L${x + 5},${y - 5}`)
.attr(
'class',
'commit ' + typeClass + ' ' + commit.id + ' commit' + branchPos[commit.branch].index
);
}
}
}
commitPos[commit.id] = {x: pos + 10, y: y};
commitPos[commit.id] = { x: pos + 10, y: y };
// The first iteration over the commits are for positioning purposes, this
// is required for drawing the lines. The circles and labels is drawn after the labels
// placing them on top of the lines.
if (modifyGraph) {
const px=4;
const py=2;
if(commit.type !== commitType.MERGE) {
const labelBkg = gLabels.insert('rect')
.attr('class', 'commit-label-bkg');
const px = 4;
const py = 2;
// Draw the commit label
if (commit.type !== commitType.MERGE && gitGraphConfig.showCommitLabel) {
const labelBkg = gLabels.insert('rect').attr('class', 'commit-label-bkg');
const text = gLabels.append('text')
const text = gLabels
.append('text')
.attr('x', pos)
.attr('y', y + 25)
.attr('class', 'commit-label')
@@ -206,48 +192,52 @@ const drawCommits = (svg, commits, modifyGraph) => {
.attr('height', bbox.height + 2 * py);
text.attr('x', pos + 10 - bbox.width / 2);
}
if(commit.tag) {
if (commit.tag) {
const rect = gLabels.insert('polygon');
const hole = gLabels.append('circle');
const tag = gLabels.append('text')
// Note that we are delaying setting the x position until we know the width of the text
.attr('y', y - 16)
.attr('class', 'tag-label')
.text(commit.tag);
const tag = gLabels
.append('text')
// Note that we are delaying setting the x position until we know the width of the text
.attr('y', y - 16)
.attr('class', 'tag-label')
.text(commit.tag);
let tagBbox = tag.node().getBBox();
tag.attr('x', pos + 10 - tagBbox.width / 2);
const h2 = tagBbox.height/2
const ly = y - 19.2 ;
rect
.attr('class', 'tag-label-bkg')
.attr('points', `
${pos - tagBbox.width / 2 - px/2},${ly + py}
${pos - tagBbox.width / 2 - px/2},${ly - py}
const h2 = tagBbox.height / 2;
const ly = y - 19.2;
rect.attr('class', 'tag-label-bkg').attr(
'points',
`
${pos - tagBbox.width / 2 - px / 2},${ly + py}
${pos - tagBbox.width / 2 - px / 2},${ly - py}
${pos + 10 - tagBbox.width / 2 - px},${ly - h2 - py}
${pos + 10 + tagBbox.width / 2 + px},${ly - h2 - py}
${pos + 10 + tagBbox.width / 2 + px},${ly + h2 + py }
${pos + 10 - tagBbox.width / 2 - px},${ly + h2 + py}`);
${pos + 10 + tagBbox.width / 2 + px},${ly + h2 + py}
${pos + 10 - tagBbox.width / 2 - px},${ly + h2 + py}`
);
hole
.attr('cx', pos - tagBbox.width / 2 + px/2)
.attr('cx', pos - tagBbox.width / 2 + px / 2)
.attr('cy', ly)
.attr('r', 1.5)
.attr('class', 'tag-hole');
}
}
pos +=50;
if(pos>maxPos) {
pos += 50;
if (pos > maxPos) {
maxPos = pos;
}
});
}
};
/**
* Detect if there are other commits between commit1s x-position and commit2s x-position on the same branch as commit2.
* @param {*} commit1
* @param {*} commit2
* Detect if there are other commits between commit1s x-position and commit2s x-position on the same
* branch as commit2.
*
* @param {any} commit1
* @param {any} commit2
* @param allCommits
* @returns
*/
const hasOverlappingCommits = (commit1, commit2, allCommits) => {
@@ -257,43 +247,62 @@ const hasOverlappingCommits = (commit1, commit2, allCommits) => {
// Find commits on the same branch as commit2
const keys = Object.keys(allCommits);
const overlappingComits = keys.filter((key) => {
return allCommits[key].branch === commit2.branch && allCommits[key].seq > commit1.seq && allCommits[key].seq < commit2.seq
return (
allCommits[key].branch === commit2.branch &&
allCommits[key].seq > commit1.seq &&
allCommits[key].seq < commit2.seq
);
});
return overlappingComits.length > 0;
}
};
/**
* This function find a lane in the y-axis that is not overlapping with any other lanes. This is
* used for drawing the lines between commits.
*
* @param {any} y1
* @param {any} y2
* @param {any} _depth
* @returns
*/
const findLane = (y1, y2, _depth) => {
const depth = _depth || 0;
const candidate = y1 + Math.abs(y1 - y2) / 2;
if(depth > 5) {
const candidate = y1 + Math.abs(y1 - y2) / 2;
if (depth > 5) {
return candidate;
}
let ok = true;
for(let i = 0; i < lanes.length; i++) {
if(Math.abs(lanes[i] - candidate) < 10) {
for (let i = 0; i < lanes.length; i++) {
if (Math.abs(lanes[i] - candidate) < 10) {
ok = false;
}
}
if(ok) {
if (ok) {
lanes.push(candidate);
return candidate;
}
const diff = Math.abs(y1 - y2);
return findLane(y1, y2-(diff/5), depth);
}
return findLane(y1, y2 - diff / 5, depth);
};
/**
* This function draw trhe lines between the commits. They were arrows initially.
*
* @param {any} svg
* @param {any} commit1
* @param {any} commit2
* @param {any} allCommits
*/
const drawArrow = (svg, commit1, commit2, allCommits) => {
const conf = getConfig();
const p1 = commitPos[commit1.id];
const p2 = commitPos[commit2.id];
const overlappingCommits = hasOverlappingCommits(commit1, commit2, allCommits);
log.debug('drawArrow', p1, p2, overlappingCommits, commit1.id, commit2.id);
// log.debug('drawArrow', p1, p2, overlappingCommits, commit1.id, commit2.id);
let url = '';
if (conf.arrowMarkerAbsolute) {
@@ -310,28 +319,30 @@ const drawArrow = (svg, commit1, commit2, allCommits) => {
let arc = '';
let arc2 = '';
let radius = 0;
let offset = 0
let colorClassNum = branchPos[commit2.branch].index
let offset = 0;
let colorClassNum = branchPos[commit2.branch].index;
let lineDef;
if(overlappingCommits) {
arc = 'A 10 10, 0, 0, 0,';
arc2 = 'A 10 10, 0, 0, 1,';
radius = 10;
offset = 10;
if (overlappingCommits) {
arc = 'A 10 10, 0, 0, 0,';
arc2 = 'A 10 10, 0, 0, 1,';
radius = 10;
offset = 10;
// Figure out the color of the arrow,arrows going down take the color from the destination branch
colorClassNum = branchPos[commit2.branch].index;
colorClassNum = branchPos[commit2.branch].index;
const lineY = p1.y < p2.y ? findLane(p1.y, p2.y):findLane(p2.y, p1.y);
if(p1.y < p2.y) {
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${lineY-radius} ${arc} ${p1.x + offset} ${lineY} L ${p2.x-radius} ${lineY} ${arc2} ${p2.x} ${lineY+offset} L ${p2.x} ${p2.y}`;
} else {
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${lineY+radius} ${arc2} ${p1.x + offset} ${lineY} L ${p2.x-radius} ${lineY} ${arc} ${p2.x} ${lineY-offset} L ${p2.x} ${p2.y}`;
}
const lineY = p1.y < p2.y ? findLane(p1.y, p2.y) : findLane(p2.y, p1.y);
if (p1.y < p2.y) {
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${lineY - radius} ${arc} ${p1.x + offset} ${lineY} L ${
p2.x - radius
} ${lineY} ${arc2} ${p2.x} ${lineY + offset} L ${p2.x} ${p2.y}`;
} else {
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${lineY + radius} ${arc2} ${
p1.x + offset
} ${lineY} L ${p2.x - radius} ${lineY} ${arc} ${p2.x} ${lineY - offset} L ${p2.x} ${p2.y}`;
}
} else {
if(p1.y < p2.y) {
if (p1.y < p2.y) {
arc = 'A 20 20, 0, 0, 0,';
radius = 20;
offset = 20;
@@ -339,26 +350,34 @@ const drawArrow = (svg, commit1, commit2, allCommits) => {
// Figure out the color of the arrow,arrows going down take the color from the destination branch
colorClassNum = branchPos[commit2.branch].index;
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y-radius} ${arc} ${p1.x + offset} ${p2.y} L ${p2.x} ${p2.y}`;
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc} ${p1.x + offset} ${p2.y} L ${
p2.x
} ${p2.y}`;
}
if(p1.y > p2.y) {
if (p1.y > p2.y) {
arc = 'A 20 20, 0, 0, 0,';
radius = 20;
offset = 20;
// Arrows going up take the color from the source branch
colorClassNum = branchPos[commit1.branch].index;
lineDef = `M ${p1.x} ${p1.y} L ${p2.x-radius} ${p1.y} ${arc} ${p2.x} ${p1.y-offset} L ${p2.x} ${p2.y}`;
lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc} ${p2.x} ${p1.y - offset} L ${
p2.x
} ${p2.y}`;
}
if(p1.y === p2.y) {
colorClassNum = branchPos[commit1.branch].index
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y-radius} ${arc} ${p1.x + offset} ${p2.y} L ${p2.x} ${p2.y}`;
if (p1.y === p2.y) {
colorClassNum = branchPos[commit1.branch].index;
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc} ${p1.x + offset} ${p2.y} L ${
p2.x
} ${p2.y}`;
}
}
const arrow = svg.append('path').attr('d', lineDef)
.attr('class', 'arrow arrow' + colorClassNum)
}
const arrow = svg
.append('path')
.attr('d', lineDef)
.attr('class', 'arrow arrow' + colorClassNum);
};
const drawArrows = (svg, commits) => {
const gArrows = svg.append('g').attr('class', 'commit-arrows');
@@ -367,123 +386,114 @@ const drawArrows = (svg, commits) => {
const k = Object.keys(commits);
k.forEach((key, index) => {
const commit = commits[key];
if(commit.parents && commit.parents.length>0) {
if (commit.parents && commit.parents.length > 0) {
commit.parents.forEach((parent) => {
drawArrow(gArrows, commits[parent], commit, commits);
});
}
});
}
};
/**
* This function adds the branches and the branches' labels to the svg.
*
* @param svg
* @param commitid
* @param branches
* @param direction
*/
const drawBranches = (svg, branches) => {
const g = svg.append('g')
branches.forEach((branch, index) => {
const pos = branchPos[branch.name].pos;
const line = g.append('line');
line.attr('x1', 0);
line.attr('y1', pos);
line.attr('x2', maxPos);
line.attr('y2', pos);
line.attr('class', 'branch branch'+index)
const gitGraphConfig = getConfig().gitGraph;
const g = svg.append('g');
branches.forEach((branch, index) => {
const pos = branchPos[branch.name].pos;
const line = g.append('line');
line.attr('x1', 0);
line.attr('y1', pos);
line.attr('x2', maxPos);
line.attr('y2', pos);
line.attr('class', 'branch branch' + index);
lanes.push(pos);
lanes.push(pos);
// Create the actual text element
const labelElement = drawText(branch.name);
// Create outer g, edgeLabel, this will be positioned after graph layout
const bkg = g.insert('rect');
const branchLabel = g.insert('g').attr('class', 'branchLabel');
let name = index === 0 ? gitGraphConfig.mainBranchName : branch.name;
// Create inner g, label, this will be positioned now for centering the text
const label = branchLabel.insert('g').attr('class', 'label branch-label'+index);
label.node().appendChild(labelElement);
let bbox = labelElement.getBBox();
bkg.attr('class', 'branchLabelBkg label' + index)
.attr('rx', 4)
.attr('ry', 4)
.attr('x', -bbox.width -4)
.attr('y', -bbox.height / 2 +8 )
.attr('width', bbox.width + 18)
.attr('height', bbox.height + 4);
// Create the actual text element
const labelElement = drawText(name);
// Create outer g, edgeLabel, this will be positioned after graph layout
const bkg = g.insert('rect');
const branchLabel = g.insert('g').attr('class', 'branchLabel');
label.attr('transform', 'translate(' + (-bbox.width -14) + ', ' + (pos - bbox.height/2-1) + ')');
bkg.attr('transform', 'translate(' + -19 + ', ' + (pos - bbox.height/2) + ')');
})
// Create inner g, label, this will be positioned now for centering the text
const label = branchLabel.insert('g').attr('class', 'label branch-label' + index);
label.node().appendChild(labelElement);
let bbox = labelElement.getBBox();
bkg
.attr('class', 'branchLabelBkg label' + index)
.attr('rx', 4)
.attr('ry', 4)
.attr('x', -bbox.width - 4)
.attr('y', -bbox.height / 2 + 8)
.attr('width', bbox.width + 18)
.attr('height', bbox.height + 4);
}
label.attr(
'transform',
'translate(' + (-bbox.width - 14) + ', ' + (pos - bbox.height / 2 - 1) + ')'
);
bkg.attr('transform', 'translate(' + -19 + ', ' + (pos - bbox.height / 2) + ')');
});
};
/**
* @param svg
* @param commit
* @param direction
* @param branchColor
* @param txt
* @param id
* @param ver
*/
export const draw = function (txt, id, ver) {
clear();
const conf = getConfig();
const config = conf.gitGraph;
const gitGraphConfig = getConfig().gitGraph;
// try {
const parser = gitGraphParser.parser;
parser.yy = db;
parser.yy.clear();
log.debug('in gitgraph renderer', txt + '\n', 'id:', id, ver);
log.debug('in gitgraph renderer', txt + '\n', 'id:', id, ver);
// // Parse the graph definition
parser.parse(txt + '\n');
// config = Object.assign(config, apiConfig, db.getOptions());
const direction = db.getDirection();
allCommitsDict = db.getCommits();
const branches = db.getBranchesAsObjArray();
// Position branches vertically
let pos=0;
let pos = 0;
branches.forEach((branch, index) => {
branchPos[branch.name] = {pos, index};
pos+=50;
branchPos[branch.name] = { pos, index };
pos += 50;
});
log.debug('brach pos ', branchPos);
log.debug('effective options', config, branches);
log.debug('commits', allCommitsDict);
const diagram = select(`[id="${id}"]`);
svgCreateDefs(diagram);
diagram
.append('defs')
.append('marker')
.attr('id', 'arrowhead')
.attr('refX',24)
.attr('refY', 10)
.attr('markerUnits', 'userSpaceOnUse')
.attr('markerWidth', 24)
.attr('markerHeight', 24)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M 0 0 L 20 10 L 0 20 z'); // this is actual shape for arrowhead
drawCommits(diagram, allCommitsDict, false);
drawBranches(diagram, branches);
if (gitGraphConfig.showBranches) {
drawBranches(diagram, branches);
}
drawArrows(diagram, allCommitsDict);
drawCommits(diagram, allCommitsDict, true);
const padding = config.diagramPadding;
const padding = gitGraphConfig.diagramPadding;
const svgBounds = diagram.node().getBBox();
const width = svgBounds.width + padding * 2;
const height = svgBounds.height + padding * 2;
configureSvgSize(diagram, height, width, conf.useMaxWidth);
const vBox = `${svgBounds.x - padding} ${svgBounds.y - padding} ${width} ${height}`;
// logger.debug(`viewBox ${vBox}`);
diagram.attr('viewBox', vBox);
};

View File

@@ -2,7 +2,6 @@ import { log } from '../../logger';
import mermaidAPI from '../../mermaidAPI';
import * as configApi from '../../config';
import common from '../common/common';
const sanitizeText = (txt) => common.sanitizeText(txt, configApi.getConfig());
let sections = {};
let title = '';

View File

@@ -21,6 +21,9 @@
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
"title"\s[^#\n;]+ return 'title';
"accDescription"\s[^#\n;]+ return 'accDescription';
(\r?\n)+ return 'NEWLINE';
\s+ /* skip all whitespace */
\#[^\n]* /* skip comments */
@@ -90,7 +93,9 @@ start
directive
: openDirective typeDirective closeDirective
| openDirective typeDirective ':' argDirective closeDirective;
| openDirective typeDirective ':' argDirective closeDirective
| title {yy.setTitle($1.substring(6));$$=$1.substring(6);}
| accDescription {yy.setAccDescription($1.substring(15));$$=$1.substring(15);};
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); };
@@ -191,6 +196,7 @@ relationship
| TRACES
{ $$=yy.Relationships.TRACES;};
requirementName: unqString | qString;
id : unqString | qString;
text : unqString | qString;

View File

@@ -72,6 +72,29 @@ describe('when parsing requirement diagram it...', function () {
expect(Object.keys(requirementDb.getRelationships()).length).toBe(0);
});
it('will use a title and accDescription', function () {
const expectedTitle = 'test title';
const expectedAccDescription = 'my chart description';
const expectedDocRef = 'test_ref';
let lines = [
`requirementDiagram`,
``,
`title ${expectedTitle}`,
`accDescription ${expectedAccDescription}`,
`element test_name {`,
`type: test_type`,
`docref: test_ref`,
`}`,
];
let doc = lines.join('\n');
reqDiagram.parser.parse(doc);
expect(requirementDb.getTitle()).toBe(expectedTitle);
expect(requirementDb.getAccDescription()).toBe(expectedAccDescription);
});
it('will accept full relationship definition', function () {
const expectedSrc = 'a';
const expectedDest = 'b';

View File

@@ -1,12 +1,17 @@
import * as configApi from '../../config';
import { log } from '../../logger';
import mermaidAPI from '../../mermaidAPI';
import common from '../common/common';
let relations = [];
let latestRequirement = {};
let requirements = {};
let latestElement = {};
let elements = {};
let title = '';
let accDescription = '';
const sanitizeText = (txt) => common.sanitizeText(txt, configApi.getConfig());
const RequirementType = {
REQUIREMENT: 'Requirement',
@@ -134,6 +139,24 @@ const clear = () => {
elements = {};
};
export const setTitle = function (txt) {
let sanitizedText = sanitizeText(txt, configApi.getConfig());
title = sanitizedText;
};
export const getTitle = function () {
return title;
};
export const setAccDescription = function (txt) {
let sanitizedText = sanitizeText(txt, configApi.getConfig());
accDescription = sanitizedText;
};
export const getAccDescription = function () {
return accDescription;
};
export default {
RequirementType,
RiskLevel,
@@ -149,6 +172,10 @@ export default {
setNewReqText,
setNewReqRisk,
setNewReqVerifyMethod,
setTitle,
getTitle,
setAccDescription,
getAccDescription,
addElement,
getElements,

View File

@@ -9,6 +9,7 @@ import { parser } from './parser/requirementDiagram';
import requirementDb from './requirementDb';
import markers from './requirementMarkers';
import { getConfig } from '../../config';
import addSVGAccessibilityFields from '../../accessibility';
const conf = {};
let relCnt = 0;
@@ -377,6 +378,8 @@ export const draw = (text, id) => {
configureSvgSize(svg, height, width, conf.useMaxWidth);
svg.attr('viewBox', `${svgBounds.x - padding} ${svgBounds.y - padding} ${width} ${height}`);
// Adds title and description to the requirements diagram
addSVGAccessibilityFields(parser.yy, svg, id);
};
export default {

View File

@@ -21,7 +21,7 @@ describe('when parsing a sequenceDiagram', function () {
parser.yy = sequenceDb;
parser.yy.clear();
});
it('it should handle a sequenceDiagram definition', function () {
it('should handle a sequenceDiagram definition', function () {
const str = `
sequenceDiagram
Alice->Bob:Hello Bob, how are you?

View File

@@ -272,20 +272,65 @@ const actorFont = (cnf) => {
};
/**
* Draws a message
* Process a message by adding its dimensions to the bound. It returns the Y coordinate of the
* message so it can be drawn later. We do not draw the message at this point so the arrowhead can
* be on top of the activation box.
*
* @param {any} g - The parent of the message element
* @param {any} diagram - The parent of the message element
* @param {any} msgModel - The model containing fields describing a message
* @returns {number} LineStarty - The Y coordinate at which the message line starts
*/
const drawMessage = function (g, msgModel) {
const boundMessage = function (diagram, msgModel) {
bounds.bumpVerticalPos(10);
const { startx, stopx, starty, message, type, sequenceIndex } = msgModel;
const { startx, stopx, message } = msgModel;
const lines = common.splitBreaks(message).length;
let textDims = utils.calculateTextDimensions(message, messageFont(conf));
const lineHeight = textDims.height / lines;
msgModel.height += lineHeight;
bounds.bumpVerticalPos(lineHeight);
let lineStarty;
let totalOffset = textDims.height - 10;
let textWidth = textDims.width;
if (startx === stopx) {
lineStarty = bounds.getVerticalPos() + totalOffset;
if (!conf.rightAngles) {
totalOffset += conf.boxMargin;
lineStarty = bounds.getVerticalPos() + totalOffset;
}
totalOffset += 30;
const dx = Math.max(textWidth / 2, conf.width / 2);
bounds.insert(
startx - dx,
bounds.getVerticalPos() - 10 + totalOffset,
stopx + dx,
bounds.getVerticalPos() + 30 + totalOffset
);
} else {
totalOffset += conf.boxMargin;
lineStarty = bounds.getVerticalPos() + totalOffset;
bounds.insert(startx, lineStarty - 10, stopx, lineStarty);
}
bounds.bumpVerticalPos(totalOffset);
msgModel.height += totalOffset;
msgModel.stopy = msgModel.starty + msgModel.height;
bounds.insert(msgModel.fromBounds, msgModel.starty, msgModel.toBounds, msgModel.stopy);
return lineStarty;
};
/**
* Draws a message. Note that the bounds have previously been updated by boundMessage.
*
* @param {any} diagram - The parent of the message element
* @param {any} msgModel - The model containing fields describing a message
* @param {float} lineStarty - The Y coordinate at which the message line starts
*/
const drawMessage = function (diagram, msgModel, lineStarty) {
const { startx, stopx, starty, message, type, sequenceIndex } = msgModel;
let textDims = utils.calculateTextDimensions(message, messageFont(conf));
const textObj = svgDraw.getTextObj();
textObj.x = startx;
textObj.y = starty + 10;
@@ -301,17 +346,14 @@ const drawMessage = function (g, msgModel) {
textObj.textMargin = conf.wrapPadding;
textObj.tspan = false;
drawText(g, textObj);
let totalOffset = textDims.height - 10;
drawText(diagram, textObj);
let textWidth = textDims.width;
let line, lineStarty;
let line;
if (startx === stopx) {
lineStarty = bounds.getVerticalPos() + totalOffset;
if (conf.rightAngles) {
line = g
line = diagram
.append('path')
.attr(
'd',
@@ -320,10 +362,7 @@ const drawMessage = function (g, msgModel) {
} H ${startx}`
);
} else {
totalOffset += conf.boxMargin;
lineStarty = bounds.getVerticalPos() + totalOffset;
line = g
line = diagram
.append('path')
.attr(
'd',
@@ -345,24 +384,12 @@ const drawMessage = function (g, msgModel) {
(lineStarty + 20)
);
}
totalOffset += 30;
const dx = Math.max(textWidth / 2, conf.width / 2);
bounds.insert(
startx - dx,
bounds.getVerticalPos() - 10 + totalOffset,
stopx + dx,
bounds.getVerticalPos() + 30 + totalOffset
);
} else {
totalOffset += conf.boxMargin;
lineStarty = bounds.getVerticalPos() + totalOffset;
line = g.append('line');
line = diagram.append('line');
line.attr('x1', startx);
line.attr('y1', lineStarty);
line.attr('x2', stopx);
line.attr('y2', lineStarty);
bounds.insert(startx, lineStarty - 10, stopx, lineStarty);
}
// Make an SVG Container
// Draw the line
@@ -407,7 +434,8 @@ const drawMessage = function (g, msgModel) {
// add node number
if (sequenceDb.showSequenceNumbers() || conf.showSequenceNumbers) {
line.attr('marker-start', 'url(' + url + '#sequencenumber)');
g.append('text')
diagram
.append('text')
.attr('x', startx)
.attr('y', lineStarty + 4)
.attr('font-family', 'sans-serif')
@@ -417,10 +445,6 @@ const drawMessage = function (g, msgModel) {
.attr('class', 'sequenceNumber')
.text(sequenceIndex);
}
bounds.bumpVerticalPos(totalOffset);
msgModel.height += totalOffset;
msgModel.stopy = msgModel.starty + msgModel.height;
bounds.insert(msgModel.fromBounds, msgModel.starty, msgModel.toBounds, msgModel.stopy);
};
export const drawActors = function (diagram, actors, actorKeys, verticalPos) {
@@ -613,6 +637,7 @@ export const draw = function (text, id) {
// Draw the messages/signals
let sequenceIndex = 1;
let messagesToDraw = Array();
messages.forEach(function (msg) {
let loopModel, noteModel, msgModel;
@@ -722,7 +747,8 @@ export const draw = function (text, id) {
msgModel = msg.msgModel;
msgModel.starty = bounds.getVerticalPos();
msgModel.sequenceIndex = sequenceIndex;
drawMessage(diagram, msgModel);
let lineStarty = boundMessage(diagram, msgModel);
messagesToDraw.push({ messageModel: msgModel, lineStarty: lineStarty });
bounds.models.addMessage(msgModel);
} catch (e) {
log.error('error while drawing message', e);
@@ -746,6 +772,8 @@ export const draw = function (text, id) {
}
});
messagesToDraw.forEach((e) => drawMessage(diagram, e.messageModel, e.lineStarty));
if (conf.mirrorActors) {
// Draw actors below diagram
bounds.bumpVerticalPos(conf.boxMargin * 2);

View File

@@ -20,6 +20,8 @@
%x STATE_ID
%x ALIAS
%x SCALE
%x title
%x accDescription
%x NOTE
%x NOTE_ID
%x NOTE_TEXT
@@ -58,6 +60,11 @@
<SCALE>\d+ return 'WIDTH';
<SCALE>\s+"width" {this.popState();}
title { this.begin("title");return 'title'; }
<title>(?!\n|;|#)*[^\n]* { this.popState(); return "title_value"; }
accDescription { this.begin("accDescription");return 'accDescription'; }
<accDescription>(?!\n|;|#)*[^\n]* { this.popState(); return "description_value"; }
<INITIAL,struct>"state"\s+ { /*console.log('Starting STATE zxzx'+yy.getDirection());*/this.pushState('STATE'); }
<STATE>.*"<<fork>>" {this.popState();yytext=yytext.slice(0,-8).trim(); /*console.warn('Fork Fork: ',yytext);*/return 'FORK';}
<STATE>.*"<<join>>" {this.popState();yytext=yytext.slice(0,-8).trim();/*console.warn('Fork Join: ',yytext);*/return 'JOIN';}
@@ -193,6 +200,8 @@ statement
| note NOTE_TEXT AS ID
| directive
| direction
| title title_value { $$=$2.trim();yy.setTitle($$); }
| accDescription description_value { $$=$2.trim();yy.setAccDescription($$); }
;
directive

View File

@@ -4,6 +4,8 @@ import mermaidAPI from '../../mermaidAPI';
import common from '../common/common';
import * as configApi from '../../config';
const sanitizeText = (txt) => common.sanitizeText(txt, configApi.getConfig());
const clone = (o) => JSON.parse(JSON.stringify(o));
let rootDoc = [];
@@ -115,6 +117,25 @@ let startCnt = 0;
let endCnt = 0; // eslint-disable-line
// let stateCnt = 0;
let title = 'State diagram';
let description = '';
const setTitle = function (txt) {
title = sanitizeText(txt);
};
const getTitle = function () {
return title;
};
const setAccDescription = function (txt) {
description = sanitizeText(txt);
};
const getAccDescription = function () {
return description;
};
/**
* Function called by parser when a node definition has been found.
*
@@ -280,4 +301,8 @@ export default {
getRootDocV2,
extract,
trimColon,
getTitle,
setTitle,
getAccDescription,
setAccDescription,
};

View File

@@ -0,0 +1,376 @@
import { parser } from './parser/stateDiagram';
import stateDb from './stateDb';
describe('state diagram, ', function () {
describe('when parsing an info graph it', function () {
beforeEach(function () {
parser.yy = stateDb;
});
it('super simple', function () {
const str = `
stateDiagram-v2
[*] --> State1
State1 --> [*]
`;
parser.parse(str);
});
it('simple', function () {
const str = `stateDiagram-v2\n
State1 : this is another string
[*] --> State1
State1 --> [*]
`;
parser.parse(str);
const description = stateDb.getAccDescription();
expect(description).toBe('');
});
it('simple with accDescription', function () {
const str = `stateDiagram-v2\n
accDescription a simple description of the diagram
State1 : this is another string
[*] --> State1
State1 --> [*]
`;
parser.parse(str);
const description = stateDb.getAccDescription();
expect(description).toBe('a simple description of the diagram');
});
it('simple with title', function () {
const str = `stateDiagram-v2\n
title a simple title of the diagram
State1 : this is another string
[*] --> State1
State1 --> [*]
`;
parser.parse(str);
const title = stateDb.getTitle();
expect(title).toBe('a simple title of the diagram');
});
it('simple with directive', function () {
const str = `%%{init: {'logLevel': 0 }}%%
stateDiagram-v2\n
State1 : this is another string
[*] --> State1
State1 --> [*]
`;
parser.parse(str);
});
it('should handle relation definitions', function () {
const str = `stateDiagram-v2\n
[*] --> State1
State1 --> [*]
State1 : this is a string
State1 : this is another string
State1 --> State2
State2 --> [*]
`;
parser.parse(str);
});
it('hide empty description', function () {
const str = `stateDiagram-v2\n
hide empty description
[*] --> State1
State1 --> [*]
State1 : this is a string
State1 : this is another string
State1 --> State2
State2 --> [*]
`;
parser.parse(str);
});
it('handle "as" in state names', function () {
const str = `stateDiagram-v2
assemble
state assemble
`;
parser.parse(str);
});
it('handle "as" in state names 1', function () {
const str = `stateDiagram-v2
assemble
state assemble
`;
parser.parse(str);
});
it('handle "as" in state names 2', function () {
const str = `stateDiagram-v2
assembleas
state assembleas
`;
parser.parse(str);
});
it('handle "as" in state names 3', function () {
const str = `stateDiagram-v2
state "as" as as
`;
parser.parse(str);
});
it('scale', function () {
const str = `stateDiagram-v2\n
scale 350 width
[*] --> State1
State1 --> [*]
State1 : this is a string with - in it
State1 : this is another string
State1 --> State2
State2 --> [*]
`;
parser.parse(str);
});
it('description after second state', function () {
const str = `stateDiagram-v2\n
scale 350 width
[*] --> State1 : This is the description with - in it
State1 --> [*]
`;
parser.parse(str);
})
it('shall handle descriptions including minus signs', function () {
const str = `stateDiagram-v2\n
scale 350 width
[*] --> State1 : This is the description +-!
State1 --> [*]
`;
parser.parse(str);
});
it('should handle state statements', function () {
const str = `stateDiagram-v2\n
state Configuring {
[*] --> NewValueSelection
NewValueSelection --> NewValuePreview : EvNewValue
NewValuePreview --> NewValueSelection : EvNewValueRejected
NewValuePreview --> NewValueSelection : EvNewValueSaved1
}
`;
parser.parse(str);
});
it('should handle recursive state definitions', function () {
const str = `stateDiagram-v2\n
state Configuring {
[*] --> NewValueSelection
NewValueSelection --> NewValuePreview : EvNewValue
NewValuePreview --> NewValueSelection : EvNewValueRejected
NewValuePreview --> NewValueSelection : EvNewValueSaved
state NewValuePreview {
State1 --> State2
}
}
`;
parser.parse(str);
});
it('should handle multiple recursive state definitions', function () {
const str = `stateDiagram-v2\n
scale 350 width
[*] --> NotShooting
state NotShooting {
[*] --> Idle
Idle --> Configuring : EvConfig
Configuring --> Idle : EvConfig
}
state Configuring {
[*] --> NewValueSelection
NewValueSelection --> NewValuePreview : EvNewValue
NewValuePreview --> NewValueSelection : EvNewValueRejected
NewValuePreview --> NewValueSelection : EvNewValueSaved
state NewValuePreview {
State1 --> State2
}
}
`;
parser.parse(str);
});
it('should handle state deifintions with separation of id', function () {
const str = `stateDiagram-v2\n
state "Long state description" as state1
`;
parser.parse(str);
});
it('should handle state deifintions with separation of id', function () {
const str = `stateDiagram-v2
state "Not Shooting State" as NotShooting {
state "Idle mode" as Idle
state "Configuring mode" as Configuring
[*] --> Idle
Idle --> Configuring : EvConfig
Configuring --> Idle : EvConfig
}
`;
parser.parse(str);
});
it('should State definition with quotes', function () {
const str = `stateDiagram-v2\n
scale 600 width
[*] --> State1
State1 --> State2 : Succeeded
State1 --> [*] : Aborted
State2 --> State3 : Succeeded
State2 --> [*] : Aborted
state State3 {
state "Accumulate Enough Data\nLong State Name" as long1
long1 : Just a test
[*] --> long1
long1 --> long1 : New Data
long1 --> ProcessData : Enough Data
}
State3 --> State3 : Failed
State3 --> [*] : Succeeded / Save Result
State3 --> [*] : Aborted
`;
parser.parse(str);
});
it('should handle fork statements', function () {
const str = `stateDiagram-v2\n
state fork_state <<fork>>
[*] --> fork_state
fork_state --> State2
fork_state --> State3
state join_state <<join>>
State2 --> join_state
State3 --> join_state
join_state --> State4
State4 --> [*]
`;
parser.parse(str);
});
it('should handle concurrent state', function () {
const str = `stateDiagram-v2\n
[*] --> Active
state Active {
[*] --> NumLockOff
NumLockOff --> NumLockOn : EvNumLockPressed
NumLockOn --> NumLockOff : EvNumLockPressed
--
[*] --> CapsLockOff
CapsLockOff --> CapsLockOn : EvCapsLockPressed
CapsLockOn --> CapsLockOff : EvCapsLockPressed
--
[*] --> ScrollLockOff
ScrollLockOff --> ScrollLockOn : EvCapsLockPressed
ScrollLockOn --> ScrollLockOff : EvCapsLockPressed
}
`;
parser.parse(str);
});
it('should handle concurrent state', function () {
const str = `stateDiagram-v2\n
[*] --> Active
state Active {
[*] --> NumLockOff
--
[*] --> CapsLockOff
--
[*] --> ScrollLockOff
}
`;
parser.parse(str);
});
// it('should handle arrow directions definitions', function() {
// const str = `stateDiagram-v2\n
// [*] -up-> First
// First -right-> Second
// Second --> Third
// Third -left-> Last
// `;
// parser.parse(str);
// });
it('should handle note statements', function () {
const str = `stateDiagram-v2\n
[*] --> Active
Active --> Inactive
note left of Active : this is a short<br/>note
note right of Inactive
A note can also
be defined on
several lines
end note
`;
parser.parse(str);
});
it('should handle multiline notes with different line breaks', function () {
const str = `stateDiagram-v2
State1
note right of State1
Line1<br>Line2<br/>Line3<br />Line4<br />Line5
end note
`;
parser.parse(str);
});
it('should handle floating notes', function () {
const str = `stateDiagram-v2
foo: bar
note "This is a floating note" as N1
`;
parser.parse(str);
});
it('should handle floating notes', function () {
const str = `stateDiagram-v2\n
state foo
note "This is a floating note" as N1
`;
parser.parse(str);
});
it('should handle notes for composit states', function () {
const str = `stateDiagram-v2\n
[*] --> NotShooting
state "Not Shooting State" as NotShooting {
state "Idle mode" as Idle
state "Configuring mode" as Configuring
[*] --> Idle
Idle --> Configuring : EvConfig
Configuring --> Idle : EvConfig
}
note right of NotShooting : This is a note on a composite state
`;
parser.parse(str);
});
});
});

View File

@@ -24,6 +24,32 @@ describe('state diagram, ', function () {
`;
parser.parse(str);
const description = stateDb.getAccDescription();
expect(description).toBe('');
});
it('simple with accDescription', function () {
const str = `stateDiagram\n
accDescription a simple description of the diagram
State1 : this is another string
[*] --> State1
State1 --> [*]
`;
parser.parse(str);
const description = stateDb.getAccDescription();
expect(description).toBe('a simple description of the diagram');
});
it('simple with title', function () {
const str = `stateDiagram\n
title a simple title of the diagram
State1 : this is another string
[*] --> State1
State1 --> [*]
`;
parser.parse(str);
const title = stateDb.getTitle();
expect(title).toBe('a simple title of the diagram');
});
it('simple with directive', function () {
const str = `%%{init: {'logLevel': 0 }}%%
@@ -119,7 +145,7 @@ describe('state diagram, ', function () {
parser.parse(str);
});
it('shall handle descriptions inkluding minus signs', function () {
it('shall handle descriptions including minus signs', function () {
const str = `stateDiagram\n
scale 350 width
[*] --> State1 : This is the description +-!

View File

@@ -7,6 +7,7 @@ import { render } from '../../dagre-wrapper/index.js';
import { log } from '../../logger';
import { configureSvgSize } from '../../utils';
import common from '../common/common';
import addSVGAccessibilityFields from '../../accessibility';
const conf = {};
export const setConf = function (cnf) {
@@ -336,6 +337,7 @@ export const draw = function (text, id) {
label.insertBefore(rect, label.firstChild);
// }
}
addSVGAccessibilityFields(parser.yy, svg, id);
};
export default {

View File

@@ -9,6 +9,7 @@ import { parser } from './parser/stateDiagram';
import { drawState, addTitleAndBox, drawEdge } from './shapes';
import { getConfig } from '../../config';
import { configureSvgSize } from '../../utils';
import addSVGAccessibilityFields from '../../accessibility';
parser.yy = stateDb;
@@ -97,6 +98,7 @@ export const draw = function (text, id) {
'viewBox',
`${bounds.x - conf.padding} ${bounds.y - conf.padding} ` + width + ' ' + height
);
addSVGAccessibilityFields(parser.yy, diagram, id);
};
const getLabelWidth = (text) => {
return text ? text.length * conf.fontSizeFactor : 1;