Refactored rendering sequence diagrams

This commit is contained in:
Chris Moran 2020-06-17 05:54:24 -04:00
parent fd7dbaf0a7
commit 3c72d28511
No known key found for this signature in database
GPG Key ID: FBD13F2A0E1B9152
17 changed files with 1334 additions and 930 deletions

View File

@ -122,10 +122,23 @@ context('Sequence diagram', () => {
it('should render long actor descriptions', () => {
imgSnapshotTest(
`
%%{init: {'theme': 'dark'}}%%
sequenceDiagram
participant A as Extremely utterly long line of longness which had preivously overflown the actor box as it is much longer than what it should be
A->>Bob: Hola
Bob-->A: Pasten !
`,
{logLevel: 0}
);
});
it('should render long actor descriptions', () => {
imgSnapshotTest(
`
sequenceDiagram
%%{wrap}%%
participant A as Extremely utterly long line of longness which had preivously overflown the actor box as it is much longer than what it should be
A->>Bob: Hola
Bob-->A: Pasten !
`,
{}
);
@ -141,6 +154,17 @@ context('Sequence diagram', () => {
{}
);
});
it('should render long notes wrapped (inline) left of actor', () => {
imgSnapshotTest(
`
sequenceDiagram
Alice->>Bob: Hola
Note left of Alice:wrap: Extremely utterly long line of longness which had preivously overflown the actor box as it is much longer than what it should be
Bob->>Alice: I'm short though
`,
{}
);
});
it('should render long notes right of actor', () => {
imgSnapshotTest(
`
@ -152,6 +176,17 @@ context('Sequence diagram', () => {
{}
);
});
it('should render long notes wrapped (inline) right of actor', () => {
imgSnapshotTest(
`
sequenceDiagram
Alice->>Bob: Hola
Note right of Alice:wrap: Extremely utterly long line of longness which had preivously overflown the actor box as it is much longer than what it should be
Bob->>Alice: I'm short though
`,
{}
);
});
it('should render long notes over actor', () => {
imgSnapshotTest(
`
@ -163,6 +198,17 @@ context('Sequence diagram', () => {
{}
);
});
it('should render long notes wrapped (inline) over actor', () => {
imgSnapshotTest(
`
sequenceDiagram
Alice->>Bob: Hola
Note over Alice:wrap: Extremely utterly long line of longness which had preivously overflown the actor box as it is much longer than what it should be
Bob->>Alice: I'm short though
`,
{}
);
});
it('should render long messages from an actor to the left to one to the right', () => {
imgSnapshotTest(
`
@ -173,6 +219,16 @@ context('Sequence diagram', () => {
{}
);
});
it('should render long messages wrapped (inline) from an actor to the left to one to the right', () => {
imgSnapshotTest(
`
sequenceDiagram
Alice->>Bob:wrap: Extremely utterly long line of longness which had preivously overflown the actor box as it is much longer than what it should be
Bob->>Alice: I'm short though
`,
{}
);
});
it('should render long messages from an actor to the right to one to the left', () => {
imgSnapshotTest(
`
@ -183,6 +239,16 @@ context('Sequence diagram', () => {
{}
);
});
it('should render long messages wrapped (inline) from an actor to the right to one to the left', () => {
imgSnapshotTest(
`
sequenceDiagram
Alice->>Bob: I'm short
Bob->>Alice:wrap: Extremely utterly long line of longness which had preivously overflown the actor box as it is much longer than what it should be
`,
{}
);
});
});
context('background rects', () => {
it('should render a single and nested rects', () => {
@ -327,5 +393,33 @@ context('Sequence diagram', () => {
{}
);
});
it('should render dark theme from init directive and size 24 font set from config directive', () => {
imgSnapshotTest(
`
%%{init: {'theme': 'dark'}}%%
sequenceDiagram
%%{config: {'fontSize': 24}}%%
Alice->>John: Hello John, how are you?
Alice->>John: John, can you hear me?
John-->>Alice: Hi Alice, I can hear you!
John-->>Alice: I feel great!
`,
{}
);
});
it('should render with wrapping enabled', () => {
imgSnapshotTest(
`
sequenceDiagram
%%{wrap}%%
participant A as Alice, the talkative one
A->>John: Hello John, how are you today? I'm feeling quite verbose today.
A->>John: John, can you hear me? If you are not available, we can talk later.
John-->>A: Hi Alice, I can hear you! I was finishing up an important meeting.
John-->>A: I feel great! I was not ignoring you. I am sorry you had to wait for a response.
`,
{}
);
});
});
});

736
dist/mermaid.core.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

736
dist/mermaid.js vendored

File diff suppressed because one or more lines are too long

2
dist/mermaid.js.map vendored

File diff suppressed because one or more lines are too long

12
dist/mermaid.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -454,12 +454,11 @@ mermaidAPI.initialize({
### Parameters
- `id` the id of the element to be rendered
- `_txt`
- `_txt` the graph definition
- `cb` callback which is called after rendering is finished with the svg code as inparam.
- `container` selector to element in which a div with the graph temporarily will be inserted. In one is
provided a hidden div will be inserted in the body of the page instead. The element will be removed when rendering is
completed.
- `txt` the graph definition
##

View File

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

View File

@ -1,6 +1,4 @@
import { logger } from '../../logger';
import { getConfig, setConfig } from '../../config';
import mermaidAPI from '../../mermaidAPI';
let prevActor = undefined;
let actors = {};
@ -10,7 +8,6 @@ let title = '';
let titleWrapped = false;
let sequenceNumbersEnabled = false;
let wrapEnabled = false;
let configUpdated = false;
let currentDirective = {};
export const parseDirective = function(statement, context) {
@ -46,10 +43,7 @@ const handleDirective = function(directive) {
switch (directive.type) {
case 'init':
case 'initialize':
mermaidAPI.initialize(directive.args);
break;
case 'config':
updateConfig(directive.args);
logger.debug('init/initialize is handled in mermaid/mermaidAPI');
break;
case 'wrap':
case 'nowrap':
@ -57,7 +51,9 @@ const handleDirective = function(directive) {
break;
default:
logger.warn(
`Unrecognized directive: source: '%%{${directive.type}: ${directive.args}}%%`,
`Unhandled directive: source: '%%{${directive.type}: ${JSON.stringify(
directive.args ? directive.args : {}
)}}%%`,
directive
);
break;
@ -77,7 +73,7 @@ export const addActor = function(id, name, description) {
actors[id] = {
name: name,
description: description.text,
wrap: (description.wrap === null && autoWrap()) || !!description.wrap,
wrap: (description.wrap === undefined && autoWrap()) || !!description.wrap,
prevActor: prevActor
};
if (prevActor && actors[prevActor]) {
@ -111,12 +107,17 @@ export const addMessage = function(idFrom, idTo, message, answer) {
from: idFrom,
to: idTo,
message: message.text,
wrap: (message.wrap === null && autoWrap()) || !!message.wrap,
wrap: (message.wrap === undefined && autoWrap()) || !!message.wrap,
answer: answer
});
};
export const addSignal = function(idFrom, idTo, message = { text: null, wrap: null }, messageType) {
export const addSignal = function(
idFrom,
idTo,
message = { text: undefined, wrap: undefined },
messageType
) {
logger.debug(
'Adding message from=' +
idFrom +
@ -150,7 +151,7 @@ export const addSignal = function(idFrom, idTo, message = { text: null, wrap: nu
from: idFrom,
to: idTo,
message: message.text,
wrap: (message.wrap === null && autoWrap()) || !!message.wrap,
wrap: (message.wrap === undefined && autoWrap()) || !!message.wrap,
type: messageType
});
return true;
@ -180,12 +181,8 @@ export const enableSequenceNumbers = function() {
};
export const showSequenceNumbers = () => sequenceNumbersEnabled;
export const enableWrap = function() {
wrapEnabled = true;
};
export const disableWrap = function() {
wrapEnabled = false;
export const setWrap = function(wrapSetting) {
wrapEnabled = wrapSetting;
};
export const autoWrap = () => wrapEnabled;
@ -193,7 +190,6 @@ export const autoWrap = () => wrapEnabled;
export const clear = function() {
actors = {};
messages = [];
configUpdated = false;
};
export const parseMessage = function(str) {
@ -251,7 +247,7 @@ export const addNote = function(actor, placement, message) {
actor: actor,
placement: placement,
message: message.text,
wrap: (message.wrap === null && autoWrap()) || !!message.wrap
wrap: (message.wrap === undefined && autoWrap()) || !!message.wrap
};
// Coerce actor into a [to, from, ...] array
@ -262,7 +258,7 @@ export const addNote = function(actor, placement, message) {
from: actors[0],
to: actors[1],
message: message.text,
wrap: (message.wrap === null && autoWrap()) || !!message.wrap,
wrap: (message.wrap === undefined && autoWrap()) || !!message.wrap,
type: LINETYPE.NOTE,
placement: placement
});
@ -270,20 +266,7 @@ export const addNote = function(actor, placement, message) {
export const setTitle = function(titleWrap) {
title = titleWrap.text;
titleWrapped = (titleWrap.wrap === null && autoWrap()) || !!titleWrap.wrap;
};
export const updateConfig = function(config = getConfig()) {
try {
setConfig(config);
configUpdated = true;
} catch (error) {
logger.error('Error: unable to parse config');
}
};
export const hasConfigChange = function() {
return configUpdated;
titleWrapped = (titleWrap.wrap === undefined && autoWrap()) || !!titleWrap.wrap;
};
export const apply = function(param) {
@ -355,20 +338,16 @@ export default {
addActor,
addMessage,
addSignal,
enableWrap,
disableWrap,
autoWrap,
setWrap,
enableSequenceNumbers,
showSequenceNumbers,
autoWrap,
getMessages,
getActors,
getActor,
getActorKeys,
getTitle,
parseDirective,
hasConfigChange,
getConfig,
updateConfig,
getTitleWrapped,
clear,
parseMessage,

View File

@ -1,10 +1,9 @@
import { select, selectAll } from 'd3';
import svgDraw from './svgDraw';
import svgDraw, { drawText } from './svgDraw';
import { logger } from '../../logger';
import { parser } from './parser/sequenceDiagram';
import common from '../common/common';
import sequenceDb from './sequenceDb';
import { getConfig } from '../../config';
parser.yy = sequenceDb;
@ -79,9 +78,6 @@ export const bounds = {
stopy: undefined
};
this.verticalPos = 0;
if (parser.yy.hasConfigChange()) {
setConf(getConfig());
}
},
updateVal: function(obj, key, val, fun) {
if (typeof obj[key] === 'undefined') {
@ -153,15 +149,20 @@ export const bounds = {
.lastIndexOf(message.from.actor);
return this.activations.splice(lastActorActivationIdx, 1)[0];
},
newLoop: function(title, fill) {
this.sequenceItems.push({
createLoop: function(title = { message: undefined, wrap: false, width: undefined }, fill) {
return {
startx: undefined,
starty: this.verticalPos,
stopx: undefined,
stopy: undefined,
title: title,
title: title.message,
wrap: title.wrap,
width: title.width,
fill: fill
});
};
},
newLoop: function(title = { message: undefined, wrap: false, width: undefined }, fill) {
this.sequenceItems.push(this.createLoop(title, fill));
},
endLoop: function() {
return this.sequenceItems.pop();
@ -186,7 +187,7 @@ export const bounds = {
}
};
const wrapLabel = (label, maxWidth, joinWith = '<br/>') => {
export const wrapLabel = (label, maxWidth, joinWith = '<br/>', cnf = conf) => {
if (common.lineBreakRegex.test(label)) {
return label;
}
@ -194,8 +195,13 @@ const wrapLabel = (label, maxWidth, joinWith = '<br/>') => {
const completedLines = [];
let nextLine = '';
words.forEach((word, index) => {
const wordLength = calculateTextWidth(`${word} `);
const nextLineLength = calculateTextWidth(nextLine);
const wordLength = calculateTextWidth(`${word} `, cnf.fontSize, cnf.fontFamily, cnf.fontWeight);
const nextLineLength = calculateTextWidth(
nextLine,
cnf.fontSize,
cnf.fontFamily,
cnf.fontWeight
);
if (wordLength > maxWidth) {
const { hyphenatedStrings, remainingWord } = breakString(word, maxWidth);
completedLines.push(nextLine, ...hyphenatedStrings);
@ -235,10 +241,7 @@ const breakString = (word, maxWidth, hyphenCharacter = '-') => {
return { hyphenatedStrings: lines, remainingWord: currentLine };
};
const _drawLongText = (text, x, y, g, width) => {
let textHeight = 0;
let prevTextHeight = 0;
const drawLongText = (text, x, y, g, width) => {
const alignmentToAnchor = {
left: 'start',
start: 'start',
@ -247,43 +250,45 @@ const _drawLongText = (text, x, y, g, width) => {
right: 'end',
end: 'end'
};
const lines = text.split(common.lineBreakRegex);
for (const line of lines) {
const textObj = svgDraw.getTextObj();
const alignment = alignmentToAnchor[conf.noteAlign] || 'middle';
switch (alignment) {
case 'start':
textObj.x = x + conf.noteMargin;
break;
case 'middle':
textObj.x = x + width / 2;
break;
case 'end':
textObj.x = x + width - conf.noteMargin;
break;
}
textObj.y = y + textHeight;
textObj.dy = '1em';
textObj.text = line;
textObj.class = 'noteText';
const textElem = svgDraw
.drawText(g, textObj)
.style('text-anchor', alignment)
.style('font-size', conf.noteFontSize)
.style('font-family', conf.noteFontFamily)
.style('font-weight', conf.noteFontWeight)
.attr('dominant-baseline', 'central')
.attr('alignment-baseline', 'central');
textHeight += (textElem._groups || textElem)[0][0].getBBox().height;
textElem.attr('y', y + (prevTextHeight + textHeight + 2 * conf.noteMargin) / 2);
prevTextHeight = textHeight;
const alignment = alignmentToAnchor[conf.noteAlign] || 'middle';
const textObj = svgDraw.getTextObj();
switch (alignment) {
case 'start':
textObj.x = x + conf.noteMargin;
break;
case 'middle':
textObj.x = x + width / 2;
break;
case 'end':
textObj.x = x + width - conf.noteMargin;
break;
}
return textHeight;
textObj.y = y;
textObj.dy = '1em';
textObj.text = text;
textObj.class = 'noteText';
textObj.fontFamily = conf.noteFontFamily;
textObj.fontSize = conf.noteFontSize;
textObj.fontWeight = conf.noteFontWeight;
textObj.anchor = alignment;
textObj.textMargin = conf.noteMargin;
textObj.valign = alignment;
textObj.wrap = true;
let textElem = drawText(g, textObj);
if (!Array.isArray(textElem)) {
textElem = [textElem];
}
textElem.forEach(te => {
te.attr('dominant-baseline', 'central').attr('alignment-baseline', 'central');
});
return textElem
.map(te => (te._groups || te)[0][0].getBBox().height)
.reduce((acc, curr) => acc + curr);
};
/**
@ -304,7 +309,7 @@ const drawNote = function(elem, startx, verticalPos, msg, forceWidth) {
let g = elem.append('g');
const rectElem = svgDraw.drawRect(g, rect);
const textHeight = _drawLongText(msg.message, startx, verticalPos, g, rect.width);
const textHeight = drawLongText(msg.message, startx, verticalPos, g, rect.width);
bounds.insert(
startx,
@ -331,26 +336,6 @@ const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceInde
const txtCenter = startx + (stopx - startx) / 2;
let textElems = [];
/*
let textHeight = 0;
const breaklines = msg.message.split(common.lineBreakRegex);
for (const breakline of breaklines) {
let textElem = g
.append('text') // text label for the x axis
.attr('x', txtCenter)
.attr('y', verticalPos + textHeight)
.style('font-size', conf.messageFontSize)
.style('font-family', conf.messageFontFamily)
.style('font-weight', conf.messageFontWeight)
.style('text-anchor', 'middle')
.attr('class', 'messageText')
.text(breakline.trim());
textElems.push(textElem);
textHeight += (textElem._groups || textElem)[0][0].getBBox().height;
}
let totalOffset = textHeight;
*/
let counterBreaklines = 0;
let breaklineOffset = conf.messageFontSize + 4;
@ -500,24 +485,13 @@ export const drawActors = function(diagram, actors, actorKeys, verticalPos) {
// Draw the actors
let prevWidth = 0;
let prevMargin = 0;
let maxActorHeight = conf.height;
for (let i = 0; i < actorKeys.length; i++) {
const actor = actors[actorKeys[i]];
// Add some rendering data to the object
actor.width = actor.width || calculateActorWidth(actor);
actor.height = actor.wrap
? calculateTextHeight(
actor.message,
conf.height,
actor.width,
conf.wrapPadding,
actor.wrap,
conf.actorFontSize
)
: conf.height;
maxActorHeight = Math.max(maxActorHeight, actor.height);
actor.height = conf.height;
actor.margin = actor.margin || conf.actorMargin;
actor.x = prevWidth + prevMargin;
@ -535,7 +509,7 @@ export const drawActors = function(diagram, actors, actorKeys, verticalPos) {
}
// Add a margin between the actor boxes and the first arrow
bounds.bumpVerticalPos(maxActorHeight);
bounds.bumpVerticalPos(conf.height);
};
export const setConf = function(cnf) {
@ -645,15 +619,16 @@ export const calculateTextHeight = function(
* @param fontSize - The font size of the given text
* @param fontFamily - The font family (one, or more fonts) to render
* @param fontWeight - The font weight (normal, bold, italics)
* @param pad - Whether to add the left and right wrapPadding to the width (default: true)
*/
export const calculateTextWidth = function(text, fontSize, fontFamily, fontWeight) {
export const calculateTextWidth = function(text, fontSize, fontFamily, fontWeight, pad = true) {
if (!text) {
return 0;
}
fontSize = fontSize ? fontSize : conf.actorFontSize;
fontFamily = fontFamily ? fontFamily : conf.actorFontFamily;
fontWeight = fontWeight ? fontWeight : conf.actorFontWeight;
fontSize = fontSize ? fontSize : conf.fontSize;
fontFamily = fontFamily ? fontFamily : conf.fontFamily;
fontWeight = fontWeight ? fontWeight : conf.fontWeight;
// We can't really know if the user supplied font family will render on the user agent;
// thus, we'll take the max width between the user supplied font family, and a default
@ -676,7 +651,7 @@ export const calculateTextWidth = function(text, fontSize, fontFamily, fontWeigh
const textObj = svgDraw.getTextObj();
textObj.text = line;
const textElem = svgDraw
.drawText(g, textObj)
.drawSimpleText(g, textObj)
.style('font-size', fontSize)
.style('font-weight', fontWeight)
.style('font-family', fontFamily);
@ -688,9 +663,38 @@ export const calculateTextWidth = function(text, fontSize, fontFamily, fontWeigh
g.remove();
// Adds some padding, so the text won't sit exactly within the actor's borders
return maxWidth + conf.wrapPadding * 2;
return maxWidth + (pad ? conf.wrapPadding * 2 : 0);
};
function adjustLoopHeightForWrap(loopWidths, msg, preMargin, postMargin, addLoopFn) {
let heightAdjust = 0;
bounds.bumpVerticalPos(preMargin);
if (msg.message && msg.wrap && loopWidths[msg.message]) {
let loopWidth = loopWidths[msg.message].width;
let minSize =
Math.round((3 * conf.fontSize) / 4) < 10
? conf.fontSize
: Math.round((3 * conf.fontSize) / 4);
msg.message = msg.message
? wrapLabel(`[ ${msg.message} ]`, loopWidth - 2 * conf.wrapPadding, '<br/>', {
fontSize: minSize,
fontFamily: conf.fontFamily,
fontWeight: conf.fontWeight
})
: msg.message;
heightAdjust = calculateTextHeight(
msg.message,
minSize,
loopWidth,
conf.wrapPadding,
msg.wrap,
minSize
);
}
addLoopFn(msg);
bounds.bumpVerticalPos(heightAdjust + postMargin);
}
/**
* Draws a flowchart in the tag with id: id based on the graph definition in text.
* @param text
@ -698,6 +702,7 @@ export const calculateTextWidth = function(text, fontSize, fontFamily, fontWeigh
*/
export const draw = function(text, id) {
parser.yy.clear();
parser.yy.setWrap(conf.wrapEnabled);
parser.parse(text + '\n');
bounds.init();
@ -714,15 +719,10 @@ export const draw = function(text, id) {
const title = parser.yy.getTitle();
const maxMessageWidthPerActor = getMaxMessageWidthPerActor(actors, messages);
const maxActorHeight = calculateActorMargins(actors, maxMessageWidthPerActor);
conf.height = calculateActorMargins(actors, maxMessageWidthPerActor);
drawActors(diagram, actors, actorKeys, 0);
bounds.bumpVerticalPos(
maxActorHeight > conf.height
? Math.min(conf.boxMargin, Math.abs(maxActorHeight - conf.height))
: 0
);
const loopWidths = calculateLoopMargins(messages, actors);
// The arrow head definition is attached to the svg once
svgDraw.insertArrowHead(diagram);
@ -826,13 +826,16 @@ export const draw = function(text, id) {
activeEnd(msg, bounds.getVerticalPos());
break;
case parser.yy.LINETYPE.LOOP_START:
bounds.bumpVerticalPos(conf.boxMargin);
bounds.newLoop(msg.message);
bounds.bumpVerticalPos(conf.boxMargin + conf.boxTextMargin);
adjustLoopHeightForWrap(
loopWidths,
msg,
conf.boxMargin,
conf.boxMargin + conf.boxTextMargin,
message => bounds.newLoop(message)
);
break;
case parser.yy.LINETYPE.LOOP_END:
loopData = bounds.endLoop();
svgDraw.drawLoop(diagram, loopData, 'loop', conf);
bounds.bumpVerticalPos(conf.boxMargin);
break;
@ -841,51 +844,56 @@ export const draw = function(text, id) {
bounds.newLoop(undefined, msg.message);
bounds.bumpVerticalPos(conf.boxMargin);
break;
case parser.yy.LINETYPE.RECT_END: {
const rectData = bounds.endLoop();
svgDraw.drawBackgroundRect(diagram, rectData);
case parser.yy.LINETYPE.RECT_END:
svgDraw.drawBackgroundRect(diagram, bounds.endLoop());
bounds.bumpVerticalPos(conf.boxMargin);
break;
}
case parser.yy.LINETYPE.OPT_START:
bounds.bumpVerticalPos(conf.boxMargin);
bounds.newLoop(msg.message);
bounds.bumpVerticalPos(conf.boxMargin + conf.boxTextMargin);
adjustLoopHeightForWrap(
loopWidths,
msg,
conf.boxMargin,
conf.boxMargin + conf.boxTextMargin,
message => bounds.newLoop(message)
);
break;
case parser.yy.LINETYPE.OPT_END:
loopData = bounds.endLoop();
svgDraw.drawLoop(diagram, loopData, 'opt', conf);
bounds.bumpVerticalPos(conf.boxMargin);
break;
case parser.yy.LINETYPE.ALT_START:
bounds.bumpVerticalPos(conf.boxMargin);
bounds.newLoop(msg.message);
bounds.bumpVerticalPos(conf.boxMargin + conf.boxTextMargin);
adjustLoopHeightForWrap(
loopWidths,
msg,
conf.boxMargin,
conf.boxMargin + conf.boxTextMargin,
message => bounds.newLoop(message)
);
break;
case parser.yy.LINETYPE.ALT_ELSE:
bounds.bumpVerticalPos(conf.boxMargin);
loopData = bounds.addSectionToLoop(msg.message);
bounds.bumpVerticalPos(conf.boxMargin);
adjustLoopHeightForWrap(loopWidths, msg, conf.boxMargin, conf.boxMargin, message =>
bounds.addSectionToLoop(message)
);
break;
case parser.yy.LINETYPE.ALT_END:
loopData = bounds.endLoop();
svgDraw.drawLoop(diagram, loopData, 'alt', conf);
bounds.bumpVerticalPos(conf.boxMargin);
break;
case parser.yy.LINETYPE.PAR_START:
bounds.bumpVerticalPos(conf.boxMargin);
if (shouldWrap) {
msg.message = wrapLabel(msg.message, conf.boxMargin);
}
bounds.newLoop(msg.message);
bounds.bumpVerticalPos(conf.boxMargin + conf.boxTextMargin);
adjustLoopHeightForWrap(
loopWidths,
msg,
conf.boxMargin,
conf.boxMargin + conf.boxTextMargin,
message => bounds.newLoop(message)
);
break;
case parser.yy.LINETYPE.PAR_AND:
bounds.bumpVerticalPos(conf.boxMargin);
loopData = bounds.addSectionToLoop(msg.message);
bounds.bumpVerticalPos(conf.boxMargin);
adjustLoopHeightForWrap(loopWidths, msg, conf.boxMargin, conf.boxMargin, message =>
bounds.addSectionToLoop(message)
);
break;
case parser.yy.LINETYPE.PAR_END:
loopData = bounds.endLoop();
@ -971,7 +979,7 @@ export const draw = function(text, id) {
if (conf.useMaxWidth) {
diagram.attr('height', '100%');
diagram.attr('width', '100%');
diagram.attr('style', 'max-width:' + width + 'px;');
diagram.attr('style', 'max-width:100%;');
} else {
diagram.attr('height', height);
diagram.attr('width', width);
@ -1125,13 +1133,16 @@ const calculateActorMargins = function(actors, actorToMessageWidth) {
);
act.height = act.wrap
? calculateTextHeight(
act.description,
conf.height,
actor.width,
conf.actorMargin,
act.wrap,
conf.actorFontSize
? Math.max(
calculateTextHeight(
act.description,
conf.height,
actor.width,
conf.wrapPadding,
act.wrap,
conf.actorFontSize
),
conf.height
)
: conf.height;
maxHeight = Math.max(maxHeight, act.height);
@ -1142,11 +1153,57 @@ const calculateActorMargins = function(actors, actorToMessageWidth) {
actor.margin = Math.max(actorWidth, conf.actorMargin);
}
Object.keys(actors).forEach(function(key) {
actors[key].height = maxHeight;
});
return maxHeight;
return Math.max(maxHeight, conf.height);
};
const calculateLoopMargins = function(messages, actors) {
const loops = {};
const stack = [];
let current;
messages.forEach(function(msg) {
switch (msg.type) {
case parser.yy.LINETYPE.LOOP_START:
case parser.yy.LINETYPE.ALT_START:
case parser.yy.LINETYPE.OPT_START:
case parser.yy.LINETYPE.PAR_START:
stack.push({
msg: msg.message,
from: Number.MAX_SAFE_INTEGER,
to: Number.MIN_SAFE_INTEGER,
width: 0
});
break;
case parser.yy.LINETYPE.ALT_ELSE:
case parser.yy.LINETYPE.PAR_AND:
if (msg.message) {
current = stack.pop();
loops[msg.message] = current;
stack.push(current);
}
break;
case parser.yy.LINETYPE.LOOP_END:
case parser.yy.LINETYPE.ALT_END:
case parser.yy.LINETYPE.OPT_END:
case parser.yy.LINETYPE.PAR_END:
current = stack.pop();
loops[current.msg] = current;
break;
}
if (msg.from && msg.to && stack.length > 0) {
current = stack.pop();
let from = actors[msg.from];
let to = actors[msg.to];
current.from = Math.min(current.from, from.x - from.width / 2);
current.to = Math.max(current.to, to.x + to.width / 2);
current.width =
Math.abs(current.from - current.to) - 2 * conf.wrapPadding - 40 /*2 * labelBoxWidth*/;
stack.push(current);
}
});
logger.debug('LoopWidths:', loops);
return loops;
};
export default {

View File

@ -18,7 +18,7 @@ export const drawRect = function(elem, rectData) {
return rectElem;
};
export const drawText = function(elem, textData) {
export const drawSimpleText = function(elem, textData) {
// Remove and ignore br:s
const nText = textData.text.replace(common.lineBreakRegex, ' ');
@ -42,6 +42,98 @@ export const drawText = function(elem, textData) {
return textElem;
};
export const drawText = function(elem, textData) {
let prevTextHeight = 0,
textHeight = 0;
const lines = textData.wrap
? textData.text.split(common.lineBreakRegex)
: [textData.text.replace(common.lineBreakRegex, ' ')];
let textElems = [];
let dy = 0;
let yfunc = () => textData.y;
if (
typeof textData.valign !== 'undefined' &&
typeof textData.textMargin !== 'undefined' &&
textData.textMargin > 0
) {
switch (textData.valign) {
case 'top':
case 'start':
yfunc = () => textData.y + textData.textMargin;
break;
case 'middle':
case 'center':
yfunc = () => textData.y + (prevTextHeight + textHeight + textData.textMargin) / 2;
break;
case 'bottom':
case 'end':
yfunc = () =>
textData.y +
(prevTextHeight + textHeight + 2 * textData.textMargin) -
textData.textMargin;
break;
}
}
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
if (
typeof textData.textMargin !== 'undefined' &&
textData.textMargin === 0 &&
typeof textData.fontSize !== 'undefined'
) {
dy = i * textData.fontSize;
}
const textElem = elem.append('text');
textElem.attr('x', textData.x);
textElem.attr('y', yfunc());
if (typeof textData.anchor !== 'undefined') {
textElem.style('text-anchor', textData.anchor);
}
if (typeof textData.fontFamily !== 'undefined') {
textElem.style('font-family', textData.fontFamily);
}
if (typeof textData.fontSize !== 'undefined') {
textElem.style('font-size', textData.fontSize);
}
if (typeof textData.fontWeight !== 'undefined') {
textElem.style('font-weight', textData.fontWeight);
}
if (typeof textData.fill !== 'undefined') {
textElem.attr('fill', textData.fill);
}
if (typeof textData.class !== 'undefined') {
textElem.attr('class', textData.class);
}
if (typeof textData.dy !== 'undefined') {
textElem.attr('dy', textData.dy);
} else if (dy !== 0) {
textElem.attr('dy', dy);
}
const span = textElem.append('tspan');
span.attr('x', textData.x);
if (typeof textData.fill !== 'undefined') {
span.attr('fill', textData.fill);
}
span.text(line);
if (
typeof textData.valign !== 'undefined' &&
typeof textData.textMargin !== 'undefined' &&
textData.textMargin > 0
) {
textHeight += (textElem._groups || textElem)[0][0].getBBox().height;
prevTextHeight = textHeight;
}
textElems.push(textElem);
}
return textElems.length === 1 ? textElems[0] : textElems;
};
export const drawLabel = function(elem, txtObject) {
function genPoints(x, y, width, height, cut) {
return (
@ -72,7 +164,7 @@ export const drawLabel = function(elem, txtObject) {
txtObject.y = txtObject.y + txtObject.labelMargin;
txtObject.x = txtObject.x + 0.5 * txtObject.labelMargin;
drawText(elem, txtObject);
return drawText(elem, txtObject);
};
let actorCnt = -1;
@ -80,7 +172,7 @@ let actorCnt = -1;
* Draws an actor in the diagram with the attaced line
* @param elem - The diagram we'll draw to.
* @param actor - The actor to draw.
* @param conf - drawText implementation discriminator object
* @param conf - utils.drawText implementation discriminator object
*/
export const drawActor = function(elem, actor, conf) {
const center = actor.x + actor.width / 2;
@ -185,33 +277,39 @@ export const drawLoop = function(elem, bounds, labelText, conf) {
txt.fontWeight = conf.fontWeight;
txt.class = 'labelText'; // Its size & position are fixed.
drawLabel(g, txt);
let labelElem = drawLabel(g, txt);
let labelBoxWidth = (labelElem._groups || labelElem)[0][0].getBBox().width;
txt = getTextObj();
txt.text = '[ ' + bounds.title + ' ]';
txt.x = bounds.startx + (bounds.stopx - bounds.startx) / 2;
txt.y = bounds.starty + 1.5 * conf.boxMargin;
txt.text = bounds.title;
txt.x = bounds.startx + (bounds.stopx - bounds.startx) / 2 + labelBoxWidth;
txt.y = bounds.starty + conf.boxMargin + conf.boxTextMargin;
txt.anchor = 'middle';
txt.class = 'loopText';
txt.fontFamily = conf.fontFamily;
txt.fontSize = minSize;
txt.fontWeight = conf.fontWeight;
txt.wrap = bounds.wrap;
let textElem = drawText(g, txt);
let textHeight = (textElem._groups || textElem)[0][0].getBBox().height;
drawText(g, txt);
if (typeof bounds.sectionTitles !== 'undefined') {
bounds.sectionTitles.forEach(function(item, idx) {
if (item !== '') {
txt.text = '[ ' + item + ' ]';
if (item.message) {
txt.text = item.message;
txt.x = bounds.startx + (bounds.stopx - bounds.startx) / 2;
txt.y = bounds.sections[idx] + 1.5 * conf.boxMargin;
textElem = drawText(g, txt);
textHeight += (textElem._groups || textElem)[0][0].getBBox().height;
txt.class = 'loopText';
txt.anchor = 'middle';
txt.fontFamily = conf.fontFamily;
txt.fontSize = minSize;
txt.fontWeight = conf.fontWeight;
txt.wrap = bounds.wrap;
drawText(g, txt);
}
});
}
return textHeight + 4;
return g;
};
/**
@ -300,23 +398,23 @@ export const insertArrowCrossHead = function(elem) {
};
export const getTextObj = function() {
const txt = {
return {
x: 0,
y: 0,
fill: undefined,
'text-anchor': 'start',
anchor: 'start',
style: '#666',
width: 100,
height: 100,
textMargin: 0,
rx: 0,
ry: 0
ry: 0,
valign: undefined
};
return txt;
};
export const getNoteRect = function() {
const rect = {
return {
x: 0,
y: 0,
fill: '#EDF2AE',
@ -327,7 +425,6 @@ export const getNoteRect = function() {
rx: 0,
ry: 0
};
return rect;
};
const _drawTextCandidateFunc = (function() {
@ -412,6 +509,7 @@ const _drawTextCandidateFunc = (function() {
export default {
drawRect,
drawText,
drawSimpleText,
drawLabel,
drawActor,
anchorElement,

View File

@ -6,6 +6,8 @@
import decode from 'entity-decode/browser';
import mermaidAPI from './mermaidAPI';
import { logger } from './logger';
import utils from './utils';
/**
* ## init
* Function that goes through the document to find the chart definitions in there and render them.
@ -98,6 +100,11 @@ const init = function() {
.trim()
.replace(/<br\s*\/?>/gi, '<br/>');
const init = utils.detectInit(txt);
if (init) {
logger.debug('Detected early reinit: ', init);
}
try {
mermaidAPI.render(
id,

View File

@ -561,11 +561,6 @@ setLogLevel(config.logLevel);
setConfig(config);
function parse(text) {
const graphInit = utils.detectInit(text);
if (graphInit) {
reinitialize(graphInit);
logger.debug('Init ', graphInit);
}
const graphType = utils.detectType(text);
let parser;
@ -692,7 +687,7 @@ export const decodeEntities = function(text) {
* });
*```
* @param id the id of the element to be rendered
* @param txt the graph definition
* @param _txt the graph definition
* @param cb callback which is called after rendering is finished with the svg code as inparam.
* @param container selector to element in which a div with the graph temporarily will be inserted. In one is
* provided a hidden div will be inserted in the body of the page instead. The element will be removed when rendering is
@ -741,10 +736,6 @@ const render = function(id, _txt, cb, container) {
txt = encodeEntities(txt);
const element = select('#d' + id).node();
const graphInit = utils.detectInit(txt);
if (graphInit) {
reinitialize(graphInit);
}
const graphType = utils.detectType(txt);
// insert inline style into svg
@ -927,51 +918,18 @@ const render = function(id, _txt, cb, container) {
const setConf = function(cnf) {
// Top level initially mermaid, gflow, sequenceDiagram and gantt
const lvl1Keys = Object.keys(cnf);
for (let i = 0; i < lvl1Keys.length; i++) {
if (typeof cnf[lvl1Keys[i]] === 'object' && cnf[lvl1Keys[i]] != null) {
const lvl2Keys = Object.keys(cnf[lvl1Keys[i]]);
for (let j = 0; j < lvl2Keys.length; j++) {
logger.debug('Setting conf ', lvl1Keys[i], '-', lvl2Keys[j]);
if (typeof config[lvl1Keys[i]] === 'undefined') {
config[lvl1Keys[i]] = {};
}
logger.debug(
'Setting config: ' +
lvl1Keys[i] +
' ' +
lvl2Keys[j] +
' to ' +
cnf[lvl1Keys[i]][lvl2Keys[j]]
);
config[lvl1Keys[i]][lvl2Keys[j]] = cnf[lvl1Keys[i]][lvl2Keys[j]];
}
} else {
config[lvl1Keys[i]] = cnf[lvl1Keys[i]];
}
}
utils.assignWithDepth(config, cnf);
};
function reinitialize(options) {
function initialize(options) {
logger.debug('Initializing mermaidAPI ', pkg.version);
// Update default config with options supplied at initialization
if (typeof options === 'object') {
setConf(options);
}
setConfig(config);
setLogLevel(config.logLevel);
logger.debug('RE-Initializing mermaidAPI ', { version: pkg.version, options, config });
}
function initialize(options) {
let _config = config;
logger.debug('Initializing mermaidAPI ', { version: pkg.version, options, _config });
// Update default config with options supplied at initialization
if (typeof options === 'object') {
_config = Object.assign(_config, options);
setConf(_config);
}
setConfig(_config);
setLogLevel(_config.logLevel);
logger.debug(`Initialized mermaidAPI ${JSON.stringify(config, null, 2)}`);
}
// function getConfig () {
@ -983,7 +941,6 @@ const mermaidAPI = {
render,
parse,
initialize,
reinitialize,
getConfig
};

View File

@ -1,10 +1,10 @@
$mainBkg: #1f2020;
$secondBkg: lighten(#1f2020, 16);
$mainContrastColor: lightgrey;
$secondBkg: lighten(#1f2020, 24);
$mainContrastColor: rgba(226, 226, 226, .75);
$darkTextColor: #323D47;
$lineColor: $mainContrastColor;
$border1: #81B1DB;
$border2: rgba(255, 255, 255, 0.25);
$lineColor: lighten($mainContrastColor, 24);
$border1: #81b1db;
$border2: darken(#81b1db, 32);
$arrowheadColor: $mainContrastColor;
/* Flowchart variables */
@ -31,10 +31,10 @@ $labelTextColor: $mainContrastColor;
$loopTextColor: $mainContrastColor;
$noteBorderColor: $border2;
$noteBkgColor: #fff5ad;
$noteTextColor: $mainBkg;
$noteTextColor: #1f2020;
$activationBorderColor: $border1;
$activationBkgColor: $secondBkg;
$sequenceNumberColor: white;
$sequenceNumberColor: $mainContrastColor;
/* Gantt chart variables */

View File

@ -18,7 +18,9 @@
}
/* Classes common for multiple diagrams */
body {
background-color: $mainBkg;
}
.error-icon {
fill: $errorBkgColor;
}

View File

@ -13,6 +13,7 @@ import {
} from 'd3';
import { logger } from './logger';
import { sanitizeUrl } from '@braintree/sanitize-url';
import mermaidAPI from './mermaidAPI';
// Effectively an enum of the supported curve types, accessible by name
const d3CurveTypes = {
@ -34,7 +35,7 @@ const anyComment = /\s*%%.*\n/gm;
/**
* @function detectInit
* Detects the init config object from the text
* Detects the init config object from the text and (re)initializes mermaid
* ```mermaid
* %%{init: {"theme": "debug", "logLevel": 1 }}%%
* graph LR
@ -60,17 +61,20 @@ const anyComment = /\s*%%.*\n/gm;
* ```
*
* @param {string} text The text defining the graph
* @returns {object} the json object representing the init to pass to mermaid.initialize()
* @returns {object} the json object representing the init passed to mermaid.initialize()
*/
export const detectInit = function(text) {
let inits = detectDirective(text, /(?:init\b)|(?:initialize\b)/);
let results = {};
if (Array.isArray(inits)) {
let args = inits.map(init => init.args);
results = Object.assign(results, ...args);
results = assignWithDepth(results, ...args);
} else {
results = inits.args;
}
if (results) {
mermaidAPI.initialize(results);
}
return results;
};
@ -382,6 +386,34 @@ export const generateId = () => {
);
};
export const assignWithDepth = function(dst, src, depth = 2) {
if (depth <= 0) {
if (dst !== undefined && dst !== null && typeof dst === 'object' && typeof src === 'object') {
return Object.assign(dst, src);
} else {
return src;
}
}
if (src !== undefined && src !== null && typeof dst === 'object' && typeof src === 'object') {
let optionsKeys = Object.keys(src);
for (let i = 0; i < optionsKeys.length; i++) {
let key = optionsKeys[i];
if (
typeof src[key] === 'object' &&
(dst[key] === undefined || typeof dst[key] === 'object')
) {
if (dst[key] === undefined) {
dst[key] = {};
}
dst[key] = assignWithDepth(dst[key], src[key], depth - 1);
} else {
dst[key] = src[key];
}
}
}
return dst;
};
export default {
detectInit,
detectDirective,
@ -393,5 +425,6 @@ export default {
formatUrl,
getStylesFromArray,
generateId,
runFunc
runFunc,
assignWithDepth
};