Refactored rendering sequence diagrams

Fixed default config clobbering issues
This commit is contained in:
Chris Moran
2020-06-17 18:12:01 -04:00
parent 5f257119d6
commit 67c2fe8005
9 changed files with 537 additions and 306 deletions

View File

@@ -122,7 +122,6 @@ context('Sequence diagram', () => {
it('should render long actor descriptions', () => { it('should render long actor descriptions', () => {
imgSnapshotTest( imgSnapshotTest(
` `
%%{init: {'theme': 'dark'}}%%
sequenceDiagram 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 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 A->>Bob: Hola
@@ -131,11 +130,22 @@ context('Sequence diagram', () => {
{logLevel: 0} {logLevel: 0}
); );
}); });
it('should render long actor descriptions', () => { it('should wrap (inline) long actor descriptions', () => {
imgSnapshotTest( imgSnapshotTest(
` `
sequenceDiagram sequenceDiagram
%%{wrap}%% participant A as wrap: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 wrap (directive) long actor descriptions', () => {
imgSnapshotTest(
`
%%{init: {'config': {'wrapEnabled': true }}}%%
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 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 A->>Bob: Hola
Bob-->A: Pasten ! Bob-->A: Pasten !
@@ -282,6 +292,69 @@ context('Sequence diagram', () => {
{} {}
); );
}); });
it('should render a single and nested opt with long test overflowing', () => {
imgSnapshotTest(
`
sequenceDiagram
participant A
participant B
participant C
participant D
participant E
participant G
A ->>+ B: Task 1
opt this is an opt with a long title that will overflow
B ->>+ C: Task 2
C -->>- B: Return
end
A ->> D: Task 3
opt this is another opt with a long title that will overflow
D ->>+ E: Task 4
opt this is a nested opt with a long title that will overflow
E ->>+ G: Task 5
G -->>- E: Return
end
E ->> E: Task 6
end
D -->> A: Complete
`,
{}
);
});
it('should render a single and nested opt with long test wrapping', () => {
imgSnapshotTest(
`
%%{init: { 'config': { 'wrapEnabled': true } } }%%
sequenceDiagram
participant A
participant B
participant C
participant D
participant E
participant G
A ->>+ B: Task 1
opt this is an opt with a long title that will overflow
B ->>+ C: Task 2
C -->>- B: Return
end
A ->> D: Task 3
opt this is another opt with a long title that will overflow
D ->>+ E: Task 4
opt this is a nested opt with a long title that will overflow
E ->>+ G: Task 5
G -->>- E: Return
end
E ->> E: Task 6
end
D -->> A: Complete
`,
{}
);
});
it('should render rect around and inside loops', () => { it('should render rect around and inside loops', () => {
imgSnapshotTest( imgSnapshotTest(
` `
@@ -393,12 +466,11 @@ context('Sequence diagram', () => {
{} {}
); );
}); });
it('should render dark theme from init directive and size 24 font set from config directive', () => { it('should render dark theme from init directive and configure font size 24 font', () => {
imgSnapshotTest( imgSnapshotTest(
` `
%%{init: {'theme': 'dark'}}%% %%{init: {'theme': 'dark', 'config': {'fontSize': 24}}}%%
sequenceDiagram sequenceDiagram
%%{config: {'fontSize': 24}}%%
Alice->>John: Hello John, how are you? Alice->>John: Hello John, how are you?
Alice->>John: John, can you hear me? Alice->>John: John, can you hear me?
John-->>Alice: Hi Alice, I can hear you! John-->>Alice: Hi Alice, I can hear you!
@@ -410,8 +482,8 @@ context('Sequence diagram', () => {
it('should render with wrapping enabled', () => { it('should render with wrapping enabled', () => {
imgSnapshotTest( imgSnapshotTest(
` `
%%{init: { 'config': { 'wrapEnabled': true }}}%%
sequenceDiagram sequenceDiagram
%%{wrap}%%
participant A as Alice, the talkative one participant A as Alice, the talkative one
A->>John: Hello John, how are you today? I'm feeling quite verbose today. 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. A->>John: John, can you hear me? If you are not available, we can talk later.
@@ -426,8 +498,8 @@ context('Sequence diagram', () => {
it('should overide config with directive settings', () => { it('should overide config with directive settings', () => {
imgSnapshotTest( imgSnapshotTest(
` `
%%{init: { "sequence": { "mirrorActors": true }}}%%
sequenceDiagram sequenceDiagram
%%{config: { "mirrorActors": true} }%%
Alice->>Bob: I'm short Alice->>Bob: I'm short
note left of Alice: config set to mirrorActors: false<br/>directive set to mirrorActors: true note left of Alice: config set to mirrorActors: false<br/>directive set to mirrorActors: true
Bob->>Alice: Short as well Bob->>Alice: Short as well
@@ -438,6 +510,7 @@ context('Sequence diagram', () => {
it('should overide config with directive settings', () => { it('should overide config with directive settings', () => {
imgSnapshotTest( imgSnapshotTest(
` `
%%{init: { "sequence": { "mirrorActors": false }}}%%
sequenceDiagram sequenceDiagram
%%{config: { "mirrorActors": false} }%% %%{config: { "mirrorActors": false} }%%
Alice->>Bob: I'm short Alice->>Bob: I'm short

View File

@@ -1,14 +1,8 @@
import utils from './utils'; import { assignWithDepth } from './utils';
const config = {}; const config = {};
const setConf = function(cnf) {
// Top level initially mermaid, gflow, sequenceDiagram and gantt
utils.assignWithDepth(config, cnf);
};
export const setConfig = conf => { export const setConfig = conf => {
setConf(conf); assignWithDepth(config, conf);
}; };
export const getConfig = () => config; export const getConfig = () => config;

View File

@@ -1,4 +1,6 @@
import { logger } from '../../logger'; import { logger } from '../../logger';
import mermaidAPI from '../../mermaidAPI';
import { detectType } from '../../utils';
let prevActor = undefined; let prevActor = undefined;
let actors = {}; let actors = {};
@@ -44,6 +46,14 @@ const handleDirective = function(directive) {
case 'init': case 'init':
case 'initialize': case 'initialize':
logger.debug('init/initialize is handled in mermaid/mermaidAPI'); logger.debug('init/initialize is handled in mermaid/mermaidAPI');
['config'].forEach(prop => {
if (typeof directive.args[prop] !== 'undefined') {
directive.args.sequence = directive.args[prop];
delete directive.args[prop];
}
});
mermaidAPI.initialize(directive.args);
break; break;
case 'wrap': case 'wrap':
case 'nowrap': case 'nowrap':
@@ -194,7 +204,7 @@ export const clear = function() {
export const parseMessage = function(str) { export const parseMessage = function(str) {
const _str = str.trim(); const _str = str.trim();
return { const retVal = {
text: _str.replace(/^[:]?(?:no)?wrap:/, '').trim(), text: _str.replace(/^[:]?(?:no)?wrap:/, '').trim(),
wrap: wrap:
_str.match(/^[:]?(?:no)?wrap:/) === null _str.match(/^[:]?(?:no)?wrap:/) === null
@@ -205,6 +215,8 @@ export const parseMessage = function(str) {
? false ? false
: autoWrap() : autoWrap()
}; };
logger.debug(`ParseMessage[${str}] [${JSON.stringify(retVal, null, 2)}`);
return retVal;
}; };
export const LINETYPE = { export const LINETYPE = {

View File

@@ -1,7 +1,7 @@
/* eslint-env jasmine */ /* eslint-env jasmine */
import { parser } from './parser/sequenceDiagram'; import { parser } from './parser/sequenceDiagram';
import sequenceDb from './sequenceDb'; import sequenceDb from './sequenceDb';
import renderer, { calculateTextHeight, calculateTextWidth } from './sequenceRenderer'; import renderer from './sequenceRenderer';
import mermaidAPI from '../../mermaidAPI'; import mermaidAPI from '../../mermaidAPI';
function addConf(conf, key, value) { function addConf(conf, key, value) {
@@ -10,7 +10,26 @@ function addConf(conf, key, value) {
} }
return conf; return conf;
} }
describe('when processing', function() {
beforeEach(function() {
parser.yy = sequenceDb;
parser.yy.clear();
});
it('should handle long opts', function() {
const str = `
%%{init: {'config': { 'fontFamily': 'Menlo'}}}%%
sequenceDiagram
participant A as wrap: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 !
`;
mermaidAPI.parse(str);
renderer.setConf(mermaidAPI.getConfig().sequence);
renderer.draw(str, 'tst');
const messages = parser.yy.getMessages();
expect(messages).toBeTruthy();
});
});
describe('when parsing a sequenceDiagram', function() { describe('when parsing a sequenceDiagram', function() {
beforeEach(function() { beforeEach(function() {
parser.yy = sequenceDb; parser.yy = sequenceDb;
@@ -923,9 +942,12 @@ describe('when rendering a sequenceDiagram', function() {
boxMargin: 10, boxMargin: 10,
messageMargin: 40, messageMargin: 40,
boxTextMargin: 15, boxTextMargin: 15,
noteMargin: 25 noteMargin: 25,
wrapEnabled: false,
mirrorActors: false
}; };
renderer.setConf(conf); renderer.setConf(conf);
renderer.bounds.init();
}); });
['tspan', 'fo', 'old', undefined].forEach(function(textPlacement) { ['tspan', 'fo', 'old', undefined].forEach(function(textPlacement) {
it(` it(`
@@ -1035,7 +1057,7 @@ Alice->Bob: Hello Bob, how are you?`;
sequenceDiagram sequenceDiagram
Alice->Bob: Hello Bob, how are you?`; Alice->Bob: Hello Bob, how are you?`;
parser.parse(str); mermaidAPI.parse(str);
renderer.draw(str, 'tst'); renderer.draw(str, 'tst');
const bounds = renderer.bounds.getBounds(); const bounds = renderer.bounds.getBounds();
@@ -1044,7 +1066,7 @@ Alice->Bob: Hello Bob, how are you?`;
expect(bounds.startx).toBe(0); expect(bounds.startx).toBe(0);
expect(bounds.starty).toBe(0); expect(bounds.starty).toBe(0);
expect(bounds.stopx).toBe(conf.width * 2 + conf.actorMargin); expect(bounds.stopx).toBe(conf.width * 2 + conf.actorMargin);
expect(bounds.stopy).toBe(0 + conf.messageMargin + conf.height); expect(bounds.stopy).toBe(conf.messageMargin + conf.height);
}); });
it('it should handle two actors with init directive with multiline directive', function() { it('it should handle two actors with init directive with multiline directive', function() {
renderer.bounds.init(); renderer.bounds.init();
@@ -1221,13 +1243,14 @@ Bob->>Alice: Fine!`;
it('it should draw two actors notes to the left with text wrapped and the init directive sets the theme to dark and fontFamily to Menlo, fontSize to 18, and fontWeight to 800', function() { it('it should draw two actors notes to the left with text wrapped and the init directive sets the theme to dark and fontFamily to Menlo, fontSize to 18, and fontWeight to 800', function() {
renderer.bounds.init(); renderer.bounds.init();
const str = ` const str = `
%%{init: { "theme": "dark" }}%% %%{init: { "theme": "dark", 'config': { "fontFamily": "Menlo", "fontSize": 18, "fontWeight": 400, "wrapEnabled": true }}}%%
sequenceDiagram sequenceDiagram
%%{config: { "fontFamily": "Menlo", "fontSize": 18, "fontWeight": 400 } }%%
%%{wrap}%%
Alice->>Bob: Hello Bob, how are you? If you are not available right now, I can leave you a message. Please get back to me as soon as you can! Alice->>Bob: Hello Bob, how are you? If you are not available right now, I can leave you a message. Please get back to me as soon as you can!
Note left of Alice: Bob thinks Note left of Alice: Bob thinks
Bob->>Alice: Fine!`; Bob->>Alice: Fine!`;
mermaidAPI.parse(str);
renderer.setConf(mermaidAPI.getConfig().sequence);
parser.yy.clear();
parser.parse(str); parser.parse(str);
renderer.draw(str, 'tst'); renderer.draw(str, 'tst');
@@ -1237,9 +1260,9 @@ Bob->>Alice: Fine!`;
expect(bounds.startx).toBe(-(conf.width / 2) - conf.actorMargin / 2); expect(bounds.startx).toBe(-(conf.width / 2) - conf.actorMargin / 2);
expect(bounds.starty).toBe(0); expect(bounds.starty).toBe(0);
expect(mermaid.theme).toBe('dark'); expect(mermaid.theme).toBe('dark');
expect(mermaid.fontFamily).toBe('Menlo'); expect(mermaid.sequence.fontFamily).toBe('Menlo');
expect(mermaid.fontSize).toBe(18); expect(mermaid.sequence.fontSize).toBe(18);
expect(mermaid.fontWeight).toBe(400); expect(mermaid.sequence.fontWeight).toBe(400);
expect(msgs.every(v => v.wrap)).toBe(true); expect(msgs.every(v => v.wrap)).toBe(true);
expect(bounds.stopx).toBe(conf.width * 2 + conf.actorMargin); expect(bounds.stopx).toBe(conf.width * 2 + conf.actorMargin);

View File

@@ -4,6 +4,7 @@ import { logger } from '../../logger';
import { parser } from './parser/sequenceDiagram'; import { parser } from './parser/sequenceDiagram';
import common from '../common/common'; import common from '../common/common';
import sequenceDb from './sequenceDb'; import sequenceDb from './sequenceDb';
import utils from '../../utils';
parser.yy = sequenceDb; parser.yy = sequenceDb;
@@ -187,60 +188,6 @@ export const bounds = {
} }
}; };
export const wrapLabel = (label, maxWidth, joinWith = '<br/>', cnf = conf) => {
if (common.lineBreakRegex.test(label)) {
return label;
}
const words = label.split(' ');
const completedLines = [];
let nextLine = '';
words.forEach((word, index) => {
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);
nextLine = remainingWord;
} else if (nextLineLength + wordLength >= maxWidth) {
completedLines.push(nextLine);
nextLine = word;
} else {
nextLine = [nextLine, word].filter(Boolean).join(' ');
}
const currentWord = index + 1;
const isLastWord = currentWord === words.length;
if (isLastWord) {
completedLines.push(nextLine);
}
});
return completedLines.filter(line => line !== '').join(joinWith);
};
const breakString = (word, maxWidth, hyphenCharacter = '-') => {
const characters = word.split('');
const lines = [];
let currentLine = '';
characters.forEach((character, index) => {
const nextLine = `${currentLine}${character}`;
const lineWidth = calculateTextWidth(nextLine);
if (lineWidth >= maxWidth) {
const currentCharacter = index + 1;
const isLastLine = characters.length === currentCharacter;
const hyphenatedNextLine = `${nextLine}${hyphenCharacter}`;
lines.push(isLastLine ? nextLine : hyphenatedNextLine);
currentLine = '';
} else {
currentLine = nextLine;
}
});
return { hyphenatedStrings: lines, remainingWord: currentLine };
};
const drawLongText = (text, x, y, g, width) => { const drawLongText = (text, x, y, g, width) => {
const alignmentToAnchor = { const alignmentToAnchor = {
left: 'start', left: 'start',
@@ -338,14 +285,13 @@ const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceInde
let textElems = []; let textElems = [];
let counterBreaklines = 0; let counterBreaklines = 0;
let breaklineOffset = conf.messageFontSize + 4; let breaklineOffset = conf.messageFontSize;
const breaklines = msg.message.split(common.lineBreakRegex); const breaklines = msg.message.split(common.lineBreakRegex);
for (const breakline of breaklines) { for (const breakline of breaklines) {
textElems.push( textElems.push(
g g
.append('text') // text label for the x axis .append('text') // text label for the x axis
.attr('x', txtCenter) .attr('x', txtCenter)
// .attr('y', verticalPos - breaklineVerticalOffset + counterBreaklines * breaklineOffset)
.attr('y', verticalPos - 7 + counterBreaklines * breaklineOffset) .attr('y', verticalPos - 7 + counterBreaklines * breaklineOffset)
.style('font-size', conf.messageFontSize) .style('font-size', conf.messageFontSize)
.style('font-family', conf.messageFontFamily) .style('font-family', conf.messageFontFamily)
@@ -492,14 +438,11 @@ export const drawActors = function(diagram, actors, actorKeys, verticalPos) {
// Add some rendering data to the object // Add some rendering data to the object
actor.width = actor.width || calculateActorWidth(actor); actor.width = actor.width || calculateActorWidth(actor);
actor.height = conf.height; actor.height = conf.height;
actor.margin = actor.margin || conf.actorMargin; actor.margin = conf.actorMargin;
actor.x = prevWidth + prevMargin; actor.x = prevWidth + prevMargin;
actor.y = verticalPos; actor.y = verticalPos;
if (actor.wrap) {
actor.description = wrapLabel(actor.description, actor.width);
}
// Draw the box with the attached line // Draw the box with the attached line
svgDraw.drawActor(diagram, actor, conf); svgDraw.drawActor(diagram, actor, conf);
bounds.insert(actor.x, verticalPos, actor.x + actor.width, actor.height); bounds.insert(actor.x, verticalPos, actor.x + actor.width, actor.height);
@@ -521,13 +464,18 @@ export const setConf = function(cnf) {
if (cnf.fontFamily) { if (cnf.fontFamily) {
conf.actorFontFamily = conf.noteFontFamily = conf.messageFontFamily = cnf.fontFamily; conf.actorFontFamily = conf.noteFontFamily = conf.messageFontFamily = cnf.fontFamily;
} else {
conf.fontFamily = conf.messageFontFamily;
} }
if (cnf.fontSize) { if (cnf.fontSize) {
conf.actorFontSize = conf.noteFontSize = conf.messageFontSize = cnf.fontSize; conf.actorFontSize = conf.noteFontSize = conf.messageFontSize = cnf.fontSize;
// conf.height = cnf.fontSize * (65 / 14); } else {
conf.fontSize = conf.messageFontSize;
} }
if (cnf.fontWeight) { if (cnf.fontWeight) {
conf.actorFontWeight = conf.noteFontWeight = conf.messageFontWeight = cnf.fontWeight; conf.actorFontWeight = conf.noteFontWeight = conf.messageFontWeight = cnf.fontWeight;
} else {
conf.fontWeight = conf.messageFontWeight;
} }
}; };
@@ -570,102 +518,15 @@ const calculateActorWidth = function(actor) {
? conf.width ? conf.width
: Math.max( : Math.max(
conf.width, conf.width,
calculateTextWidth( utils.calculateTextWidth(actor.description, {
actor.description, fontSize: conf.actorFontSize,
conf.actorFontSize, fontFamily: conf.actorFontFamily,
conf.actorFontFamily, fontWeight: conf.actorFontWeight,
conf.actorFontWeight margin: conf.wrapPadding
) })
); );
}; };
/**
* This calculates the text's height, taking into account the wrap value and
* both the statically configured height, width, and the length of the text (in pixels).
*
* If the wrapped text text has greater height, we extend the height, so it's
* value won't overflow.
*
* @return - The height for the given actor
* @param message the text to measure
* @param elementHeight the height of the default bounding box containing the text
* @param elementWidth the width of the default bounding box containing the text
* @param margin space above and below
* @param wrap wrap the text based on: elementWidth - 2 * margin
* @param fontSize
*/
export const calculateTextHeight = function(
message,
elementHeight,
elementWidth,
margin,
wrap,
fontSize
) {
if (!message) {
return elementHeight;
}
let lineHeightFactor = wrap
? wrapLabel(message, elementWidth - 2 * margin).split(common.lineBreakRegex).length
: 1;
return wrap ? Math.max(elementHeight, lineHeightFactor * fontSize) : elementHeight;
};
/**
* This calculates the width of the given text, font size and family.
*
* @param text - The text to calculate the width of
* @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, pad = true) {
if (!text) {
return 0;
}
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
// of sans-serif.
const fontFamilies = ['sans-serif', fontFamily];
const lines = text.split(common.lineBreakRegex);
let maxWidth = 0;
const body = select('body');
// We don't want to leak DOM elements - if a removal operation isn't available
// for any reason, do not continue.
if (!body.remove) {
return 0;
}
const g = body.append('svg');
for (let line of lines) {
for (let fontFamily of fontFamilies) {
const textObj = svgDraw.getTextObj();
textObj.text = line;
const textElem = svgDraw
.drawSimpleText(g, textObj)
.style('font-size', fontSize)
.style('font-weight', fontWeight)
.style('font-family', fontFamily);
maxWidth = Math.max(maxWidth, (textElem._groups || textElem)[0][0].getBBox().width);
}
}
g.remove();
// Adds some padding, so the text won't sit exactly within the actor's borders
return maxWidth + (pad ? conf.wrapPadding * 2 : 0);
};
function adjustLoopHeightForWrap(loopWidths, msg, preMargin, postMargin, addLoopFn) { function adjustLoopHeightForWrap(loopWidths, msg, preMargin, postMargin, addLoopFn) {
let heightAdjust = 0; let heightAdjust = 0;
bounds.bumpVerticalPos(preMargin); bounds.bumpVerticalPos(preMargin);
@@ -676,19 +537,22 @@ function adjustLoopHeightForWrap(loopWidths, msg, preMargin, postMargin, addLoop
? conf.fontSize ? conf.fontSize
: Math.round((3 * conf.fontSize) / 4); : Math.round((3 * conf.fontSize) / 4);
msg.message = msg.message msg.message = msg.message
? wrapLabel(`[ ${msg.message} ]`, loopWidth - 2 * conf.wrapPadding, '<br/>', { ? utils.wrapLabel(`[${msg.message}]`, loopWidth, {
fontSize: minSize, fontSize: minSize,
fontFamily: conf.fontFamily, fontFamily: conf.fontFamily,
fontWeight: conf.fontWeight fontWeight: conf.fontWeight,
margin: conf.wrapPadding
}) })
: msg.message; : msg.message;
heightAdjust = calculateTextHeight( heightAdjust = Math.max(
msg.message, 0,
minSize, utils.calculateTextHeight(msg.message, {
loopWidth, fontSize: minSize,
conf.wrapPadding, fontFamily: conf.fontFamily,
msg.wrap, fontWeight: conf.fontWeight,
minSize margin: conf.wrapPadding
}) -
(preMargin + postMargin)
); );
} }
addLoopFn(msg); addLoopFn(msg);
@@ -760,17 +624,12 @@ export const draw = function(text, id) {
startx = actors[msg.from].x; startx = actors[msg.from].x;
stopx = actors[msg.to].x; stopx = actors[msg.to].x;
textWidth = calculateTextWidth( textWidth = utils.calculateTextWidth(msg.message, conf);
msg.message,
conf.noteFontSize,
conf.noteFontFamily,
conf.noteFontWeight
);
noteWidth = shouldWrap ? conf.width : Math.max(conf.width, textWidth); noteWidth = shouldWrap ? conf.width : Math.max(conf.width, textWidth);
if (msg.placement === parser.yy.PLACEMENT.RIGHTOF) { if (msg.placement === parser.yy.PLACEMENT.RIGHTOF) {
if (shouldWrap) { if (shouldWrap) {
msg.message = wrapLabel(msg.message, noteWidth); msg.message = utils.wrapLabel(msg.message, noteWidth, conf);
} }
drawNote( drawNote(
diagram, diagram,
@@ -781,7 +640,7 @@ export const draw = function(text, id) {
); );
} else if (msg.placement === parser.yy.PLACEMENT.LEFTOF) { } else if (msg.placement === parser.yy.PLACEMENT.LEFTOF) {
if (shouldWrap) { if (shouldWrap) {
msg.message = wrapLabel(msg.message, noteWidth); msg.message = utils.wrapLabel(msg.message, noteWidth, conf);
} }
drawNote( drawNote(
diagram, diagram,
@@ -793,7 +652,7 @@ export const draw = function(text, id) {
} else if (msg.to === msg.from) { } else if (msg.to === msg.from) {
// Single-actor over // Single-actor over
if (shouldWrap) { if (shouldWrap) {
msg.message = wrapLabel(msg.message, noteWidth); msg.message = utils.wrapLabel(msg.message, noteWidth, conf);
} }
drawNote( drawNote(
diagram, diagram,
@@ -807,7 +666,7 @@ export const draw = function(text, id) {
forceWidth = Math.abs(startx - stopx) + conf.actorMargin / 2; forceWidth = Math.abs(startx - stopx) + conf.actorMargin / 2;
if (shouldWrap) { if (shouldWrap) {
noteWidth = forceWidth; noteWidth = forceWidth;
msg.message = wrapLabel(msg.message, noteWidth); msg.message = utils.wrapLabel(msg.message, noteWidth, conf);
} else { } else {
noteWidth = Math.max(forceWidth, textWidth - 2 * conf.noteMargin); noteWidth = Math.max(forceWidth, textWidth - 2 * conf.noteMargin);
} }
@@ -911,12 +770,13 @@ export const draw = function(text, id) {
startx = fromBounds[fromIdx]; startx = fromBounds[fromIdx];
stopx = toBounds[toIdx]; stopx = toBounds[toIdx];
if (shouldWrap) { if (shouldWrap) {
msg.message = wrapLabel( msg.message = utils.wrapLabel(
msg.message, msg.message,
Math.max( Math.max(
Math.abs(stopx - startx) + conf.messageMargin * 2, Math.abs(stopx - startx) + conf.messageMargin * 2,
conf.width + conf.messageMargin * 2 conf.width + conf.messageMargin * 2
) ),
conf
); );
} }
@@ -1031,12 +891,17 @@ const getMaxMessageWidthPerActor = function(actors, messages) {
const fontSize = isNote ? conf.noteFontSize : conf.messageFontSize; const fontSize = isNote ? conf.noteFontSize : conf.messageFontSize;
const fontFamily = isNote ? conf.noteFontFamily : conf.messageFontFamily; const fontFamily = isNote ? conf.noteFontFamily : conf.messageFontFamily;
const fontWeight = isNote ? conf.noteFontWeight : conf.messageFontWeight; const fontWeight = isNote ? conf.noteFontWeight : conf.messageFontWeight;
const messageWidth = calculateTextWidth( const textConf = { fontFamily, fontSize, fontWeight, margin: conf.wrapPadding };
msg.wrap ? wrapLabel(msg.message, conf.width - conf.noteMargin) : msg.message, let wrappedMessage = msg.wrap
? utils.wrapLabel(msg.message, conf.width - conf.noteMargin, textConf)
: msg.message;
const messageDimensions = utils.calculateTextDimensions(wrappedMessage, {
fontSize, fontSize,
fontFamily, fontFamily,
fontWeight fontWeight,
); margin: conf.wrapPadding
});
const messageWidth = messageDimensions.width;
/* /*
* The following scenarios should be supported: * The following scenarios should be supported:
@@ -1112,6 +977,13 @@ const calculateActorMargins = function(actors, actorToMessageWidth) {
continue; continue;
} }
const textConf = {
fontSize: conf.actorFontSize,
fontFamily: conf.actorFontFamily,
fontWeight: conf.actorFontWeight,
margin: conf.wrapPadding
};
const nextActor = actors[actor.nextActor]; const nextActor = actors[actor.nextActor];
// No need to space out an actor that doesn't have a next link // No need to space out an actor that doesn't have a next link
@@ -1120,31 +992,14 @@ const calculateActorMargins = function(actors, actorToMessageWidth) {
} }
[actor, nextActor].forEach(function(act) { [actor, nextActor].forEach(function(act) {
act.width = act.wrap if (act.wrap) {
? conf.width actor.description = utils.wrapLabel(actor.description, conf.width, textConf);
: Math.max( }
conf.width, const actDims = utils.calculateTextDimensions(act.description, textConf);
calculateTextWidth( act.width = act.wrap ? conf.width : Math.max(conf.width, actDims.width);
act.description,
conf.actorFontSize,
conf.actorFontFamily,
conf.actorFontWeight
)
);
act.height = act.wrap act.height = act.wrap ? Math.max(actDims.height, conf.height) : conf.height;
? Math.max( logger.debug(`Actor h:${act.height} ${actDims.height} d:${act.description}`);
calculateTextHeight(
act.description,
conf.height,
actor.width,
conf.wrapPadding,
act.wrap,
conf.actorFontSize
),
conf.height
)
: conf.height;
maxHeight = Math.max(maxHeight, act.height); maxHeight = Math.max(maxHeight, act.height);
}); });
@@ -1192,17 +1047,22 @@ const calculateLoopMargins = function(messages, actors) {
break; break;
} }
if (msg.from && msg.to && stack.length > 0) { if (msg.from && msg.to && stack.length > 0) {
current = stack.pop(); stack.forEach(stk => {
current = stk;
let from = actors[msg.from]; let from = actors[msg.from];
let to = actors[msg.to]; let to = actors[msg.to];
current.from = Math.min(current.from, from.x - from.width / 2); if (from.x < to.x) {
current.to = Math.max(current.to, to.x + to.width / 2); current.from = Math.min(current.from, from.x);
current.width = current.to = Math.max(current.to, to.x);
Math.abs(current.from - current.to) - 2 * conf.wrapPadding - 40 /*2 * labelBoxWidth*/; } else {
stack.push(current); current.from = Math.min(current.from, to.x);
current.to = Math.max(current.to, from.x);
}
current.width = Math.abs(current.from - current.to) - 20 + 2 * conf.wrapPadding;
});
} }
}); });
logger.debug('LoopWidths:', loops); logger.debug('LoopWidths:', { loops, actors });
return loops; return loops;
}; };

View File

@@ -18,30 +18,6 @@ export const drawRect = function(elem, rectData) {
return rectElem; return rectElem;
}; };
export const drawSimpleText = function(elem, textData) {
// Remove and ignore br:s
const nText = textData.text.replace(common.lineBreakRegex, ' ');
const textElem = elem.append('text');
textElem.attr('x', textData.x);
textElem.attr('y', textData.y);
textElem.style('text-anchor', textData.anchor);
textElem.style('font-family', textData.fontFamily);
textElem.style('font-size', textData.fontSize);
textElem.style('font-weight', textData.fontWeight);
textElem.attr('fill', textData.fill);
if (typeof textData.class !== 'undefined') {
textElem.attr('class', textData.class);
}
const span = textElem.append('tspan');
span.attr('x', textData.x + textData.textMargin * 2);
span.attr('fill', textData.fill);
span.text(nText);
return textElem;
};
export const drawText = function(elem, textData) { export const drawText = function(elem, textData) {
let prevTextHeight = 0, let prevTextHeight = 0,
textHeight = 0; textHeight = 0;
@@ -509,7 +485,6 @@ const _drawTextCandidateFunc = (function() {
export default { export default {
drawRect, drawRect,
drawText, drawText,
drawSimpleText,
drawLabel, drawLabel,
drawActor, drawActor,
anchorElement, anchorElement,

View File

@@ -15,7 +15,7 @@ import scope from 'scope-css';
import pkg from '../package.json'; import pkg from '../package.json';
import { setConfig, getConfig } from './config'; import { setConfig, getConfig } from './config';
import { logger, setLogLevel } from './logger'; import { logger, setLogLevel } from './logger';
import utils from './utils'; import utils, { assignWithDepth } from './utils';
import flowRenderer from './diagrams/flowchart/flowRenderer'; import flowRenderer from './diagrams/flowchart/flowRenderer';
import flowRendererV2 from './diagrams/flowchart/flowRenderer-v2'; import flowRendererV2 from './diagrams/flowchart/flowRenderer-v2';
import flowParser from './diagrams/flowchart/parser/flow'; import flowParser from './diagrams/flowchart/parser/flow';
@@ -565,7 +565,7 @@ setConfig(config);
function parse(text) { function parse(text) {
const graphInit = utils.detectInit(text); const graphInit = utils.detectInit(text);
if (graphInit) { if (graphInit) {
reinitialize(graphInit); initialize(graphInit);
logger.debug('Init ', graphInit); logger.debug('Init ', graphInit);
} }
const graphType = utils.detectType(text); const graphType = utils.detectType(text);
@@ -694,7 +694,7 @@ export const decodeEntities = function(text) {
* }); * });
*``` *```
* @param id the id of the element to be rendered * @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 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 * @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 * provided a hidden div will be inserted in the body of the page instead. The element will be removed when rendering is
@@ -708,6 +708,11 @@ const render = function(id, _txt, cb, container) {
if (_txt.length > cnf.maxTextSize) { if (_txt.length > cnf.maxTextSize) {
txt = 'graph TB;a[Maximum text size in diagram exceeded];style a fill:#faa'; txt = 'graph TB;a[Maximum text size in diagram exceeded];style a fill:#faa';
} }
const graphInit = utils.detectInit(txt);
if (graphInit) {
initialize(graphInit);
assignWithDepth(cnf, getConfig());
}
if (typeof container !== 'undefined') { if (typeof container !== 'undefined') {
container.innerHTML = ''; container.innerHTML = '';
@@ -745,10 +750,6 @@ const render = function(id, _txt, cb, container) {
txt = encodeEntities(txt); txt = encodeEntities(txt);
const element = select('#d' + id).node(); const element = select('#d' + id).node();
const graphInit = utils.detectInit(txt);
if (graphInit) {
reinitialize(graphInit);
}
const graphType = utils.detectType(txt); const graphType = utils.detectType(txt);
// insert inline style into svg // insert inline style into svg
@@ -929,17 +930,6 @@ const render = function(id, _txt, cb, container) {
return svgCode; return svgCode;
}; };
function reinitialize(options) {
// console.log('re-initialize ', options.logLevel, cnf.logLevel, getConfig().logLevel);
if (typeof options === 'object') {
// setConf(options);
setConfig(options);
}
// setConfig(config);
setLogLevel(getConfig().logLevel);
logger.debug('RE-Initializing mermaidAPI ', { version: pkg.version, options, config });
}
let firstInit = true; let firstInit = true;
function initialize(options) { function initialize(options) {
console.log('mermaidAPI.initialize'); console.log('mermaidAPI.initialize');
@@ -951,12 +941,8 @@ function initialize(options) {
} }
setConfig(options); setConfig(options);
} }
console.warn('Initializing mermaidAPI theme', { assignWithDepth(config, getConfig());
version: pkg.version, console.warn(`Initializing mermaidAPI: v${pkg.version} theme: ${config.theme}`);
options,
config,
current: getConfig().theme
});
setLogLevel(getConfig().logLevel); setLogLevel(getConfig().logLevel);
} }
@@ -970,7 +956,6 @@ const mermaidAPI = {
render, render,
parse, parse,
initialize, initialize,
reinitialize,
getConfig, getConfig,
setConfig, setConfig,
reset: () => { reset: () => {

View File

@@ -9,11 +9,12 @@ import {
curveNatural, curveNatural,
curveStep, curveStep,
curveStepAfter, curveStepAfter,
curveStepBefore curveStepBefore,
select
} from 'd3'; } from 'd3';
import { logger } from './logger'; import { logger } from './logger';
import { sanitizeUrl } from '@braintree/sanitize-url'; import { sanitizeUrl } from '@braintree/sanitize-url';
import mermaidAPI from './mermaidAPI'; import common from './diagrams/common/common';
// Effectively an enum of the supported curve types, accessible by name // Effectively an enum of the supported curve types, accessible by name
const d3CurveTypes = { const d3CurveTypes = {
@@ -68,12 +69,21 @@ export const detectInit = function(text) {
let results = {}; let results = {};
if (Array.isArray(inits)) { if (Array.isArray(inits)) {
let args = inits.map(init => init.args); let args = inits.map(init => init.args);
results = assignWithDepth(results, ...args); results = assignWithDepth(results, [...args]);
} else { } else {
results = inits.args; results = inits.args;
} }
if (results) { if (results) {
mermaidAPI.initialize(results); let type = detectType(text);
['config'].forEach(prop => {
if (typeof results[prop] !== 'undefined') {
if (type === 'flowchart-v2') {
type = 'flowchart';
}
results[type] = results[prop];
delete results[prop];
}
});
} }
return results; return results;
}; };
@@ -386,18 +396,47 @@ export const generateId = () => {
); );
}; };
export const assignWithDepth = function(dst, src, depth = 2) { /**
if (depth <= 0) { * @function assignWithDepth
* Extends the functionality of {@link ObjectConstructor.assign} with the ability to merge arbitrary-depth objects
* For each key in src with path `k` (recursively) performs an Object.assign(dst[`k`], src[`k`]) with
* a slight change from the typical handling of undefined for dst[`k`]: instead of raising an error,
* dst[`k`] is auto-initialized to {} and effectively merged with src[`k`]
* <p>
* Additionally, dissimilar types will not clobber unless the config.clobber parameter === true. Example:
* ```
* let config_0 = { foo: { bar: 'bar' }, bar: 'foo' };
* let config_1 = { foo: 'foo', bar: 'bar' };
* let result = assignWithDepth(config_0, config_1);
* console.log(result);
* //-> result: { foo: { bar: 'bar' }, bar: 'bar' }
* ```
* <p>
* Traditional Object.assign would have clobbered foo in config_0 with foo in config_1.
* <p>
* If src is a destructured array of objects and dst is not an array, assignWithDepth will apply each element of src to dst
* in order.
* @param dst:any - the destination of the merge
* @param src:any - the source object(s) to merge into destination
* @param config:{ depth: number, clobber: boolean } - depth: depth to traverse within src and dst for merging -
* clobber: should dissimilar types clobber (default: { depth: 2, clobber: false })
* @returns {*}
*/
export const assignWithDepth = function(dst, src, config) {
const { depth, clobber } = Object.assign({ depth: 2, clobber: false }, config);
if (Array.isArray(src) && !Array.isArray(dst)) {
src.forEach(s => assignWithDepth(dst, s, config));
return dst;
}
if (typeof dst === 'undefined' || depth <= 0) {
if (dst !== undefined && dst !== null && typeof dst === 'object' && typeof src === 'object') { if (dst !== undefined && dst !== null && typeof dst === 'object' && typeof src === 'object') {
return Object.assign(dst, src); return Object.assign(dst, src);
} else { } else {
return src; return src;
} }
} }
if (src !== undefined && src !== null && typeof dst === 'object' && typeof src === 'object') { if (typeof src !== 'undefined' && typeof dst === 'object' && typeof src === 'object') {
let optionsKeys = Object.keys(src); Object.keys(src).forEach(key => {
for (let i = 0; i < optionsKeys.length; i++) {
let key = optionsKeys[i];
if ( if (
typeof src[key] === 'object' && typeof src[key] === 'object' &&
(dst[key] === undefined || typeof dst[key] === 'object') (dst[key] === undefined || typeof dst[key] === 'object')
@@ -405,16 +444,209 @@ export const assignWithDepth = function(dst, src, depth = 2) {
if (dst[key] === undefined) { if (dst[key] === undefined) {
dst[key] = {}; dst[key] = {};
} }
dst[key] = assignWithDepth(dst[key], src[key], depth - 1); dst[key] = assignWithDepth(dst[key], src[key], { depth: depth - 1, clobber });
} else { } else if (clobber || (typeof dst[key] !== 'object' && typeof src[key] !== 'object')) {
dst[key] = src[key]; dst[key] = src[key];
} }
} });
} }
return dst; return dst;
}; };
export const getTextObj = function() {
return {
x: 0,
y: 0,
fill: undefined,
anchor: 'start',
style: '#666',
width: 100,
height: 100,
textMargin: 0,
rx: 0,
ry: 0,
valign: undefined
};
};
export const drawSimpleText = function(elem, textData) {
// Remove and ignore br:s
const nText = textData.text.replace(common.lineBreakRegex, ' ');
const textElem = elem.append('text');
textElem.attr('x', textData.x);
textElem.attr('y', textData.y);
textElem.style('text-anchor', textData.anchor);
textElem.style('font-family', textData.fontFamily);
textElem.style('font-size', textData.fontSize);
textElem.style('font-weight', textData.fontWeight);
textElem.attr('fill', textData.fill);
if (typeof textData.class !== 'undefined') {
textElem.attr('class', textData.class);
}
const span = textElem.append('tspan');
span.attr('x', textData.x + textData.textMargin * 2);
span.attr('fill', textData.fill);
span.text(nText);
return textElem;
};
export const wrapLabel = (label, maxWidth, config) => {
config = Object.assign(
{ fontSize: 12, fontWeight: 400, fontFamily: 'Arial', margin: 15, joinWith: '<br/>' },
config
);
if (common.lineBreakRegex.test(label)) {
return label;
}
const words = label.split(' ');
const completedLines = [];
let nextLine = '';
words.forEach((word, index) => {
const wordLength = calculateTextWidth(`${word} `, config);
const nextLineLength = calculateTextWidth(nextLine, config);
if (wordLength > maxWidth) {
const { hyphenatedStrings, remainingWord } = breakString(word, maxWidth, '-', config);
completedLines.push(nextLine, ...hyphenatedStrings);
nextLine = remainingWord;
} else if (nextLineLength + wordLength >= maxWidth) {
completedLines.push(nextLine);
nextLine = word;
} else {
nextLine = [nextLine, word].filter(Boolean).join(' ');
}
const currentWord = index + 1;
const isLastWord = currentWord === words.length;
if (isLastWord) {
completedLines.push(nextLine);
}
});
return completedLines.filter(line => line !== '').join(config.joinWith);
};
const breakString = (word, maxWidth, hyphenCharacter = '-', config) => {
config = Object.assign(
{ fontSize: 12, fontWeight: 400, fontFamily: 'Arial', margin: 15 },
config
);
const characters = word.split('');
const lines = [];
let currentLine = '';
characters.forEach((character, index) => {
const nextLine = `${currentLine}${character}`;
const lineWidth = calculateTextWidth(nextLine, config);
if (lineWidth >= maxWidth) {
const currentCharacter = index + 1;
const isLastLine = characters.length === currentCharacter;
const hyphenatedNextLine = `${nextLine}${hyphenCharacter}`;
lines.push(isLastLine ? nextLine : hyphenatedNextLine);
currentLine = '';
} else {
currentLine = nextLine;
}
});
return { hyphenatedStrings: lines, remainingWord: currentLine };
};
/**
* This calculates the text's height, taking into account the wrap breaks and
* both the statically configured height, width, and the length of the text (in pixels).
*
* If the wrapped text text has greater height, we extend the height, so it's
* value won't overflow.
*
* @return - The height for the given text
* @param text the text to measure
* @param config - the config for fontSize, fontFamily, fontWeight, and margin all impacting the resulting size
*/
export const calculateTextHeight = function(text, config) {
config = Object.assign(
{ fontSize: 12, fontWeight: 400, fontFamily: 'Arial', margin: 15 },
config
);
return calculateTextDimensions(text, config).height;
};
/**
* This calculates the width of the given text, font size and family.
*
* @return - The width for the given text
* @param text - The text to calculate the width of
* @param config - the config for fontSize, fontFamily, fontWeight, and margin all impacting the resulting size
*/
export const calculateTextWidth = function(text, config) {
config = Object.assign(
{ fontSize: 12, fontWeight: 400, fontFamily: 'Arial', margin: 15 },
config
);
return calculateTextDimensions(text, config).width;
};
/**
* This calculates the dimensions of the given text, font size, font family, font weight, and margins.
*
* @return - The width for the given text
* @param text - The text to calculate the width of
* @param config - the config for fontSize, fontFamily, fontWeight, and margin all impacting the resulting size
*/
export const calculateTextDimensions = function(text, config) {
config = Object.assign(
{ fontSize: 12, fontWeight: 400, fontFamily: 'Arial', margin: 15 },
config
);
const { fontSize, fontFamily, fontWeight, margin } = config;
if (!text) {
return 0;
}
// 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
// of sans-serif.
const fontFamilies = ['sans-serif', fontFamily];
const lines = text.split(common.lineBreakRegex);
let maxWidth = 0,
height = 0;
const body = select('body');
// We don't want to leak DOM elements - if a removal operation isn't available
// for any reason, do not continue.
if (!body.remove) {
return 0;
}
const g = body.append('svg');
for (let line of lines) {
let cheight = 0;
for (let fontFamily of fontFamilies) {
const textObj = getTextObj();
textObj.text = line;
const textElem = drawSimpleText(g, textObj)
.style('font-size', fontSize)
.style('font-weight', fontWeight)
.style('font-family', fontFamily);
let bBox = (textElem._groups || textElem)[0][0].getBBox();
maxWidth = Math.max(maxWidth, bBox.width);
cheight = Math.max(bBox.height, cheight);
}
height += cheight;
}
g.remove();
// Adds some padding, so the text won't sit exactly within the actor's borders
return { width: maxWidth + 2 * margin, height: height + 2 * margin };
};
export default { export default {
assignWithDepth,
wrapLabel,
calculateTextHeight,
calculateTextWidth,
calculateTextDimensions,
detectInit, detectInit,
detectDirective, detectDirective,
detectType, detectType,
@@ -425,6 +657,5 @@ export default {
formatUrl, formatUrl,
getStylesFromArray, getStylesFromArray,
generateId, generateId,
runFunc, runFunc
assignWithDepth
}; };

View File

@@ -1,6 +1,74 @@
/* eslint-env jasmine */ /* eslint-env jasmine */
import utils from './utils'; import utils from './utils';
describe('when assignWithDepth: should merge objects within objects', function() {
it('should handle simple, depth:1 types (identity)', function() {
let config_0 = { foo: 'bar', bar: 0 };
let config_1 = { foo: 'bar', bar: 0 };
let result = utils.assignWithDepth(config_0, config_1);
expect(result).toEqual(config_1);
});
it('should handle simple, depth:1 types (dst: undefined)', function() {
let config_0 = undefined;
let config_1 = { foo: 'bar', bar: 0 };
let result = utils.assignWithDepth(config_0, config_1);
expect(result).toEqual(config_1);
});
it('should handle simple, depth:1 types (src: undefined)', function() {
let config_0 = { foo: 'bar', bar: 0 };
let config_1 = undefined;
let result = utils.assignWithDepth(config_0, config_1);
expect(result).toEqual(config_0);
});
it('should handle simple, depth:1 types (merge)', function() {
let config_0 = { foo: 'bar', bar: 0 };
let config_1 = { foo: 'foo' };
let result = utils.assignWithDepth(config_0, config_1);
expect(result).toEqual({ foo: 'foo', bar: 0});
});
it('should handle depth:2 types (dst: orphan)', function() {
let config_0 = { foo: 'bar', bar: { foo: 'bar' } };
let config_1 = { foo: 'bar' };
let result = utils.assignWithDepth(config_0, config_1);
expect(result).toEqual(config_0);
});
it('should handle depth:2 types (dst: object, src: simple type)', function() {
let config_0 = { foo: 'bar', bar: { foo: 'bar' } };
let config_1 = { foo: 'foo', bar: 'should NOT clobber'};
let result = utils.assignWithDepth(config_0, config_1);
expect(result).toEqual({ foo: 'foo', bar: { foo: 'bar' } } );
});
it('should handle depth:2 types (src: orphan)', function() {
let config_0 = { foo: 'bar' };
let config_1 = { foo: 'bar', bar: { foo: 'bar' } };
let result = utils.assignWithDepth(config_0, config_1);
expect(result).toEqual(config_1);
});
it('should handle depth:2 types (merge)', function() {
let config_0 = { foo: 'bar', bar: { foo: 'bar' }, boofar: 1 };
let config_1 = { foo: 'foo', bar: { bar: 0 }, foobar: 'foobar' };
let result = utils.assignWithDepth(config_0, config_1);
expect(result).toEqual({ foo: "foo", bar: { foo: "bar", bar: 0 }, foobar: "foobar", boofar: 1 });
});
it('should handle depth:3 types (merge with clobber because assignWithDepth::depth == 2)', function() {
let config_0 = { foo: 'bar', bar: { foo: 'bar', bar: { foo: { message: 'this', willbe: 'clobbered' } } }, boofar: 1 };
let config_1 = { foo: 'foo', bar: { foo: 'foo', bar: { foo: { message: 'clobbered other foo' } } }, foobar: 'foobar' };
let result = utils.assignWithDepth(config_0, config_1);
expect(result).toEqual({ foo: "foo", bar: { foo: 'foo', bar: { foo: { message: 'clobbered other foo' } } }, foobar: "foobar", boofar: 1 });
});
it('should handle depth:3 types (merge with clobber because assignWithDepth::depth == 1)', function() {
let config_0 = { foo: 'bar', bar: { foo: 'bar', bar: { foo: { message: '', willNotbe: 'present' }, bar: 'shouldNotBePresent' } }, boofar: 1 };
let config_1 = { foo: 'foo', bar: { foo: 'foo', bar: { foo: { message: 'this' } } }, foobar: 'foobar' };
let result = utils.assignWithDepth(config_0, config_1, { depth: 1 });
expect(result).toEqual({ foo: "foo", bar: { foo: 'foo', bar: { foo: { message: 'this' } } }, foobar: "foobar", boofar: 1 });
});
it('should handle depth:3 types (merge with no clobber because assignWithDepth::depth == 3)', function() {
let config_0 = { foo: 'bar', bar: { foo: 'bar', bar: { foo: { message: '', willbe: 'present' } } }, boofar: 1 };
let config_1 = { foo: 'foo', bar: { foo: 'foo', bar: { foo: { message: 'this' } } }, foobar: 'foobar' };
let result = utils.assignWithDepth(config_0, config_1, { depth: 3 });
expect(result).toEqual({ foo: "foo", bar: { foo: 'foo', bar: { foo: { message: 'this', willbe: 'present' } } }, foobar: "foobar", boofar: 1 });
});
});
describe('when detecting chart type ', function() { describe('when detecting chart type ', function() {
it('should handle a graph definition', function() { it('should handle a graph definition', function() {
const str = 'graph TB\nbfs1:queue'; const str = 'graph TB\nbfs1:queue';
@@ -27,6 +95,16 @@ Alice->Bob: hi`;
expect(type).toBe('sequence'); expect(type).toBe('sequence');
expect(init).toEqual({logLevel:0,theme:"dark"}); expect(init).toEqual({logLevel:0,theme:"dark"});
}); });
it('should handle an init definition with config converted to the proper diagram configuration', function() {
const str = `
%%{init: { 'logLevel': 0, 'theme': 'dark', 'config': {'wrapEnabled': true} } }%%
sequenceDiagram
Alice->Bob: hi`;
const type = utils.detectType(str);
const init = utils.detectInit(str);
expect(type).toBe('sequence');
expect(init).toEqual({logLevel:0,theme:"dark", sequence: { wrapEnabled: true }});
});
it('should handle a multiline init definition', function() { it('should handle a multiline init definition', function() {
const str = ` const str = `
%%{ %%{