• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mermaid-js / mermaid / 3702695729

pending completion
3702695729

push

github

GitHub
Merge pull request #3911 from mermaid-js/release/9.3.0

1462 of 1820 branches covered (80.33%)

Branch coverage included in aggregate %.

3306 of 3306 new or added lines in 85 files covered. (100.0%)

14340 of 29071 relevant lines covered (49.33%)

413.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.8
/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts
1
// @ts-nocheck TODO: fix file
1✔
2
import { select, selectAll } from 'd3';
1✔
3
import svgDraw, { drawText, fixLifeLineHeights } from './svgDraw';
1✔
4
import { log } from '../../logger';
1✔
5
// import { parser } from './parser/sequenceDiagram';
1✔
6
import common from '../common/common';
1✔
7
// import sequenceDb from './sequenceDb';
1✔
8
import * as configApi from '../../config';
1✔
9
import assignWithDepth from '../../assignWithDepth';
1✔
10
import utils from '../../utils';
1✔
11
import { configureSvgSize } from '../../setupGraphViewbox';
1✔
12
import Diagram from '../../Diagram';
1✔
13

1✔
14
let conf = {};
1✔
15

1✔
16
export const bounds = {
1✔
17
  data: {
1✔
18
    startx: undefined,
1✔
19
    stopx: undefined,
1✔
20
    starty: undefined,
1✔
21
    stopy: undefined,
1✔
22
  },
1✔
23
  verticalPos: 0,
1✔
24
  sequenceItems: [],
1✔
25
  activations: [],
1✔
26
  models: {
1✔
27
    getHeight: function () {
1✔
28
      return (
×
29
        Math.max.apply(
×
30
          null,
×
31
          this.actors.length === 0 ? [0] : this.actors.map((actor) => actor.height || 0)
×
32
        ) +
×
33
        (this.loops.length === 0
×
34
          ? 0
×
35
          : this.loops.map((it) => it.height || 0).reduce((acc, h) => acc + h)) +
×
36
        (this.messages.length === 0
×
37
          ? 0
×
38
          : this.messages.map((it) => it.height || 0).reduce((acc, h) => acc + h)) +
×
39
        (this.notes.length === 0
×
40
          ? 0
×
41
          : this.notes.map((it) => it.height || 0).reduce((acc, h) => acc + h))
×
42
      );
×
43
    },
×
44
    clear: function () {
1✔
45
      this.actors = [];
52✔
46
      this.loops = [];
52✔
47
      this.messages = [];
52✔
48
      this.notes = [];
52✔
49
    },
52✔
50
    addActor: function (actorModel) {
1✔
51
      this.actors.push(actorModel);
61✔
52
    },
61✔
53
    addLoop: function (loopModel) {
1✔
54
      this.loops.push(loopModel);
2✔
55
    },
2✔
56
    addMessage: function (msgModel) {
1✔
57
      this.messages.push(msgModel);
30✔
58
    },
30✔
59
    addNote: function (noteModel) {
1✔
60
      this.notes.push(noteModel);
15✔
61
    },
15✔
62
    lastActor: function () {
1✔
63
      return this.actors[this.actors.length - 1];
12✔
64
    },
12✔
65
    lastLoop: function () {
1✔
66
      return this.loops[this.loops.length - 1];
2✔
67
    },
2✔
68
    lastMessage: function () {
1✔
69
      return this.messages[this.messages.length - 1];
10✔
70
    },
10✔
71
    lastNote: function () {
1✔
72
      return this.notes[this.notes.length - 1];
4✔
73
    },
4✔
74
    actors: [],
1✔
75
    loops: [],
1✔
76
    messages: [],
1✔
77
    notes: [],
1✔
78
  },
1✔
79
  init: function () {
1✔
80
    this.sequenceItems = [];
52✔
81
    this.activations = [];
52✔
82
    this.models.clear();
52✔
83
    this.data = {
52✔
84
      startx: undefined,
52✔
85
      stopx: undefined,
52✔
86
      starty: undefined,
52✔
87
      stopy: undefined,
52✔
88
    };
52✔
89
    this.verticalPos = 0;
52✔
90
    setConf(configApi.getConfig());
52✔
91
  },
52✔
92
  updateVal: function (obj, key, val, fun) {
1✔
93
    if (obj[key] === undefined) {
656✔
94
      obj[key] = val;
162✔
95
    } else {
656✔
96
      obj[key] = fun(val, obj[key]);
494✔
97
    }
494✔
98
  },
656✔
99
  updateBounds: function (startx, starty, stopx, stopy) {
1✔
100
    // eslint-disable-next-line @typescript-eslint/no-this-alias
148✔
101
    const _self = this;
148✔
102
    let cnt = 0;
148✔
103
    /** @param type - Either `activation` or `undefined` */
148✔
104
    function updateFn(type?: 'activation') {
148✔
105
      return function updateItemBounds(item) {
296✔
106
        cnt++;
8✔
107
        // The loop sequenceItems is a stack so the biggest margins in the beginning of the sequenceItems
8✔
108
        const n = _self.sequenceItems.length - cnt + 1;
8✔
109

8✔
110
        _self.updateVal(item, 'starty', starty - n * conf.boxMargin, Math.min);
8✔
111
        _self.updateVal(item, 'stopy', stopy + n * conf.boxMargin, Math.max);
8✔
112

8✔
113
        _self.updateVal(bounds.data, 'startx', startx - n * conf.boxMargin, Math.min);
8✔
114
        _self.updateVal(bounds.data, 'stopx', stopx + n * conf.boxMargin, Math.max);
8✔
115

8✔
116
        if (!(type === 'activation')) {
8✔
117
          _self.updateVal(item, 'startx', startx - n * conf.boxMargin, Math.min);
8✔
118
          _self.updateVal(item, 'stopx', stopx + n * conf.boxMargin, Math.max);
8✔
119

8✔
120
          _self.updateVal(bounds.data, 'starty', starty - n * conf.boxMargin, Math.min);
8✔
121
          _self.updateVal(bounds.data, 'stopy', stopy + n * conf.boxMargin, Math.max);
8✔
122
        }
8✔
123
      };
8✔
124
    }
296✔
125

296✔
126
    this.sequenceItems.forEach(updateFn());
148✔
127
    this.activations.forEach(updateFn('activation'));
148✔
128
  },
148✔
129
  insert: function (startx, starty, stopx, stopy) {
1✔
130
    const _startx = Math.min(startx, stopx);
148✔
131
    const _stopx = Math.max(startx, stopx);
148✔
132
    const _starty = Math.min(starty, stopy);
148✔
133
    const _stopy = Math.max(starty, stopy);
148✔
134

148✔
135
    this.updateVal(bounds.data, 'startx', _startx, Math.min);
148✔
136
    this.updateVal(bounds.data, 'starty', _starty, Math.min);
148✔
137
    this.updateVal(bounds.data, 'stopx', _stopx, Math.max);
148✔
138
    this.updateVal(bounds.data, 'stopy', _stopy, Math.max);
148✔
139

148✔
140
    this.updateBounds(_startx, _starty, _stopx, _stopy);
148✔
141
  },
148✔
142
  newActivation: function (message, diagram, actors) {
1✔
143
    const actorRect = actors[message.from.actor];
×
144
    const stackedSize = actorActivations(message.from.actor).length || 0;
×
145
    const x = actorRect.x + actorRect.width / 2 + ((stackedSize - 1) * conf.activationWidth) / 2;
×
146
    this.activations.push({
×
147
      startx: x,
×
148
      starty: this.verticalPos + 2,
×
149
      stopx: x + conf.activationWidth,
×
150
      stopy: undefined,
×
151
      actor: message.from.actor,
×
152
      anchored: svgDraw.anchorElement(diagram),
×
153
    });
×
154
  },
×
155
  endActivation: function (message) {
1✔
156
    // find most recent activation for given actor
×
157
    const lastActorActivationIdx = this.activations
×
158
      .map(function (activation) {
×
159
        return activation.actor;
×
160
      })
×
161
      .lastIndexOf(message.from.actor);
×
162
    return this.activations.splice(lastActorActivationIdx, 1)[0];
×
163
  },
×
164
  createLoop: function (title = { message: undefined, wrap: false, width: undefined }, fill) {
1✔
165
    return {
6✔
166
      startx: undefined,
6✔
167
      starty: this.verticalPos,
6✔
168
      stopx: undefined,
6✔
169
      stopy: undefined,
6✔
170
      title: title.message,
6✔
171
      wrap: title.wrap,
6✔
172
      width: title.width,
6✔
173
      height: 0,
6✔
174
      fill: fill,
6✔
175
    };
6✔
176
  },
6✔
177
  newLoop: function (title = { message: undefined, wrap: false, width: undefined }, fill) {
1✔
178
    this.sequenceItems.push(this.createLoop(title, fill));
6✔
179
  },
6✔
180
  endLoop: function () {
1✔
181
    return this.sequenceItems.pop();
6✔
182
  },
6✔
183
  addSectionToLoop: function (message) {
1✔
184
    const loop = this.sequenceItems.pop();
×
185
    loop.sections = loop.sections || [];
×
186
    loop.sectionTitles = loop.sectionTitles || [];
×
187
    loop.sections.push({ y: bounds.getVerticalPos(), height: 0 });
×
188
    loop.sectionTitles.push(message);
×
189
    this.sequenceItems.push(loop);
×
190
  },
×
191
  bumpVerticalPos: function (bump) {
1✔
192
    this.verticalPos = this.verticalPos + bump;
186✔
193
    this.data.stopy = this.verticalPos;
186✔
194
  },
186✔
195
  getVerticalPos: function () {
1✔
196
    return this.verticalPos;
97✔
197
  },
97✔
198
  getBounds: function () {
1✔
199
    return { bounds: this.data, models: this.models };
62✔
200
  },
62✔
201
};
1✔
202

1✔
203
/** Options for drawing a note in {@link drawNote} */
1✔
204
interface NoteModel {
1✔
205
  /** x axis start position */
1✔
206
  startx: number;
1✔
207
  /** y axis position */
1✔
208
  starty: number;
1✔
209
  /** the message to be shown */
1✔
210
  message: string;
1✔
211
  /** Set this with a custom width to override the default configured width. */
1✔
212
  width: number;
1✔
213
}
1✔
214

1✔
215
/**
1✔
216
 * Draws an note in the diagram with the attached line
1✔
217
 *
1✔
218
 * @param elem - The diagram to draw to.
1✔
219
 * @param noteModel - Note model options.
1✔
220
 */
1✔
221
const drawNote = function (elem: any, noteModel: NoteModel) {
1✔
222
  bounds.bumpVerticalPos(conf.boxMargin);
15✔
223
  noteModel.height = conf.boxMargin;
15✔
224
  noteModel.starty = bounds.getVerticalPos();
15✔
225
  const rect = svgDraw.getNoteRect();
15✔
226
  rect.x = noteModel.startx;
15✔
227
  rect.y = noteModel.starty;
15✔
228
  rect.width = noteModel.width || conf.width;
15!
229
  rect.class = 'note';
15✔
230

15✔
231
  const g = elem.append('g');
15✔
232
  const rectElem = svgDraw.drawRect(g, rect);
15✔
233
  const textObj = svgDraw.getTextObj();
15✔
234
  textObj.x = noteModel.startx;
15✔
235
  textObj.y = noteModel.starty;
15✔
236
  textObj.width = rect.width;
15✔
237
  textObj.dy = '1em';
15✔
238
  textObj.text = noteModel.message;
15✔
239
  textObj.class = 'noteText';
15✔
240
  textObj.fontFamily = conf.noteFontFamily;
15✔
241
  textObj.fontSize = conf.noteFontSize;
15✔
242
  textObj.fontWeight = conf.noteFontWeight;
15✔
243
  textObj.anchor = conf.noteAlign;
15✔
244
  textObj.textMargin = conf.noteMargin;
15✔
245
  textObj.valign = 'center';
15✔
246

15✔
247
  const textElem = drawText(g, textObj);
15✔
248

15✔
249
  const textHeight = Math.round(
15✔
250
    textElem
15✔
251
      .map((te) => (te._groups || te)[0][0].getBBox().height)
15✔
252
      .reduce((acc, curr) => acc + curr)
15✔
253
  );
15✔
254

15✔
255
  rectElem.attr('height', textHeight + 2 * conf.noteMargin);
15✔
256
  noteModel.height += textHeight + 2 * conf.noteMargin;
15✔
257
  bounds.bumpVerticalPos(textHeight + 2 * conf.noteMargin);
15✔
258
  noteModel.stopy = noteModel.starty + textHeight + 2 * conf.noteMargin;
15✔
259
  noteModel.stopx = noteModel.startx + rect.width;
15✔
260
  bounds.insert(noteModel.startx, noteModel.starty, noteModel.stopx, noteModel.stopy);
15✔
261
  bounds.models.addNote(noteModel);
15✔
262
};
15✔
263

1✔
264
const messageFont = (cnf) => {
1✔
265
  return {
129✔
266
    fontFamily: cnf.messageFontFamily,
129✔
267
    fontSize: cnf.messageFontSize,
129✔
268
    fontWeight: cnf.messageFontWeight,
129✔
269
  };
129✔
270
};
129✔
271
const noteFont = (cnf) => {
1✔
272
  return {
32✔
273
    fontFamily: cnf.noteFontFamily,
32✔
274
    fontSize: cnf.noteFontSize,
32✔
275
    fontWeight: cnf.noteFontWeight,
32✔
276
  };
32✔
277
};
32✔
278
const actorFont = (cnf) => {
1✔
279
  return {
94✔
280
    fontFamily: cnf.actorFontFamily,
94✔
281
    fontSize: cnf.actorFontSize,
94✔
282
    fontWeight: cnf.actorFontWeight,
94✔
283
  };
94✔
284
};
94✔
285

1✔
286
/**
1✔
287
 * Process a message by adding its dimensions to the bound. It returns the Y coordinate of the
1✔
288
 * message so it can be drawn later. We do not draw the message at this point so the arrowhead can
1✔
289
 * be on top of the activation box.
1✔
290
 *
1✔
291
 * @param _diagram - The parent of the message element.
1✔
292
 * @param msgModel - The model containing fields describing a message
1✔
293
 * @returns `lineStartY` - The Y coordinate at which the message line starts
1✔
294
 */
1✔
295
function boundMessage(_diagram, msgModel): number {
30✔
296
  bounds.bumpVerticalPos(10);
30✔
297
  const { startx, stopx, message } = msgModel;
30✔
298
  const lines = common.splitBreaks(message).length;
30✔
299
  const textDims = utils.calculateTextDimensions(message, messageFont(conf));
30✔
300
  const lineHeight = textDims.height / lines;
30✔
301
  msgModel.height += lineHeight;
30✔
302

30✔
303
  bounds.bumpVerticalPos(lineHeight);
30✔
304

30✔
305
  let lineStartY;
30✔
306
  let totalOffset = textDims.height - 10;
30✔
307
  const textWidth = textDims.width;
30✔
308

30✔
309
  if (startx === stopx) {
30!
310
    lineStartY = bounds.getVerticalPos() + totalOffset;
×
311
    if (!conf.rightAngles) {
×
312
      totalOffset += conf.boxMargin;
×
313
      lineStartY = bounds.getVerticalPos() + totalOffset;
×
314
    }
×
315
    totalOffset += 30;
×
316
    const dx = Math.max(textWidth / 2, conf.width / 2);
×
317
    bounds.insert(
×
318
      startx - dx,
×
319
      bounds.getVerticalPos() - 10 + totalOffset,
×
320
      stopx + dx,
×
321
      bounds.getVerticalPos() + 30 + totalOffset
×
322
    );
×
323
  } else {
30✔
324
    totalOffset += conf.boxMargin;
30✔
325
    lineStartY = bounds.getVerticalPos() + totalOffset;
30✔
326
    bounds.insert(startx, lineStartY - 10, stopx, lineStartY);
30✔
327
  }
30✔
328
  bounds.bumpVerticalPos(totalOffset);
30✔
329
  msgModel.height += totalOffset;
30✔
330
  msgModel.stopy = msgModel.starty + msgModel.height;
30✔
331
  bounds.insert(msgModel.fromBounds, msgModel.starty, msgModel.toBounds, msgModel.stopy);
30✔
332

30✔
333
  return lineStartY;
30✔
334
}
30✔
335

30✔
336
/**
30✔
337
 * Draws a message. Note that the bounds have previously been updated by boundMessage.
30✔
338
 *
30✔
339
 * @param diagram - The parent of the message element
30✔
340
 * @param msgModel - The model containing fields describing a message
30✔
341
 * @param lineStartY - The Y coordinate at which the message line starts
30✔
342
 * @param diagObj - The diagram object.
30✔
343
 */
30✔
344
const drawMessage = function (diagram, msgModel, lineStartY: number, diagObj: Diagram) {
1✔
345
  const { startx, stopx, starty, message, type, sequenceIndex, sequenceVisible } = msgModel;
30✔
346
  const textDims = utils.calculateTextDimensions(message, messageFont(conf));
30✔
347
  const textObj = svgDraw.getTextObj();
30✔
348
  textObj.x = startx;
30✔
349
  textObj.y = starty + 10;
30✔
350
  textObj.width = stopx - startx;
30✔
351
  textObj.class = 'messageText';
30✔
352
  textObj.dy = '1em';
30✔
353
  textObj.text = message;
30✔
354
  textObj.fontFamily = conf.messageFontFamily;
30✔
355
  textObj.fontSize = conf.messageFontSize;
30✔
356
  textObj.fontWeight = conf.messageFontWeight;
30✔
357
  textObj.anchor = conf.messageAlign;
30✔
358
  textObj.valign = 'center';
30✔
359
  textObj.textMargin = conf.wrapPadding;
30✔
360
  textObj.tspan = false;
30✔
361

30✔
362
  drawText(diagram, textObj);
30✔
363

30✔
364
  const textWidth = textDims.width;
30✔
365

30✔
366
  let line;
30✔
367
  if (startx === stopx) {
30!
368
    if (conf.rightAngles) {
×
369
      line = diagram
×
370
        .append('path')
×
371
        .attr(
×
372
          'd',
×
373
          `M  ${startx},${lineStartY} H ${startx + Math.max(conf.width / 2, textWidth / 2)} V ${
×
374
            lineStartY + 25
×
375
          } H ${startx}`
×
376
        );
×
377
    } else {
×
378
      line = diagram
×
379
        .append('path')
×
380
        .attr(
×
381
          'd',
×
382
          'M ' +
×
383
            startx +
×
384
            ',' +
×
385
            lineStartY +
×
386
            ' C ' +
×
387
            (startx + 60) +
×
388
            ',' +
×
389
            (lineStartY - 10) +
×
390
            ' ' +
×
391
            (startx + 60) +
×
392
            ',' +
×
393
            (lineStartY + 30) +
×
394
            ' ' +
×
395
            startx +
×
396
            ',' +
×
397
            (lineStartY + 20)
×
398
        );
×
399
    }
×
400
  } else {
30✔
401
    line = diagram.append('line');
30✔
402
    line.attr('x1', startx);
30✔
403
    line.attr('y1', lineStartY);
30✔
404
    line.attr('x2', stopx);
30✔
405
    line.attr('y2', lineStartY);
30✔
406
  }
30✔
407
  // Make an SVG Container
30✔
408
  // Draw the line
30✔
409
  if (
30✔
410
    type === diagObj.db.LINETYPE.DOTTED ||
30✔
411
    type === diagObj.db.LINETYPE.DOTTED_CROSS ||
30✔
412
    type === diagObj.db.LINETYPE.DOTTED_POINT ||
30✔
413
    type === diagObj.db.LINETYPE.DOTTED_OPEN
30✔
414
  ) {
30✔
415
    line.style('stroke-dasharray', '3, 3');
4✔
416
    line.attr('class', 'messageLine1');
4✔
417
  } else {
30✔
418
    line.attr('class', 'messageLine0');
26✔
419
  }
26✔
420

26✔
421
  let url = '';
30✔
422
  if (conf.arrowMarkerAbsolute) {
30!
423
    url =
×
424
      window.location.protocol +
×
425
      '//' +
×
426
      window.location.host +
×
427
      window.location.pathname +
×
428
      window.location.search;
×
429
    url = url.replace(/\(/g, '\\(');
×
430
    url = url.replace(/\)/g, '\\)');
×
431
  }
×
432

×
433
  line.attr('stroke-width', 2);
30✔
434
  line.attr('stroke', 'none'); // handled by theme/css anyway
30✔
435
  line.style('fill', 'none'); // remove any fill colour
30✔
436
  if (type === diagObj.db.LINETYPE.SOLID || type === diagObj.db.LINETYPE.DOTTED) {
30✔
437
    line.attr('marker-end', 'url(' + url + '#arrowhead)');
8✔
438
  }
8✔
439
  if (type === diagObj.db.LINETYPE.SOLID_POINT || type === diagObj.db.LINETYPE.DOTTED_POINT) {
30!
440
    line.attr('marker-end', 'url(' + url + '#filled-head)');
×
441
  }
×
442

×
443
  if (type === diagObj.db.LINETYPE.SOLID_CROSS || type === diagObj.db.LINETYPE.DOTTED_CROSS) {
30!
444
    line.attr('marker-end', 'url(' + url + '#crosshead)');
×
445
  }
×
446

×
447
  // add node number
×
448
  if (sequenceVisible || conf.showSequenceNumbers) {
30✔
449
    line.attr('marker-start', 'url(' + url + '#sequencenumber)');
4✔
450
    diagram
4✔
451
      .append('text')
4✔
452
      .attr('x', startx)
4✔
453
      .attr('y', lineStartY + 4)
4✔
454
      .attr('font-family', 'sans-serif')
4✔
455
      .attr('font-size', '12px')
4✔
456
      .attr('text-anchor', 'middle')
4✔
457
      .attr('class', 'sequenceNumber')
4✔
458
      .text(sequenceIndex);
4✔
459
  }
4✔
460
};
4✔
461

1✔
462
export const drawActors = function (
1✔
463
  diagram,
40✔
464
  actors,
40✔
465
  actorKeys,
40✔
466
  verticalPos,
40✔
467
  configuration,
40✔
468
  messages
40✔
469
) {
40✔
470
  if (configuration.hideUnusedParticipants === true) {
40!
471
    const newActors = new Set();
×
472
    messages.forEach((message) => {
×
473
      newActors.add(message.from);
×
474
      newActors.add(message.to);
×
475
    });
×
476
    actorKeys = actorKeys.filter((actorKey) => newActors.has(actorKey));
×
477
  }
×
478

×
479
  // Draw the actors
×
480
  let prevWidth = 0;
40✔
481
  let prevMargin = 0;
40✔
482
  let maxHeight = 0;
40✔
483
  for (const actorKey of actorKeys) {
40✔
484
    const actor = actors[actorKey];
61✔
485

61✔
486
    // Add some rendering data to the object
61✔
487
    actor.width = actor.width || conf.width;
61!
488
    actor.height = Math.max(actor.height || conf.height, conf.height);
61!
489
    actor.margin = actor.margin || conf.actorMargin;
61✔
490

61✔
491
    actor.x = prevWidth + prevMargin;
61✔
492
    actor.y = verticalPos;
61✔
493

61✔
494
    // Draw the box with the attached line
61✔
495
    const height = svgDraw.drawActor(diagram, actor, conf);
61✔
496
    maxHeight = Math.max(maxHeight, height);
61✔
497
    bounds.insert(actor.x, verticalPos, actor.x + actor.width, actor.height);
61✔
498

61✔
499
    prevWidth += actor.width;
61✔
500
    prevMargin += actor.margin;
61✔
501
    bounds.models.addActor(actor);
61✔
502
  }
61✔
503

61✔
504
  // Add a margin between the actor boxes and the first arrow
61✔
505
  bounds.bumpVerticalPos(maxHeight);
40✔
506
};
1✔
507

1✔
508
export const drawActorsPopup = function (diagram, actors, actorKeys, doc) {
1✔
509
  let maxHeight = 0;
30✔
510
  let maxWidth = 0;
30✔
511
  for (const actorKey of actorKeys) {
30✔
512
    const actor = actors[actorKey];
47✔
513
    const minMenuWidth = getRequiredPopupWidth(actor);
47✔
514
    const menuDimensions = svgDraw.drawPopup(
47✔
515
      diagram,
47✔
516
      actor,
47✔
517
      minMenuWidth,
47✔
518
      conf,
47✔
519
      conf.forceMenus,
47✔
520
      doc
47✔
521
    );
47✔
522
    if (menuDimensions.height > maxHeight) {
47!
523
      maxHeight = menuDimensions.height;
×
524
    }
×
525
    if (menuDimensions.width + actor.x > maxWidth) {
47✔
526
      maxWidth = menuDimensions.width + actor.x;
17✔
527
    }
17✔
528
  }
47✔
529

47✔
530
  return { maxHeight: maxHeight, maxWidth: maxWidth };
30✔
531
};
1✔
532

1✔
533
export const setConf = function (cnf) {
1✔
534
  assignWithDepth(conf, cnf);
223✔
535

223✔
536
  if (cnf.fontFamily) {
223✔
537
    conf.actorFontFamily = conf.noteFontFamily = conf.messageFontFamily = cnf.fontFamily;
52✔
538
  }
52✔
539
  if (cnf.fontSize) {
223✔
540
    conf.actorFontSize = conf.noteFontSize = conf.messageFontSize = cnf.fontSize;
52✔
541
  }
52✔
542
  if (cnf.fontWeight) {
223!
543
    conf.actorFontWeight = conf.noteFontWeight = conf.messageFontWeight = cnf.fontWeight;
×
544
  }
×
545
};
1✔
546

1✔
547
const actorActivations = function (actor) {
1✔
548
  return bounds.activations.filter(function (activation) {
60✔
549
    return activation.actor === actor;
×
550
  });
×
551
};
60✔
552

1✔
553
const activationBounds = function (actor, actors) {
1✔
554
  // handle multiple stacked activations for same actor
60✔
555
  const actorObj = actors[actor];
60✔
556
  const activations = actorActivations(actor);
60✔
557

60✔
558
  const left = activations.reduce(function (acc, activation) {
60✔
559
    return Math.min(acc, activation.startx);
×
560
  }, actorObj.x + actorObj.width / 2);
60✔
561
  const right = activations.reduce(function (acc, activation) {
60✔
562
    return Math.max(acc, activation.stopx);
×
563
  }, actorObj.x + actorObj.width / 2);
60✔
564
  return [left, right];
60✔
565
};
60✔
566

1✔
567
function adjustLoopHeightForWrap(loopWidths, msg, preMargin, postMargin, addLoopFn) {
2✔
568
  bounds.bumpVerticalPos(preMargin);
2✔
569
  let heightAdjust = postMargin;
2✔
570
  if (msg.id && msg.message && loopWidths[msg.id]) {
2✔
571
    const loopWidth = loopWidths[msg.id].width;
1✔
572
    const textConf = messageFont(conf);
1✔
573
    msg.message = utils.wrapLabel(`[${msg.message}]`, loopWidth - 2 * conf.wrapPadding, textConf);
1✔
574
    msg.width = loopWidth;
1✔
575
    msg.wrap = true;
1✔
576

1✔
577
    // const lines = common.splitBreaks(msg.message).length;
1✔
578
    const textDims = utils.calculateTextDimensions(msg.message, textConf);
1✔
579
    const totalOffset = Math.max(textDims.height, conf.labelBoxHeight);
1✔
580
    heightAdjust = postMargin + totalOffset;
1✔
581
    log.debug(`${totalOffset} - ${msg.message}`);
1✔
582
  }
1✔
583
  addLoopFn(msg);
2✔
584
  bounds.bumpVerticalPos(heightAdjust);
2✔
585
}
2✔
586

2✔
587
/**
2✔
588
 * Draws a sequenceDiagram in the tag with id: id based on the graph definition in text.
2✔
589
 *
2✔
590
 * @param _text - The text of the diagram
2✔
591
 * @param id - The id of the diagram which will be used as a DOM element id¨
2✔
592
 * @param _version - Mermaid version from package.json
2✔
593
 * @param diagObj - A standard diagram containing the db and the text and type etc of the diagram
2✔
594
 */
2✔
595
export const draw = function (_text: string, id: string, _version: string, diagObj: Diagram) {
1✔
596
  const { securityLevel, sequence } = configApi.getConfig();
30✔
597
  conf = sequence;
30✔
598
  // Handle root and Document for when rendering in sandbox mode
30✔
599
  let sandboxElement;
30✔
600
  if (securityLevel === 'sandbox') {
30!
601
    sandboxElement = select('#i' + id);
×
602
  }
×
603

×
604
  const root =
30✔
605
    securityLevel === 'sandbox'
30!
606
      ? select(sandboxElement.nodes()[0].contentDocument.body)
×
607
      : select('body');
30✔
608
  const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document;
30!
609
  bounds.init();
30✔
610
  log.debug(diagObj.db);
30✔
611

30✔
612
  const diagram =
30✔
613
    securityLevel === 'sandbox' ? root.select(`[id="${id}"]`) : select(`[id="${id}"]`);
30!
614

30✔
615
  // Fetch data from the parsing
30✔
616
  const actors = diagObj.db.getActors();
30✔
617
  const actorKeys = diagObj.db.getActorKeys();
30✔
618
  const messages = diagObj.db.getMessages();
30✔
619
  const title = diagObj.db.getDiagramTitle();
30✔
620

30✔
621
  const maxMessageWidthPerActor = getMaxMessageWidthPerActor(actors, messages, diagObj);
30✔
622
  conf.height = calculateActorMargins(actors, maxMessageWidthPerActor);
30✔
623

30✔
624
  svgDraw.insertComputerIcon(diagram);
30✔
625
  svgDraw.insertDatabaseIcon(diagram);
30✔
626
  svgDraw.insertClockIcon(diagram);
30✔
627

30✔
628
  drawActors(diagram, actors, actorKeys, 0, conf, messages);
30✔
629
  const loopWidths = calculateLoopBounds(messages, actors, maxMessageWidthPerActor, diagObj);
30✔
630

30✔
631
  // The arrow head definition is attached to the svg once
30✔
632
  svgDraw.insertArrowHead(diagram);
30✔
633
  svgDraw.insertArrowCrossHead(diagram);
30✔
634
  svgDraw.insertArrowFilledHead(diagram);
30✔
635
  svgDraw.insertSequenceNumber(diagram);
30✔
636

30✔
637
  /**
30✔
638
   * @param msg - The message to draw.
30✔
639
   * @param verticalPos - The vertical position of the message.
30✔
640
   */
30✔
641
  function activeEnd(msg: any, verticalPos: number) {
30✔
642
    const activationData = bounds.endActivation(msg);
×
643
    if (activationData.starty + 18 > verticalPos) {
×
644
      activationData.starty = verticalPos - 6;
×
645
      verticalPos += 12;
×
646
    }
×
647
    svgDraw.drawActivation(
×
648
      diagram,
×
649
      activationData,
×
650
      verticalPos,
×
651
      conf,
×
652
      actorActivations(msg.from.actor).length
×
653
    );
×
654

×
655
    bounds.insert(activationData.startx, verticalPos - 10, activationData.stopx, verticalPos);
×
656
  }
×
657

×
658
  // Draw the messages/signals
×
659
  let sequenceIndex = 1;
30✔
660
  let sequenceIndexStep = 1;
30✔
661
  const messagesToDraw = [];
30✔
662
  messages.forEach(function (msg) {
30✔
663
    let loopModel, noteModel, msgModel;
51✔
664

51✔
665
    switch (msg.type) {
51✔
666
      case diagObj.db.LINETYPE.NOTE:
51✔
667
        noteModel = msg.noteModel;
15✔
668
        drawNote(diagram, noteModel);
15✔
669
        break;
15✔
670
      case diagObj.db.LINETYPE.ACTIVE_START:
51!
671
        bounds.newActivation(msg, diagram, actors);
×
672
        break;
×
673
      case diagObj.db.LINETYPE.ACTIVE_END:
51!
674
        activeEnd(msg, bounds.getVerticalPos());
×
675
        break;
×
676
      case diagObj.db.LINETYPE.LOOP_START:
51✔
677
        adjustLoopHeightForWrap(
1✔
678
          loopWidths,
1✔
679
          msg,
1✔
680
          conf.boxMargin,
1✔
681
          conf.boxMargin + conf.boxTextMargin,
1✔
682
          (message) => bounds.newLoop(message)
1✔
683
        );
1✔
684
        break;
1✔
685
      case diagObj.db.LINETYPE.LOOP_END:
51✔
686
        loopModel = bounds.endLoop();
1✔
687
        svgDraw.drawLoop(diagram, loopModel, 'loop', conf);
1✔
688
        bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
1✔
689
        bounds.models.addLoop(loopModel);
1✔
690
        break;
1✔
691
      case diagObj.db.LINETYPE.RECT_START:
51✔
692
        adjustLoopHeightForWrap(loopWidths, msg, conf.boxMargin, conf.boxMargin, (message) =>
1✔
693
          bounds.newLoop(undefined, message.message)
1✔
694
        );
1✔
695
        break;
1✔
696
      case diagObj.db.LINETYPE.RECT_END:
51✔
697
        loopModel = bounds.endLoop();
1✔
698
        svgDraw.drawBackgroundRect(diagram, loopModel);
1✔
699
        bounds.models.addLoop(loopModel);
1✔
700
        bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
1✔
701
        break;
1✔
702
      case diagObj.db.LINETYPE.OPT_START:
51!
703
        adjustLoopHeightForWrap(
×
704
          loopWidths,
×
705
          msg,
×
706
          conf.boxMargin,
×
707
          conf.boxMargin + conf.boxTextMargin,
×
708
          (message) => bounds.newLoop(message)
×
709
        );
×
710
        break;
×
711
      case diagObj.db.LINETYPE.OPT_END:
51!
712
        loopModel = bounds.endLoop();
×
713
        svgDraw.drawLoop(diagram, loopModel, 'opt', conf);
×
714
        bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
×
715
        bounds.models.addLoop(loopModel);
×
716
        break;
×
717
      case diagObj.db.LINETYPE.ALT_START:
51!
718
        adjustLoopHeightForWrap(
×
719
          loopWidths,
×
720
          msg,
×
721
          conf.boxMargin,
×
722
          conf.boxMargin + conf.boxTextMargin,
×
723
          (message) => bounds.newLoop(message)
×
724
        );
×
725
        break;
×
726
      case diagObj.db.LINETYPE.ALT_ELSE:
51!
727
        adjustLoopHeightForWrap(
×
728
          loopWidths,
×
729
          msg,
×
730
          conf.boxMargin + conf.boxTextMargin,
×
731
          conf.boxMargin,
×
732
          (message) => bounds.addSectionToLoop(message)
×
733
        );
×
734
        break;
×
735
      case diagObj.db.LINETYPE.ALT_END:
51!
736
        loopModel = bounds.endLoop();
×
737
        svgDraw.drawLoop(diagram, loopModel, 'alt', conf);
×
738
        bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
×
739
        bounds.models.addLoop(loopModel);
×
740
        break;
×
741
      case diagObj.db.LINETYPE.PAR_START:
51!
742
        adjustLoopHeightForWrap(
×
743
          loopWidths,
×
744
          msg,
×
745
          conf.boxMargin,
×
746
          conf.boxMargin + conf.boxTextMargin,
×
747
          (message) => bounds.newLoop(message)
×
748
        );
×
749
        break;
×
750
      case diagObj.db.LINETYPE.PAR_AND:
51!
751
        adjustLoopHeightForWrap(
×
752
          loopWidths,
×
753
          msg,
×
754
          conf.boxMargin + conf.boxTextMargin,
×
755
          conf.boxMargin,
×
756
          (message) => bounds.addSectionToLoop(message)
×
757
        );
×
758
        break;
×
759
      case diagObj.db.LINETYPE.PAR_END:
51!
760
        loopModel = bounds.endLoop();
×
761
        svgDraw.drawLoop(diagram, loopModel, 'par', conf);
×
762
        bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
×
763
        bounds.models.addLoop(loopModel);
×
764
        break;
×
765
      case diagObj.db.LINETYPE.AUTONUMBER:
51✔
766
        sequenceIndex = msg.message.start || sequenceIndex;
2✔
767
        sequenceIndexStep = msg.message.step || sequenceIndexStep;
2✔
768
        if (msg.message.visible) {
2✔
769
          diagObj.db.enableSequenceNumbers();
2✔
770
        } else {
2!
771
          diagObj.db.disableSequenceNumbers();
×
772
        }
×
773
        break;
×
774
      case diagObj.db.LINETYPE.CRITICAL_START:
51!
775
        adjustLoopHeightForWrap(
×
776
          loopWidths,
×
777
          msg,
×
778
          conf.boxMargin,
×
779
          conf.boxMargin + conf.boxTextMargin,
×
780
          (message) => bounds.newLoop(message)
×
781
        );
×
782
        break;
×
783
      case diagObj.db.LINETYPE.CRITICAL_OPTION:
51!
784
        adjustLoopHeightForWrap(
×
785
          loopWidths,
×
786
          msg,
×
787
          conf.boxMargin + conf.boxTextMargin,
×
788
          conf.boxMargin,
×
789
          (message) => bounds.addSectionToLoop(message)
×
790
        );
×
791
        break;
×
792
      case diagObj.db.LINETYPE.CRITICAL_END:
51!
793
        loopModel = bounds.endLoop();
×
794
        svgDraw.drawLoop(diagram, loopModel, 'critical', conf);
×
795
        bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
×
796
        bounds.models.addLoop(loopModel);
×
797
        break;
×
798
      case diagObj.db.LINETYPE.BREAK_START:
51!
799
        adjustLoopHeightForWrap(
×
800
          loopWidths,
×
801
          msg,
×
802
          conf.boxMargin,
×
803
          conf.boxMargin + conf.boxTextMargin,
×
804
          (message) => bounds.newLoop(message)
×
805
        );
×
806
        break;
×
807
      case diagObj.db.LINETYPE.BREAK_END:
51!
808
        loopModel = bounds.endLoop();
×
809
        svgDraw.drawLoop(diagram, loopModel, 'break', conf);
×
810
        bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
×
811
        bounds.models.addLoop(loopModel);
×
812
        break;
×
813
      default:
51✔
814
        try {
30✔
815
          // lastMsg = msg
30✔
816
          msgModel = msg.msgModel;
30✔
817
          msgModel.starty = bounds.getVerticalPos();
30✔
818
          msgModel.sequenceIndex = sequenceIndex;
30✔
819
          msgModel.sequenceVisible = diagObj.db.showSequenceNumbers();
30✔
820
          const lineStartY = boundMessage(diagram, msgModel);
30✔
821
          messagesToDraw.push({ messageModel: msgModel, lineStartY: lineStartY });
30✔
822
          bounds.models.addMessage(msgModel);
30✔
823
        } catch (e) {
30!
824
          log.error('error while drawing message', e);
×
825
        }
×
826
    }
51✔
827

51✔
828
    // Increment sequence counter if msg.type is a line (and not another event like activation or note, etc)
51✔
829
    if (
51✔
830
      [
51✔
831
        diagObj.db.LINETYPE.SOLID_OPEN,
51✔
832
        diagObj.db.LINETYPE.DOTTED_OPEN,
51✔
833
        diagObj.db.LINETYPE.SOLID,
51✔
834
        diagObj.db.LINETYPE.DOTTED,
51✔
835
        diagObj.db.LINETYPE.SOLID_CROSS,
51✔
836
        diagObj.db.LINETYPE.DOTTED_CROSS,
51✔
837
        diagObj.db.LINETYPE.SOLID_POINT,
51✔
838
        diagObj.db.LINETYPE.DOTTED_POINT,
51✔
839
      ].includes(msg.type)
51✔
840
    ) {
51✔
841
      sequenceIndex = sequenceIndex + sequenceIndexStep;
30✔
842
    }
30✔
843
  });
51✔
844

30✔
845
  messagesToDraw.forEach((e) => drawMessage(diagram, e.messageModel, e.lineStartY, diagObj));
30✔
846

30✔
847
  if (conf.mirrorActors) {
30✔
848
    // Draw actors below diagram
10✔
849
    bounds.bumpVerticalPos(conf.boxMargin * 2);
10✔
850
    drawActors(diagram, actors, actorKeys, bounds.getVerticalPos(), conf, messages);
10✔
851
    bounds.bumpVerticalPos(conf.boxMargin);
10✔
852
    fixLifeLineHeights(diagram, bounds.getVerticalPos());
10✔
853
  }
10✔
854

10✔
855
  // only draw popups for the top row of actors.
10✔
856
  const requiredBoxSize = drawActorsPopup(diagram, actors, actorKeys, doc);
30✔
857

30✔
858
  const { bounds: box } = bounds.getBounds();
30✔
859

30✔
860
  // Adjust line height of actor lines now that the height of the diagram is known
30✔
861
  log.debug('For line height fix Querying: #' + id + ' .actor-line');
30✔
862
  const actorLines = selectAll('#' + id + ' .actor-line');
30✔
863
  actorLines.attr('y2', box.stopy);
30✔
864

30✔
865
  // Make sure the height of the diagram supports long menus.
30✔
866
  let boxHeight = box.stopy - box.starty;
30✔
867
  if (boxHeight < requiredBoxSize.maxHeight) {
30!
868
    boxHeight = requiredBoxSize.maxHeight;
×
869
  }
×
870

×
871
  let height = boxHeight + 2 * conf.diagramMarginY;
30✔
872
  if (conf.mirrorActors) {
30✔
873
    height = height - conf.boxMargin + conf.bottomMarginAdj;
10✔
874
  }
10✔
875

10✔
876
  // Make sure the width of the diagram supports wide menus.
10✔
877
  let boxWidth = box.stopx - box.startx;
30✔
878
  if (boxWidth < requiredBoxSize.maxWidth) {
30!
879
    boxWidth = requiredBoxSize.maxWidth;
×
880
  }
×
881
  const width = boxWidth + 2 * conf.diagramMarginX;
30✔
882

30✔
883
  if (title) {
30!
884
    diagram
×
885
      .append('text')
×
886
      .text(title)
×
887
      .attr('x', (box.stopx - box.startx) / 2 - 2 * conf.diagramMarginX)
×
888
      .attr('y', -25);
×
889
  }
×
890

×
891
  configureSvgSize(diagram, height, width, conf.useMaxWidth);
30✔
892

30✔
893
  const extraVertForTitle = title ? 40 : 0;
30!
894
  diagram.attr(
30✔
895
    'viewBox',
30✔
896
    box.startx -
30✔
897
      conf.diagramMarginX +
30✔
898
      ' -' +
30✔
899
      (conf.diagramMarginY + extraVertForTitle) +
30✔
900
      ' ' +
30✔
901
      width +
30✔
902
      ' ' +
30✔
903
      (height + extraVertForTitle)
30✔
904
  );
30✔
905

30✔
906
  log.debug(`models:`, bounds.models);
30✔
907
};
1✔
908

1✔
909
/**
1✔
910
 * Retrieves the max message width of each actor, supports signals (messages, loops) and notes.
1✔
911
 *
1✔
912
 * It will enumerate each given message, and will determine its text width, in relation to the actor
1✔
913
 * it originates from, and destined to.
1✔
914
 *
1✔
915
 * @param actors - The actors map
1✔
916
 * @param messages - A list of message objects to iterate
1✔
917
 * @param diagObj - The diagram object.
1✔
918
 * @returns The max message width of each actor.
1✔
919
 */
1✔
920
function getMaxMessageWidthPerActor(
30✔
921
  actors: { [id: string]: any },
30✔
922
  messages: any[],
30✔
923
  diagObj: Diagram
30✔
924
): { [id: string]: number } {
30✔
925
  const maxMessageWidthPerActor = {};
30✔
926

30✔
927
  messages.forEach(function (msg) {
30✔
928
    if (actors[msg.to] && actors[msg.from]) {
51✔
929
      const actor = actors[msg.to];
45✔
930

45✔
931
      // If this is the first actor, and the message is left of it, no need to calculate the margin
45✔
932
      if (msg.placement === diagObj.db.PLACEMENT.LEFTOF && !actor.prevActor) {
45!
933
        return;
×
934
      }
×
935

×
936
      // If this is the last actor, and the message is right of it, no need to calculate the margin
×
937
      if (msg.placement === diagObj.db.PLACEMENT.RIGHTOF && !actor.nextActor) {
45✔
938
        return;
5✔
939
      }
5✔
940

5✔
941
      const isNote = msg.placement !== undefined;
45✔
942
      const isMessage = !isNote;
40✔
943

40✔
944
      const textFont = isNote ? noteFont(conf) : messageFont(conf);
45✔
945
      const wrappedMessage = msg.wrap
45✔
946
        ? utils.wrapLabel(msg.message, conf.width - 2 * conf.wrapPadding, textFont)
11✔
947
        : msg.message;
29✔
948
      const messageDimensions = utils.calculateTextDimensions(wrappedMessage, textFont);
45✔
949
      const messageWidth = messageDimensions.width + 2 * conf.wrapPadding;
45✔
950

45✔
951
      /*
45✔
952
       * The following scenarios should be supported:
45✔
953
       *
45✔
954
       * - There's a message (non-note) between fromActor and toActor
45✔
955
       *   - If fromActor is on the right and toActor is on the left, we should
45✔
956
       *     define the toActor's margin
45✔
957
       *   - If fromActor is on the left and toActor is on the right, we should
45✔
958
       *     define the fromActor's margin
45✔
959
       * - There's a note, in which case fromActor == toActor
45✔
960
       *   - If the note is to the left of the actor, we should define the previous actor
45✔
961
       *     margin
45✔
962
       *   - If the note is on the actor, we should define both the previous and next actor
45✔
963
       *     margins, each being the half of the note size
45✔
964
       *   - If the note is on the right of the actor, we should define the current actor
45✔
965
       *     margin
45✔
966
       */
45✔
967
      if (isMessage && msg.from === actor.nextActor) {
45✔
968
        maxMessageWidthPerActor[msg.to] = Math.max(
13✔
969
          maxMessageWidthPerActor[msg.to] || 0,
13!
970
          messageWidth
13✔
971
        );
13✔
972
      } else if (isMessage && msg.from === actor.prevActor) {
45✔
973
        maxMessageWidthPerActor[msg.from] = Math.max(
17✔
974
          maxMessageWidthPerActor[msg.from] || 0,
17✔
975
          messageWidth
17✔
976
        );
17✔
977
      } else if (isMessage && msg.from === msg.to) {
27!
978
        maxMessageWidthPerActor[msg.from] = Math.max(
×
979
          maxMessageWidthPerActor[msg.from] || 0,
×
980
          messageWidth / 2
×
981
        );
×
982

×
983
        maxMessageWidthPerActor[msg.to] = Math.max(
×
984
          maxMessageWidthPerActor[msg.to] || 0,
×
985
          messageWidth / 2
×
986
        );
×
987
      } else if (msg.placement === diagObj.db.PLACEMENT.RIGHTOF) {
10✔
988
        maxMessageWidthPerActor[msg.from] = Math.max(
1✔
989
          maxMessageWidthPerActor[msg.from] || 0,
1✔
990
          messageWidth
1✔
991
        );
1✔
992
      } else if (msg.placement === diagObj.db.PLACEMENT.LEFTOF) {
10✔
993
        maxMessageWidthPerActor[actor.prevActor] = Math.max(
6✔
994
          maxMessageWidthPerActor[actor.prevActor] || 0,
6✔
995
          messageWidth
6✔
996
        );
6✔
997
      } else if (msg.placement === diagObj.db.PLACEMENT.OVER) {
9✔
998
        if (actor.prevActor) {
3✔
999
          maxMessageWidthPerActor[actor.prevActor] = Math.max(
3✔
1000
            maxMessageWidthPerActor[actor.prevActor] || 0,
3✔
1001
            messageWidth / 2
3✔
1002
          );
3✔
1003
        }
3✔
1004

3✔
1005
        if (actor.nextActor) {
3✔
1006
          maxMessageWidthPerActor[msg.from] = Math.max(
2✔
1007
            maxMessageWidthPerActor[msg.from] || 0,
2!
1008
            messageWidth / 2
2✔
1009
          );
2✔
1010
        }
2✔
1011
      }
3✔
1012
    }
45✔
1013
  });
51✔
1014

30✔
1015
  log.debug('maxMessageWidthPerActor:', maxMessageWidthPerActor);
30✔
1016
  return maxMessageWidthPerActor;
30✔
1017
}
30✔
1018

30✔
1019
const getRequiredPopupWidth = function (actor) {
1✔
1020
  let requiredPopupWidth = 0;
47✔
1021
  const textFont = actorFont(conf);
47✔
1022
  for (const key in actor.links) {
47!
1023
    const labelDimensions = utils.calculateTextDimensions(key, textFont);
×
1024
    const labelWidth = labelDimensions.width + 2 * conf.wrapPadding + 2 * conf.boxMargin;
×
1025
    if (requiredPopupWidth < labelWidth) {
×
1026
      requiredPopupWidth = labelWidth;
×
1027
    }
×
1028
  }
×
1029

×
1030
  return requiredPopupWidth;
47✔
1031
};
47✔
1032

1✔
1033
/**
1✔
1034
 * This will calculate the optimal margin for each given actor,
1✔
1035
 * for a given actor → messageWidth map.
1✔
1036
 *
1✔
1037
 * An actor's margin is determined by the width of the actor, the width of the largest message that
1✔
1038
 * originates from it, and the configured conf.actorMargin.
1✔
1039
 *
1✔
1040
 * @param actors - The actors map to calculate margins for
1✔
1041
 * @param actorToMessageWidth - A map of actor key → max message width it holds
1✔
1042
 */
1✔
1043
function calculateActorMargins(
30✔
1044
  actors: { [id: string]: any },
30✔
1045
  actorToMessageWidth: ReturnType<typeof getMaxMessageWidthPerActor>
30✔
1046
) {
30✔
1047
  let maxHeight = 0;
30✔
1048
  Object.keys(actors).forEach((prop) => {
30✔
1049
    const actor = actors[prop];
47✔
1050
    if (actor.wrap) {
47!
1051
      actor.description = utils.wrapLabel(
×
1052
        actor.description,
×
1053
        conf.width - 2 * conf.wrapPadding,
×
1054
        actorFont(conf)
×
1055
      );
×
1056
    }
×
1057
    const actDims = utils.calculateTextDimensions(actor.description, actorFont(conf));
47✔
1058
    actor.width = actor.wrap
47!
1059
      ? conf.width
×
1060
      : Math.max(conf.width, actDims.width + 2 * conf.wrapPadding);
47✔
1061

47✔
1062
    actor.height = actor.wrap ? Math.max(actDims.height, conf.height) : conf.height;
47!
1063
    maxHeight = Math.max(maxHeight, actor.height);
47✔
1064
  });
47✔
1065

30✔
1066
  for (const actorKey in actorToMessageWidth) {
30✔
1067
    const actor = actors[actorKey];
26✔
1068

26✔
1069
    if (!actor) {
26!
1070
      continue;
×
1071
    }
×
1072

×
1073
    const nextActor = actors[actor.nextActor];
26✔
1074

26✔
1075
    // No need to space out an actor that doesn't have a next link
26✔
1076
    if (!nextActor) {
26✔
1077
      continue;
6✔
1078
    }
6✔
1079

6✔
1080
    const messageWidth = actorToMessageWidth[actorKey];
26✔
1081
    const actorWidth = messageWidth + conf.actorMargin - actor.width / 2 - nextActor.width / 2;
20✔
1082

20✔
1083
    actor.margin = Math.max(actorWidth, conf.actorMargin);
20✔
1084
  }
20✔
1085

20✔
1086
  return Math.max(maxHeight, conf.height);
30✔
1087
}
30✔
1088

30✔
1089
const buildNoteModel = function (msg, actors, diagObj) {
1✔
1090
  const startx = actors[msg.from].x;
15✔
1091
  const stopx = actors[msg.to].x;
15✔
1092
  const shouldWrap = msg.wrap && msg.message;
15✔
1093

15✔
1094
  let textDimensions = utils.calculateTextDimensions(
15✔
1095
    shouldWrap ? utils.wrapLabel(msg.message, conf.width, noteFont(conf)) : msg.message,
15✔
1096
    noteFont(conf)
15✔
1097
  );
15✔
1098
  const noteModel = {
15✔
1099
    width: shouldWrap
15✔
1100
      ? conf.width
3✔
1101
      : Math.max(conf.width, textDimensions.width + 2 * conf.noteMargin),
12✔
1102
    height: 0,
15✔
1103
    startx: actors[msg.from].x,
15✔
1104
    stopx: 0,
15✔
1105
    starty: 0,
15✔
1106
    stopy: 0,
15✔
1107
    message: msg.message,
15✔
1108
  };
15✔
1109
  if (msg.placement === diagObj.db.PLACEMENT.RIGHTOF) {
15✔
1110
    noteModel.width = shouldWrap
6!
1111
      ? Math.max(conf.width, textDimensions.width)
×
1112
      : Math.max(
6✔
1113
          actors[msg.from].width / 2 + actors[msg.to].width / 2,
6✔
1114
          textDimensions.width + 2 * conf.noteMargin
6✔
1115
        );
6✔
1116
    noteModel.startx = startx + (actors[msg.from].width + conf.actorMargin) / 2;
6✔
1117
  } else if (msg.placement === diagObj.db.PLACEMENT.LEFTOF) {
15✔
1118
    noteModel.width = shouldWrap
6✔
1119
      ? Math.max(conf.width, textDimensions.width + 2 * conf.noteMargin)
3✔
1120
      : Math.max(
3✔
1121
          actors[msg.from].width / 2 + actors[msg.to].width / 2,
3✔
1122
          textDimensions.width + 2 * conf.noteMargin
3✔
1123
        );
3✔
1124
    noteModel.startx = startx - noteModel.width + (actors[msg.from].width - conf.actorMargin) / 2;
6✔
1125
  } else if (msg.to === msg.from) {
9✔
1126
    textDimensions = utils.calculateTextDimensions(
1✔
1127
      shouldWrap
1!
1128
        ? utils.wrapLabel(msg.message, Math.max(conf.width, actors[msg.from].width), noteFont(conf))
×
1129
        : msg.message,
1✔
1130
      noteFont(conf)
1✔
1131
    );
1✔
1132
    noteModel.width = shouldWrap
1!
1133
      ? Math.max(conf.width, actors[msg.from].width)
×
1134
      : Math.max(actors[msg.from].width, conf.width, textDimensions.width + 2 * conf.noteMargin);
1✔
1135
    noteModel.startx = startx + (actors[msg.from].width - noteModel.width) / 2;
1✔
1136
  } else {
3✔
1137
    noteModel.width =
2✔
1138
      Math.abs(startx + actors[msg.from].width / 2 - (stopx + actors[msg.to].width / 2)) +
2✔
1139
      conf.actorMargin;
2✔
1140
    noteModel.startx =
2✔
1141
      startx < stopx
2✔
1142
        ? startx + actors[msg.from].width / 2 - conf.actorMargin / 2
1✔
1143
        : stopx + actors[msg.to].width / 2 - conf.actorMargin / 2;
1✔
1144
  }
2✔
1145
  if (shouldWrap) {
15✔
1146
    noteModel.message = utils.wrapLabel(
3✔
1147
      msg.message,
3✔
1148
      noteModel.width - 2 * conf.wrapPadding,
3✔
1149
      noteFont(conf)
3✔
1150
    );
3✔
1151
  }
3✔
1152
  log.debug(
15✔
1153
    `NM:[${noteModel.startx},${noteModel.stopx},${noteModel.starty},${noteModel.stopy}:${noteModel.width},${noteModel.height}=${msg.message}]`
15✔
1154
  );
15✔
1155
  return noteModel;
15✔
1156
};
15✔
1157

1✔
1158
const buildMessageModel = function (msg, actors, diagObj) {
1✔
1159
  let process = false;
36✔
1160
  if (
36✔
1161
    [
36✔
1162
      diagObj.db.LINETYPE.SOLID_OPEN,
36✔
1163
      diagObj.db.LINETYPE.DOTTED_OPEN,
36✔
1164
      diagObj.db.LINETYPE.SOLID,
36✔
1165
      diagObj.db.LINETYPE.DOTTED,
36✔
1166
      diagObj.db.LINETYPE.SOLID_CROSS,
36✔
1167
      diagObj.db.LINETYPE.DOTTED_CROSS,
36✔
1168
      diagObj.db.LINETYPE.SOLID_POINT,
36✔
1169
      diagObj.db.LINETYPE.DOTTED_POINT,
36✔
1170
    ].includes(msg.type)
36✔
1171
  ) {
36✔
1172
    process = true;
30✔
1173
  }
30✔
1174
  if (!process) {
36✔
1175
    return {};
6✔
1176
  }
6✔
1177
  const fromBounds = activationBounds(msg.from, actors);
36✔
1178
  const toBounds = activationBounds(msg.to, actors);
30✔
1179
  const fromIdx = fromBounds[0] <= toBounds[0] ? 1 : 0;
36✔
1180
  const toIdx = fromBounds[0] < toBounds[0] ? 0 : 1;
36✔
1181
  const allBounds = [...fromBounds, ...toBounds];
36✔
1182
  const boundedWidth = Math.abs(toBounds[toIdx] - fromBounds[fromIdx]);
36✔
1183
  if (msg.wrap && msg.message) {
36✔
1184
    msg.message = utils.wrapLabel(
8✔
1185
      msg.message,
8✔
1186
      Math.max(boundedWidth + 2 * conf.wrapPadding, conf.width),
8✔
1187
      messageFont(conf)
8✔
1188
    );
8✔
1189
  }
8✔
1190
  const msgDims = utils.calculateTextDimensions(msg.message, messageFont(conf));
36✔
1191

30✔
1192
  return {
30✔
1193
    width: Math.max(
30✔
1194
      msg.wrap ? 0 : msgDims.width + 2 * conf.wrapPadding,
36✔
1195
      boundedWidth + 2 * conf.wrapPadding,
36✔
1196
      conf.width
36✔
1197
    ),
36✔
1198
    height: 0,
36✔
1199
    startx: fromBounds[fromIdx],
36✔
1200
    stopx: toBounds[toIdx],
36✔
1201
    starty: 0,
36✔
1202
    stopy: 0,
36✔
1203
    message: msg.message,
36✔
1204
    type: msg.type,
36✔
1205
    wrap: msg.wrap,
36✔
1206
    fromBounds: Math.min.apply(null, allBounds),
36✔
1207
    toBounds: Math.max.apply(null, allBounds),
36✔
1208
  };
36✔
1209
};
36✔
1210

1✔
1211
const calculateLoopBounds = function (messages, actors, _maxWidthPerActor, diagObj) {
1✔
1212
  const loops = {};
30✔
1213
  const stack = [];
30✔
1214
  let current, noteModel, msgModel;
30✔
1215

30✔
1216
  messages.forEach(function (msg) {
30✔
1217
    msg.id = utils.random({ length: 10 });
51✔
1218
    switch (msg.type) {
51✔
1219
      case diagObj.db.LINETYPE.LOOP_START:
51✔
1220
      case diagObj.db.LINETYPE.ALT_START:
51✔
1221
      case diagObj.db.LINETYPE.OPT_START:
51✔
1222
      case diagObj.db.LINETYPE.PAR_START:
51✔
1223
      case diagObj.db.LINETYPE.CRITICAL_START:
51✔
1224
      case diagObj.db.LINETYPE.BREAK_START:
51✔
1225
        stack.push({
1✔
1226
          id: msg.id,
1✔
1227
          msg: msg.message,
1✔
1228
          from: Number.MAX_SAFE_INTEGER,
1✔
1229
          to: Number.MIN_SAFE_INTEGER,
1✔
1230
          width: 0,
1✔
1231
        });
1✔
1232
        break;
1✔
1233
      case diagObj.db.LINETYPE.ALT_ELSE:
51!
1234
      case diagObj.db.LINETYPE.PAR_AND:
51!
1235
      case diagObj.db.LINETYPE.CRITICAL_OPTION:
51!
1236
        if (msg.message) {
×
1237
          current = stack.pop();
×
1238
          loops[current.id] = current;
×
1239
          loops[msg.id] = current;
×
1240
          stack.push(current);
×
1241
        }
×
1242
        break;
×
1243
      case diagObj.db.LINETYPE.LOOP_END:
51✔
1244
      case diagObj.db.LINETYPE.ALT_END:
51✔
1245
      case diagObj.db.LINETYPE.OPT_END:
51✔
1246
      case diagObj.db.LINETYPE.PAR_END:
51✔
1247
      case diagObj.db.LINETYPE.CRITICAL_END:
51✔
1248
      case diagObj.db.LINETYPE.BREAK_END:
51✔
1249
        current = stack.pop();
1✔
1250
        loops[current.id] = current;
1✔
1251
        break;
1✔
1252
      case diagObj.db.LINETYPE.ACTIVE_START:
51!
1253
        {
×
1254
          const actorRect = actors[msg.from ? msg.from.actor : msg.to.actor];
×
1255
          const stackedSize = actorActivations(msg.from ? msg.from.actor : msg.to.actor).length;
×
1256
          const x =
×
1257
            actorRect.x + actorRect.width / 2 + ((stackedSize - 1) * conf.activationWidth) / 2;
×
1258
          const toAdd = {
×
1259
            startx: x,
×
1260
            stopx: x + conf.activationWidth,
×
1261
            actor: msg.from.actor,
×
1262
            enabled: true,
×
1263
          };
×
1264
          bounds.activations.push(toAdd);
×
1265
        }
×
1266
        break;
×
1267
      case diagObj.db.LINETYPE.ACTIVE_END:
51!
1268
        {
×
1269
          const lastActorActivationIdx = bounds.activations
×
1270
            .map((a) => a.actor)
×
1271
            .lastIndexOf(msg.from.actor);
×
1272
          delete bounds.activations.splice(lastActorActivationIdx, 1)[0];
×
1273
        }
×
1274
        break;
×
1275
    }
51✔
1276
    const isNote = msg.placement !== undefined;
51✔
1277
    if (isNote) {
51✔
1278
      noteModel = buildNoteModel(msg, actors, diagObj);
15✔
1279
      msg.noteModel = noteModel;
15✔
1280
      stack.forEach((stk) => {
15✔
1281
        current = stk;
×
1282
        current.from = Math.min(current.from, noteModel.startx);
×
1283
        current.to = Math.max(current.to, noteModel.startx + noteModel.width);
×
1284
        current.width =
×
1285
          Math.max(current.width, Math.abs(current.from - current.to)) - conf.labelBoxWidth;
×
1286
      });
×
1287
    } else {
51✔
1288
      msgModel = buildMessageModel(msg, actors, diagObj);
36✔
1289
      msg.msgModel = msgModel;
36✔
1290
      if (msgModel.startx && msgModel.stopx && stack.length > 0) {
36✔
1291
        stack.forEach((stk) => {
1✔
1292
          current = stk;
1✔
1293
          if (msgModel.startx === msgModel.stopx) {
1!
1294
            const from = actors[msg.from];
×
1295
            const to = actors[msg.to];
×
1296
            current.from = Math.min(
×
1297
              from.x - msgModel.width / 2,
×
1298
              from.x - from.width / 2,
×
1299
              current.from
×
1300
            );
×
1301
            current.to = Math.max(to.x + msgModel.width / 2, to.x + from.width / 2, current.to);
×
1302
            current.width =
×
1303
              Math.max(current.width, Math.abs(current.to - current.from)) - conf.labelBoxWidth;
×
1304
          } else {
1✔
1305
            current.from = Math.min(msgModel.startx, current.from);
1✔
1306
            current.to = Math.max(msgModel.stopx, current.to);
1✔
1307
            current.width = Math.max(current.width, msgModel.width) - conf.labelBoxWidth;
1✔
1308
          }
1✔
1309
        });
1✔
1310
      }
1✔
1311
    }
36✔
1312
  });
51✔
1313
  bounds.activations = [];
30✔
1314
  log.debug('Loop type widths:', loops);
30✔
1315
  return loops;
30✔
1316
};
30✔
1317

1✔
1318
export default {
1✔
1319
  bounds,
1✔
1320
  drawActors,
1✔
1321
  drawActorsPopup,
1✔
1322
  setConf,
1✔
1323
  draw,
1✔
1324
};
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc