mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-08-31 06:06:44 +02:00
Refactored rendering sequence diagrams
Fixed default config clobbering issues
This commit is contained in:
@@ -122,7 +122,6 @@ 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
|
||||
@@ -131,11 +130,22 @@ context('Sequence diagram', () => {
|
||||
{logLevel: 0}
|
||||
);
|
||||
});
|
||||
it('should render long actor descriptions', () => {
|
||||
it('should wrap (inline) long actor descriptions', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
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
|
||||
A->>Bob: Hola
|
||||
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', () => {
|
||||
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(
|
||||
`
|
||||
%%{init: {'theme': 'dark'}}%%
|
||||
%%{init: {'theme': 'dark', 'config': {'fontSize': 24}}}%%
|
||||
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!
|
||||
@@ -410,8 +482,8 @@ context('Sequence diagram', () => {
|
||||
it('should render with wrapping enabled', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
%%{init: { 'config': { 'wrapEnabled': true }}}%%
|
||||
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.
|
||||
@@ -426,8 +498,8 @@ context('Sequence diagram', () => {
|
||||
it('should overide config with directive settings', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
%%{init: { "sequence": { "mirrorActors": true }}}%%
|
||||
sequenceDiagram
|
||||
%%{config: { "mirrorActors": true} }%%
|
||||
Alice->>Bob: I'm short
|
||||
note left of Alice: config set to mirrorActors: false<br/>directive set to mirrorActors: true
|
||||
Bob->>Alice: Short as well
|
||||
@@ -438,6 +510,7 @@ context('Sequence diagram', () => {
|
||||
it('should overide config with directive settings', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
%%{init: { "sequence": { "mirrorActors": false }}}%%
|
||||
sequenceDiagram
|
||||
%%{config: { "mirrorActors": false} }%%
|
||||
Alice->>Bob: I'm short
|
||||
|
@@ -1,14 +1,8 @@
|
||||
import utils from './utils';
|
||||
|
||||
import { assignWithDepth } from './utils';
|
||||
const config = {};
|
||||
|
||||
const setConf = function(cnf) {
|
||||
// Top level initially mermaid, gflow, sequenceDiagram and gantt
|
||||
utils.assignWithDepth(config, cnf);
|
||||
};
|
||||
|
||||
export const setConfig = conf => {
|
||||
setConf(conf);
|
||||
assignWithDepth(config, conf);
|
||||
};
|
||||
export const getConfig = () => config;
|
||||
|
||||
|
@@ -1,4 +1,6 @@
|
||||
import { logger } from '../../logger';
|
||||
import mermaidAPI from '../../mermaidAPI';
|
||||
import { detectType } from '../../utils';
|
||||
|
||||
let prevActor = undefined;
|
||||
let actors = {};
|
||||
@@ -44,6 +46,14 @@ const handleDirective = function(directive) {
|
||||
case 'init':
|
||||
case 'initialize':
|
||||
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;
|
||||
case 'wrap':
|
||||
case 'nowrap':
|
||||
@@ -194,7 +204,7 @@ export const clear = function() {
|
||||
|
||||
export const parseMessage = function(str) {
|
||||
const _str = str.trim();
|
||||
return {
|
||||
const retVal = {
|
||||
text: _str.replace(/^[:]?(?:no)?wrap:/, '').trim(),
|
||||
wrap:
|
||||
_str.match(/^[:]?(?:no)?wrap:/) === null
|
||||
@@ -205,6 +215,8 @@ export const parseMessage = function(str) {
|
||||
? false
|
||||
: autoWrap()
|
||||
};
|
||||
logger.debug(`ParseMessage[${str}] [${JSON.stringify(retVal, null, 2)}`);
|
||||
return retVal;
|
||||
};
|
||||
|
||||
export const LINETYPE = {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
/* eslint-env jasmine */
|
||||
import { parser } from './parser/sequenceDiagram';
|
||||
import sequenceDb from './sequenceDb';
|
||||
import renderer, { calculateTextHeight, calculateTextWidth } from './sequenceRenderer';
|
||||
import renderer from './sequenceRenderer';
|
||||
import mermaidAPI from '../../mermaidAPI';
|
||||
|
||||
function addConf(conf, key, value) {
|
||||
@@ -10,7 +10,26 @@ function addConf(conf, key, value) {
|
||||
}
|
||||
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() {
|
||||
beforeEach(function() {
|
||||
parser.yy = sequenceDb;
|
||||
@@ -923,9 +942,12 @@ describe('when rendering a sequenceDiagram', function() {
|
||||
boxMargin: 10,
|
||||
messageMargin: 40,
|
||||
boxTextMargin: 15,
|
||||
noteMargin: 25
|
||||
noteMargin: 25,
|
||||
wrapEnabled: false,
|
||||
mirrorActors: false
|
||||
};
|
||||
renderer.setConf(conf);
|
||||
renderer.bounds.init();
|
||||
});
|
||||
['tspan', 'fo', 'old', undefined].forEach(function(textPlacement) {
|
||||
it(`
|
||||
@@ -1035,7 +1057,7 @@ Alice->Bob: Hello Bob, how are you?`;
|
||||
sequenceDiagram
|
||||
Alice->Bob: Hello Bob, how are you?`;
|
||||
|
||||
parser.parse(str);
|
||||
mermaidAPI.parse(str);
|
||||
renderer.draw(str, 'tst');
|
||||
|
||||
const bounds = renderer.bounds.getBounds();
|
||||
@@ -1044,7 +1066,7 @@ Alice->Bob: Hello Bob, how are you?`;
|
||||
expect(bounds.startx).toBe(0);
|
||||
expect(bounds.starty).toBe(0);
|
||||
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() {
|
||||
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() {
|
||||
renderer.bounds.init();
|
||||
const str = `
|
||||
%%{init: { "theme": "dark" }}%%
|
||||
%%{init: { "theme": "dark", 'config': { "fontFamily": "Menlo", "fontSize": 18, "fontWeight": 400, "wrapEnabled": true }}}%%
|
||||
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!
|
||||
Note left of Alice: Bob thinks
|
||||
Bob->>Alice: Fine!`;
|
||||
mermaidAPI.parse(str);
|
||||
renderer.setConf(mermaidAPI.getConfig().sequence);
|
||||
parser.yy.clear();
|
||||
parser.parse(str);
|
||||
renderer.draw(str, 'tst');
|
||||
|
||||
@@ -1237,9 +1260,9 @@ Bob->>Alice: Fine!`;
|
||||
expect(bounds.startx).toBe(-(conf.width / 2) - conf.actorMargin / 2);
|
||||
expect(bounds.starty).toBe(0);
|
||||
expect(mermaid.theme).toBe('dark');
|
||||
expect(mermaid.fontFamily).toBe('Menlo');
|
||||
expect(mermaid.fontSize).toBe(18);
|
||||
expect(mermaid.fontWeight).toBe(400);
|
||||
expect(mermaid.sequence.fontFamily).toBe('Menlo');
|
||||
expect(mermaid.sequence.fontSize).toBe(18);
|
||||
expect(mermaid.sequence.fontWeight).toBe(400);
|
||||
expect(msgs.every(v => v.wrap)).toBe(true);
|
||||
|
||||
expect(bounds.stopx).toBe(conf.width * 2 + conf.actorMargin);
|
||||
|
@@ -4,6 +4,7 @@ import { logger } from '../../logger';
|
||||
import { parser } from './parser/sequenceDiagram';
|
||||
import common from '../common/common';
|
||||
import sequenceDb from './sequenceDb';
|
||||
import utils from '../../utils';
|
||||
|
||||
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 alignmentToAnchor = {
|
||||
left: 'start',
|
||||
@@ -338,14 +285,13 @@ const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceInde
|
||||
let textElems = [];
|
||||
|
||||
let counterBreaklines = 0;
|
||||
let breaklineOffset = conf.messageFontSize + 4;
|
||||
let breaklineOffset = conf.messageFontSize;
|
||||
const breaklines = msg.message.split(common.lineBreakRegex);
|
||||
for (const breakline of breaklines) {
|
||||
textElems.push(
|
||||
g
|
||||
.append('text') // text label for the x axis
|
||||
.attr('x', txtCenter)
|
||||
// .attr('y', verticalPos - breaklineVerticalOffset + counterBreaklines * breaklineOffset)
|
||||
.attr('y', verticalPos - 7 + counterBreaklines * breaklineOffset)
|
||||
.style('font-size', conf.messageFontSize)
|
||||
.style('font-family', conf.messageFontFamily)
|
||||
@@ -492,14 +438,11 @@ export const drawActors = function(diagram, actors, actorKeys, verticalPos) {
|
||||
// Add some rendering data to the object
|
||||
actor.width = actor.width || calculateActorWidth(actor);
|
||||
actor.height = conf.height;
|
||||
actor.margin = actor.margin || conf.actorMargin;
|
||||
actor.margin = conf.actorMargin;
|
||||
|
||||
actor.x = prevWidth + prevMargin;
|
||||
actor.y = verticalPos;
|
||||
|
||||
if (actor.wrap) {
|
||||
actor.description = wrapLabel(actor.description, actor.width);
|
||||
}
|
||||
// Draw the box with the attached line
|
||||
svgDraw.drawActor(diagram, actor, conf);
|
||||
bounds.insert(actor.x, verticalPos, actor.x + actor.width, actor.height);
|
||||
@@ -521,13 +464,18 @@ export const setConf = function(cnf) {
|
||||
|
||||
if (cnf.fontFamily) {
|
||||
conf.actorFontFamily = conf.noteFontFamily = conf.messageFontFamily = cnf.fontFamily;
|
||||
} else {
|
||||
conf.fontFamily = conf.messageFontFamily;
|
||||
}
|
||||
if (cnf.fontSize) {
|
||||
conf.actorFontSize = conf.noteFontSize = conf.messageFontSize = cnf.fontSize;
|
||||
// conf.height = cnf.fontSize * (65 / 14);
|
||||
} else {
|
||||
conf.fontSize = conf.messageFontSize;
|
||||
}
|
||||
if (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
|
||||
: Math.max(
|
||||
conf.width,
|
||||
calculateTextWidth(
|
||||
actor.description,
|
||||
conf.actorFontSize,
|
||||
conf.actorFontFamily,
|
||||
conf.actorFontWeight
|
||||
)
|
||||
utils.calculateTextWidth(actor.description, {
|
||||
fontSize: conf.actorFontSize,
|
||||
fontFamily: conf.actorFontFamily,
|
||||
fontWeight: 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) {
|
||||
let heightAdjust = 0;
|
||||
bounds.bumpVerticalPos(preMargin);
|
||||
@@ -676,19 +537,22 @@ function adjustLoopHeightForWrap(loopWidths, msg, preMargin, postMargin, addLoop
|
||||
? conf.fontSize
|
||||
: Math.round((3 * conf.fontSize) / 4);
|
||||
msg.message = msg.message
|
||||
? wrapLabel(`[ ${msg.message} ]`, loopWidth - 2 * conf.wrapPadding, '<br/>', {
|
||||
? utils.wrapLabel(`[${msg.message}]`, loopWidth, {
|
||||
fontSize: minSize,
|
||||
fontFamily: conf.fontFamily,
|
||||
fontWeight: conf.fontWeight
|
||||
fontWeight: conf.fontWeight,
|
||||
margin: conf.wrapPadding
|
||||
})
|
||||
: msg.message;
|
||||
heightAdjust = calculateTextHeight(
|
||||
msg.message,
|
||||
minSize,
|
||||
loopWidth,
|
||||
conf.wrapPadding,
|
||||
msg.wrap,
|
||||
minSize
|
||||
heightAdjust = Math.max(
|
||||
0,
|
||||
utils.calculateTextHeight(msg.message, {
|
||||
fontSize: minSize,
|
||||
fontFamily: conf.fontFamily,
|
||||
fontWeight: conf.fontWeight,
|
||||
margin: conf.wrapPadding
|
||||
}) -
|
||||
(preMargin + postMargin)
|
||||
);
|
||||
}
|
||||
addLoopFn(msg);
|
||||
@@ -760,17 +624,12 @@ export const draw = function(text, id) {
|
||||
|
||||
startx = actors[msg.from].x;
|
||||
stopx = actors[msg.to].x;
|
||||
textWidth = calculateTextWidth(
|
||||
msg.message,
|
||||
conf.noteFontSize,
|
||||
conf.noteFontFamily,
|
||||
conf.noteFontWeight
|
||||
);
|
||||
textWidth = utils.calculateTextWidth(msg.message, conf);
|
||||
noteWidth = shouldWrap ? conf.width : Math.max(conf.width, textWidth);
|
||||
|
||||
if (msg.placement === parser.yy.PLACEMENT.RIGHTOF) {
|
||||
if (shouldWrap) {
|
||||
msg.message = wrapLabel(msg.message, noteWidth);
|
||||
msg.message = utils.wrapLabel(msg.message, noteWidth, conf);
|
||||
}
|
||||
drawNote(
|
||||
diagram,
|
||||
@@ -781,7 +640,7 @@ export const draw = function(text, id) {
|
||||
);
|
||||
} else if (msg.placement === parser.yy.PLACEMENT.LEFTOF) {
|
||||
if (shouldWrap) {
|
||||
msg.message = wrapLabel(msg.message, noteWidth);
|
||||
msg.message = utils.wrapLabel(msg.message, noteWidth, conf);
|
||||
}
|
||||
drawNote(
|
||||
diagram,
|
||||
@@ -793,7 +652,7 @@ export const draw = function(text, id) {
|
||||
} else if (msg.to === msg.from) {
|
||||
// Single-actor over
|
||||
if (shouldWrap) {
|
||||
msg.message = wrapLabel(msg.message, noteWidth);
|
||||
msg.message = utils.wrapLabel(msg.message, noteWidth, conf);
|
||||
}
|
||||
drawNote(
|
||||
diagram,
|
||||
@@ -807,7 +666,7 @@ export const draw = function(text, id) {
|
||||
forceWidth = Math.abs(startx - stopx) + conf.actorMargin / 2;
|
||||
if (shouldWrap) {
|
||||
noteWidth = forceWidth;
|
||||
msg.message = wrapLabel(msg.message, noteWidth);
|
||||
msg.message = utils.wrapLabel(msg.message, noteWidth, conf);
|
||||
} else {
|
||||
noteWidth = Math.max(forceWidth, textWidth - 2 * conf.noteMargin);
|
||||
}
|
||||
@@ -911,12 +770,13 @@ export const draw = function(text, id) {
|
||||
startx = fromBounds[fromIdx];
|
||||
stopx = toBounds[toIdx];
|
||||
if (shouldWrap) {
|
||||
msg.message = wrapLabel(
|
||||
msg.message = utils.wrapLabel(
|
||||
msg.message,
|
||||
Math.max(
|
||||
Math.abs(stopx - startx) + 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 fontFamily = isNote ? conf.noteFontFamily : conf.messageFontFamily;
|
||||
const fontWeight = isNote ? conf.noteFontWeight : conf.messageFontWeight;
|
||||
const messageWidth = calculateTextWidth(
|
||||
msg.wrap ? wrapLabel(msg.message, conf.width - conf.noteMargin) : msg.message,
|
||||
const textConf = { fontFamily, fontSize, fontWeight, margin: conf.wrapPadding };
|
||||
let wrappedMessage = msg.wrap
|
||||
? utils.wrapLabel(msg.message, conf.width - conf.noteMargin, textConf)
|
||||
: msg.message;
|
||||
const messageDimensions = utils.calculateTextDimensions(wrappedMessage, {
|
||||
fontSize,
|
||||
fontFamily,
|
||||
fontWeight
|
||||
);
|
||||
fontWeight,
|
||||
margin: conf.wrapPadding
|
||||
});
|
||||
const messageWidth = messageDimensions.width;
|
||||
|
||||
/*
|
||||
* The following scenarios should be supported:
|
||||
@@ -1112,6 +977,13 @@ const calculateActorMargins = function(actors, actorToMessageWidth) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const textConf = {
|
||||
fontSize: conf.actorFontSize,
|
||||
fontFamily: conf.actorFontFamily,
|
||||
fontWeight: conf.actorFontWeight,
|
||||
margin: conf.wrapPadding
|
||||
};
|
||||
|
||||
const nextActor = actors[actor.nextActor];
|
||||
|
||||
// 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) {
|
||||
act.width = act.wrap
|
||||
? conf.width
|
||||
: Math.max(
|
||||
conf.width,
|
||||
calculateTextWidth(
|
||||
act.description,
|
||||
conf.actorFontSize,
|
||||
conf.actorFontFamily,
|
||||
conf.actorFontWeight
|
||||
)
|
||||
);
|
||||
if (act.wrap) {
|
||||
actor.description = utils.wrapLabel(actor.description, conf.width, textConf);
|
||||
}
|
||||
const actDims = utils.calculateTextDimensions(act.description, textConf);
|
||||
act.width = act.wrap ? conf.width : Math.max(conf.width, actDims.width);
|
||||
|
||||
act.height = act.wrap
|
||||
? Math.max(
|
||||
calculateTextHeight(
|
||||
act.description,
|
||||
conf.height,
|
||||
actor.width,
|
||||
conf.wrapPadding,
|
||||
act.wrap,
|
||||
conf.actorFontSize
|
||||
),
|
||||
conf.height
|
||||
)
|
||||
: conf.height;
|
||||
act.height = act.wrap ? Math.max(actDims.height, conf.height) : conf.height;
|
||||
logger.debug(`Actor h:${act.height} ${actDims.height} d:${act.description}`);
|
||||
maxHeight = Math.max(maxHeight, act.height);
|
||||
});
|
||||
|
||||
@@ -1192,17 +1047,22 @@ const calculateLoopMargins = function(messages, actors) {
|
||||
break;
|
||||
}
|
||||
if (msg.from && msg.to && stack.length > 0) {
|
||||
current = stack.pop();
|
||||
stack.forEach(stk => {
|
||||
current = stk;
|
||||
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);
|
||||
if (from.x < to.x) {
|
||||
current.from = Math.min(current.from, from.x);
|
||||
current.to = Math.max(current.to, to.x);
|
||||
} else {
|
||||
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;
|
||||
};
|
||||
|
||||
|
@@ -18,30 +18,6 @@ export const drawRect = function(elem, rectData) {
|
||||
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) {
|
||||
let prevTextHeight = 0,
|
||||
textHeight = 0;
|
||||
@@ -509,7 +485,6 @@ const _drawTextCandidateFunc = (function() {
|
||||
export default {
|
||||
drawRect,
|
||||
drawText,
|
||||
drawSimpleText,
|
||||
drawLabel,
|
||||
drawActor,
|
||||
anchorElement,
|
||||
|
@@ -15,7 +15,7 @@ import scope from 'scope-css';
|
||||
import pkg from '../package.json';
|
||||
import { setConfig, getConfig } from './config';
|
||||
import { logger, setLogLevel } from './logger';
|
||||
import utils from './utils';
|
||||
import utils, { assignWithDepth } from './utils';
|
||||
import flowRenderer from './diagrams/flowchart/flowRenderer';
|
||||
import flowRendererV2 from './diagrams/flowchart/flowRenderer-v2';
|
||||
import flowParser from './diagrams/flowchart/parser/flow';
|
||||
@@ -565,7 +565,7 @@ setConfig(config);
|
||||
function parse(text) {
|
||||
const graphInit = utils.detectInit(text);
|
||||
if (graphInit) {
|
||||
reinitialize(graphInit);
|
||||
initialize(graphInit);
|
||||
logger.debug('Init ', graphInit);
|
||||
}
|
||||
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 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
|
||||
@@ -708,6 +708,11 @@ const render = function(id, _txt, cb, container) {
|
||||
if (_txt.length > cnf.maxTextSize) {
|
||||
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') {
|
||||
container.innerHTML = '';
|
||||
@@ -745,10 +750,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
|
||||
@@ -929,17 +930,6 @@ const render = function(id, _txt, cb, container) {
|
||||
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;
|
||||
function initialize(options) {
|
||||
console.log('mermaidAPI.initialize');
|
||||
@@ -951,12 +941,8 @@ function initialize(options) {
|
||||
}
|
||||
setConfig(options);
|
||||
}
|
||||
console.warn('Initializing mermaidAPI theme', {
|
||||
version: pkg.version,
|
||||
options,
|
||||
config,
|
||||
current: getConfig().theme
|
||||
});
|
||||
assignWithDepth(config, getConfig());
|
||||
console.warn(`Initializing mermaidAPI: v${pkg.version} theme: ${config.theme}`);
|
||||
|
||||
setLogLevel(getConfig().logLevel);
|
||||
}
|
||||
@@ -970,7 +956,6 @@ const mermaidAPI = {
|
||||
render,
|
||||
parse,
|
||||
initialize,
|
||||
reinitialize,
|
||||
getConfig,
|
||||
setConfig,
|
||||
reset: () => {
|
||||
|
261
src/utils.js
261
src/utils.js
@@ -9,11 +9,12 @@ import {
|
||||
curveNatural,
|
||||
curveStep,
|
||||
curveStepAfter,
|
||||
curveStepBefore
|
||||
curveStepBefore,
|
||||
select
|
||||
} from 'd3';
|
||||
import { logger } from './logger';
|
||||
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
|
||||
const d3CurveTypes = {
|
||||
@@ -68,12 +69,21 @@ export const detectInit = function(text) {
|
||||
let results = {};
|
||||
if (Array.isArray(inits)) {
|
||||
let args = inits.map(init => init.args);
|
||||
results = assignWithDepth(results, ...args);
|
||||
results = assignWithDepth(results, [...args]);
|
||||
} else {
|
||||
results = inits.args;
|
||||
}
|
||||
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;
|
||||
};
|
||||
@@ -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') {
|
||||
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 !== 'undefined' && typeof dst === 'object' && typeof src === 'object') {
|
||||
Object.keys(src).forEach(key => {
|
||||
if (
|
||||
typeof src[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) {
|
||||
dst[key] = {};
|
||||
}
|
||||
dst[key] = assignWithDepth(dst[key], src[key], depth - 1);
|
||||
} else {
|
||||
dst[key] = assignWithDepth(dst[key], src[key], { depth: depth - 1, clobber });
|
||||
} else if (clobber || (typeof dst[key] !== 'object' && typeof src[key] !== 'object')) {
|
||||
dst[key] = src[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
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 {
|
||||
assignWithDepth,
|
||||
wrapLabel,
|
||||
calculateTextHeight,
|
||||
calculateTextWidth,
|
||||
calculateTextDimensions,
|
||||
detectInit,
|
||||
detectDirective,
|
||||
detectType,
|
||||
@@ -425,6 +657,5 @@ export default {
|
||||
formatUrl,
|
||||
getStylesFromArray,
|
||||
generateId,
|
||||
runFunc,
|
||||
assignWithDepth
|
||||
runFunc
|
||||
};
|
||||
|
@@ -1,6 +1,74 @@
|
||||
/* eslint-env jasmine */
|
||||
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() {
|
||||
it('should handle a graph definition', function() {
|
||||
const str = 'graph TB\nbfs1:queue';
|
||||
@@ -27,6 +95,16 @@ Alice->Bob: hi`;
|
||||
expect(type).toBe('sequence');
|
||||
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() {
|
||||
const str = `
|
||||
%%{
|
||||
|
Reference in New Issue
Block a user