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

mermaid-js / mermaid / 5071601472

pending completion
5071601472

push

github

Knut Sveidqvist
Merge branch 'release/10.2.0'

1633 of 2064 branches covered (79.12%)

Branch coverage included in aggregate %.

2701 of 2701 new or added lines in 128 files covered. (100.0%)

19402 of 34929 relevant lines covered (55.55%)

418.23 hits per line

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

76.26
/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.js';
1✔
4
import { log } from '../../logger.js';
1✔
5
import common from '../common/common.js';
1✔
6
import * as svgDrawCommon from '../common/svgDrawCommon';
1✔
7
import * as configApi from '../../config.js';
1✔
8
import assignWithDepth from '../../assignWithDepth.js';
1✔
9
import utils from '../../utils.js';
1✔
10
import { configureSvgSize } from '../../setupGraphViewbox.js';
1✔
11
import { Diagram } from '../../Diagram.js';
1✔
12

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

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

8✔
114
        _self.updateVal(item, 'starty', starty - n * conf.boxMargin, Math.min);
8✔
115
        _self.updateVal(item, 'stopy', stopy + n * conf.boxMargin, Math.max);
8✔
116

8✔
117
        _self.updateVal(bounds.data, 'startx', startx - n * conf.boxMargin, Math.min);
8✔
118
        _self.updateVal(bounds.data, 'stopx', stopx + n * conf.boxMargin, Math.max);
8✔
119

8✔
120
        if (!(type === 'activation')) {
8✔
121
          _self.updateVal(item, 'startx', startx - n * conf.boxMargin, Math.min);
8✔
122
          _self.updateVal(item, 'stopx', stopx + n * conf.boxMargin, Math.max);
8✔
123

8✔
124
          _self.updateVal(bounds.data, 'starty', starty - n * conf.boxMargin, Math.min);
8✔
125
          _self.updateVal(bounds.data, 'stopy', stopy + n * conf.boxMargin, Math.max);
8✔
126
        }
8✔
127
      };
8✔
128
    }
306✔
129

153✔
130
    this.sequenceItems.forEach(updateFn());
153✔
131
    this.activations.forEach(updateFn('activation'));
153✔
132
  },
153✔
133
  insert: function (startx, starty, stopx, stopy) {
1✔
134
    const _startx = common.getMin(startx, stopx);
153✔
135
    const _stopx = common.getMax(startx, stopx);
153✔
136
    const _starty = common.getMin(starty, stopy);
153✔
137
    const _stopy = common.getMax(starty, stopy);
153✔
138

153✔
139
    this.updateVal(bounds.data, 'startx', _startx, Math.min);
153✔
140
    this.updateVal(bounds.data, 'starty', _starty, Math.min);
153✔
141
    this.updateVal(bounds.data, 'stopx', _stopx, Math.max);
153✔
142
    this.updateVal(bounds.data, 'stopy', _stopy, Math.max);
153✔
143

153✔
144
    this.updateBounds(_startx, _starty, _stopx, _stopy);
153✔
145
  },
153✔
146
  newActivation: function (message, diagram, actors) {
1✔
147
    const actorRect = actors[message.from.actor];
×
148
    const stackedSize = actorActivations(message.from.actor).length || 0;
×
149
    const x = actorRect.x + actorRect.width / 2 + ((stackedSize - 1) * conf.activationWidth) / 2;
×
150
    this.activations.push({
×
151
      startx: x,
×
152
      starty: this.verticalPos + 2,
×
153
      stopx: x + conf.activationWidth,
×
154
      stopy: undefined,
×
155
      actor: message.from.actor,
×
156
      anchored: svgDraw.anchorElement(diagram),
×
157
    });
×
158
  },
×
159
  endActivation: function (message) {
1✔
160
    // find most recent activation for given actor
×
161
    const lastActorActivationIdx = this.activations
×
162
      .map(function (activation) {
×
163
        return activation.actor;
×
164
      })
×
165
      .lastIndexOf(message.from.actor);
×
166
    return this.activations.splice(lastActorActivationIdx, 1)[0];
×
167
  },
×
168
  createLoop: function (title = { message: undefined, wrap: false, width: undefined }, fill) {
1✔
169
    return {
6✔
170
      startx: undefined,
6✔
171
      starty: this.verticalPos,
6✔
172
      stopx: undefined,
6✔
173
      stopy: undefined,
6✔
174
      title: title.message,
6✔
175
      wrap: title.wrap,
6✔
176
      width: title.width,
6✔
177
      height: 0,
6✔
178
      fill: fill,
6✔
179
    };
6✔
180
  },
6✔
181
  newLoop: function (title = { message: undefined, wrap: false, width: undefined }, fill) {
1✔
182
    this.sequenceItems.push(this.createLoop(title, fill));
6✔
183
  },
6✔
184
  endLoop: function () {
1✔
185
    return this.sequenceItems.pop();
6✔
186
  },
6✔
187
  isLoopOverlap: function () {
1✔
188
    return this.sequenceItems.length
46✔
189
      ? this.sequenceItems[this.sequenceItems.length - 1].overlap
2✔
190
      : false;
44✔
191
  },
46✔
192
  addSectionToLoop: function (message) {
1✔
193
    const loop = this.sequenceItems.pop();
×
194
    loop.sections = loop.sections || [];
×
195
    loop.sectionTitles = loop.sectionTitles || [];
×
196
    loop.sections.push({ y: bounds.getVerticalPos(), height: 0 });
×
197
    loop.sectionTitles.push(message);
×
198
    this.sequenceItems.push(loop);
×
199
  },
×
200
  saveVerticalPos: function () {
1✔
201
    if (this.isLoopOverlap()) {
×
202
      this.savedVerticalPos = this.verticalPos;
×
203
    }
×
204
  },
×
205
  resetVerticalPos: function () {
1✔
206
    if (this.isLoopOverlap()) {
46!
207
      this.verticalPos = this.savedVerticalPos;
×
208
    }
×
209
  },
46✔
210
  bumpVerticalPos: function (bump) {
1✔
211
    this.verticalPos = this.verticalPos + bump;
193✔
212
    this.data.stopy = common.getMax(this.data.stopy, this.verticalPos);
193✔
213
  },
193✔
214
  getVerticalPos: function () {
1✔
215
    return this.verticalPos;
163✔
216
  },
163✔
217
  getBounds: function () {
1✔
218
    return { bounds: this.data, models: this.models };
64✔
219
  },
64✔
220
};
1✔
221

1✔
222
/** Options for drawing a note in {@link drawNote} */
1✔
223
interface NoteModel {
1✔
224
  /** x axis start position */
1✔
225
  startx: number;
1✔
226
  /** y axis position */
1✔
227
  starty: number;
1✔
228
  /** the message to be shown */
1✔
229
  message: string;
1✔
230
  /** Set this with a custom width to override the default configured width. */
1✔
231
  width: number;
1✔
232
}
1✔
233

1✔
234
/**
1✔
235
 * Draws an note in the diagram with the attached line
1✔
236
 *
1✔
237
 * @param elem - The diagram to draw to.
1✔
238
 * @param noteModel - Note model options.
1✔
239
 */
1✔
240
const drawNote = function (elem: any, noteModel: NoteModel) {
1✔
241
  bounds.bumpVerticalPos(conf.boxMargin);
15✔
242
  noteModel.height = conf.boxMargin;
15✔
243
  noteModel.starty = bounds.getVerticalPos();
15✔
244
  const rect = svgDrawCommon.getNoteRect();
15✔
245
  rect.x = noteModel.startx;
15✔
246
  rect.y = noteModel.starty;
15✔
247
  rect.width = noteModel.width || conf.width;
15!
248
  rect.class = 'note';
15✔
249

15✔
250
  const g = elem.append('g');
15✔
251
  const rectElem = svgDraw.drawRect(g, rect);
15✔
252
  const textObj = svgDrawCommon.getTextObj();
15✔
253
  textObj.x = noteModel.startx;
15✔
254
  textObj.y = noteModel.starty;
15✔
255
  textObj.width = rect.width;
15✔
256
  textObj.dy = '1em';
15✔
257
  textObj.text = noteModel.message;
15✔
258
  textObj.class = 'noteText';
15✔
259
  textObj.fontFamily = conf.noteFontFamily;
15✔
260
  textObj.fontSize = conf.noteFontSize;
15✔
261
  textObj.fontWeight = conf.noteFontWeight;
15✔
262
  textObj.anchor = conf.noteAlign;
15✔
263
  textObj.textMargin = conf.noteMargin;
15✔
264
  textObj.valign = 'center';
15✔
265

15✔
266
  const textElem = drawText(g, textObj);
15✔
267

15✔
268
  const textHeight = Math.round(
15✔
269
    textElem
15✔
270
      .map((te) => (te._groups || te)[0][0].getBBox().height)
15✔
271
      .reduce((acc, curr) => acc + curr)
15✔
272
  );
15✔
273

15✔
274
  rectElem.attr('height', textHeight + 2 * conf.noteMargin);
15✔
275
  noteModel.height += textHeight + 2 * conf.noteMargin;
15✔
276
  bounds.bumpVerticalPos(textHeight + 2 * conf.noteMargin);
15✔
277
  noteModel.stopy = noteModel.starty + textHeight + 2 * conf.noteMargin;
15✔
278
  noteModel.stopx = noteModel.startx + rect.width;
15✔
279
  bounds.insert(noteModel.startx, noteModel.starty, noteModel.stopx, noteModel.stopy);
15✔
280
  bounds.models.addNote(noteModel);
15✔
281
};
15✔
282

1✔
283
const messageFont = (cnf) => {
1✔
284
  return {
150✔
285
    fontFamily: cnf.messageFontFamily,
150✔
286
    fontSize: cnf.messageFontSize,
150✔
287
    fontWeight: cnf.messageFontWeight,
150✔
288
  };
150✔
289
};
150✔
290
const noteFont = (cnf) => {
1✔
291
  return {
46✔
292
    fontFamily: cnf.noteFontFamily,
46✔
293
    fontSize: cnf.noteFontSize,
46✔
294
    fontWeight: cnf.noteFontWeight,
46✔
295
  };
46✔
296
};
46✔
297
const actorFont = (cnf) => {
1✔
298
  return {
98✔
299
    fontFamily: cnf.actorFontFamily,
98✔
300
    fontSize: cnf.actorFontSize,
98✔
301
    fontWeight: cnf.actorFontWeight,
98✔
302
  };
98✔
303
};
98✔
304

1✔
305
/**
1✔
306
 * Process a message by adding its dimensions to the bound. It returns the Y coordinate of the
1✔
307
 * message so it can be drawn later. We do not draw the message at this point so the arrowhead can
1✔
308
 * be on top of the activation box.
1✔
309
 *
1✔
310
 * @param _diagram - The parent of the message element.
1✔
311
 * @param msgModel - The model containing fields describing a message
1✔
312
 * @returns `lineStartY` - The Y coordinate at which the message line starts
1✔
313
 */
1✔
314
function boundMessage(_diagram, msgModel): number {
31✔
315
  bounds.bumpVerticalPos(10);
31✔
316
  const { startx, stopx, message } = msgModel;
31✔
317
  const lines = common.splitBreaks(message).length;
31✔
318
  const textDims = utils.calculateTextDimensions(message, messageFont(conf));
31✔
319
  const lineHeight = textDims.height / lines;
31✔
320
  msgModel.height += lineHeight;
31✔
321

31✔
322
  bounds.bumpVerticalPos(lineHeight);
31✔
323

31✔
324
  let lineStartY;
31✔
325
  let totalOffset = textDims.height - 10;
31✔
326
  const textWidth = textDims.width;
31✔
327

31✔
328
  if (startx === stopx) {
31!
329
    lineStartY = bounds.getVerticalPos() + totalOffset;
×
330
    if (!conf.rightAngles) {
×
331
      totalOffset += conf.boxMargin;
×
332
      lineStartY = bounds.getVerticalPos() + totalOffset;
×
333
    }
×
334
    totalOffset += 30;
×
335
    const dx = common.getMax(textWidth / 2, conf.width / 2);
×
336
    bounds.insert(
×
337
      startx - dx,
×
338
      bounds.getVerticalPos() - 10 + totalOffset,
×
339
      stopx + dx,
×
340
      bounds.getVerticalPos() + 30 + totalOffset
×
341
    );
×
342
  } else {
31✔
343
    totalOffset += conf.boxMargin;
31✔
344
    lineStartY = bounds.getVerticalPos() + totalOffset;
31✔
345
    bounds.insert(startx, lineStartY - 10, stopx, lineStartY);
31✔
346
  }
31✔
347
  bounds.bumpVerticalPos(totalOffset);
31✔
348
  msgModel.height += totalOffset;
31✔
349
  msgModel.stopy = msgModel.starty + msgModel.height;
31✔
350
  bounds.insert(msgModel.fromBounds, msgModel.starty, msgModel.toBounds, msgModel.stopy);
31✔
351

31✔
352
  return lineStartY;
31✔
353
}
31✔
354

1✔
355
/**
1✔
356
 * Draws a message. Note that the bounds have previously been updated by boundMessage.
1✔
357
 *
1✔
358
 * @param diagram - The parent of the message element
1✔
359
 * @param msgModel - The model containing fields describing a message
1✔
360
 * @param lineStartY - The Y coordinate at which the message line starts
1✔
361
 * @param diagObj - The diagram object.
1✔
362
 */
1✔
363
const drawMessage = function (diagram, msgModel, lineStartY: number, diagObj: Diagram) {
1✔
364
  const { startx, stopx, starty, message, type, sequenceIndex, sequenceVisible } = msgModel;
31✔
365
  const textDims = utils.calculateTextDimensions(message, messageFont(conf));
31✔
366
  const textObj = svgDrawCommon.getTextObj();
31✔
367
  textObj.x = startx;
31✔
368
  textObj.y = starty + 10;
31✔
369
  textObj.width = stopx - startx;
31✔
370
  textObj.class = 'messageText';
31✔
371
  textObj.dy = '1em';
31✔
372
  textObj.text = message;
31✔
373
  textObj.fontFamily = conf.messageFontFamily;
31✔
374
  textObj.fontSize = conf.messageFontSize;
31✔
375
  textObj.fontWeight = conf.messageFontWeight;
31✔
376
  textObj.anchor = conf.messageAlign;
31✔
377
  textObj.valign = 'center';
31✔
378
  textObj.textMargin = conf.wrapPadding;
31✔
379
  textObj.tspan = false;
31✔
380

31✔
381
  drawText(diagram, textObj);
31✔
382

31✔
383
  const textWidth = textDims.width;
31✔
384

31✔
385
  let line;
31✔
386
  if (startx === stopx) {
31!
387
    if (conf.rightAngles) {
×
388
      line = diagram
×
389
        .append('path')
×
390
        .attr(
×
391
          'd',
×
392
          `M  ${startx},${lineStartY} H ${
×
393
            startx + common.getMax(conf.width / 2, textWidth / 2)
×
394
          } V ${lineStartY + 25} H ${startx}`
×
395
        );
×
396
    } else {
×
397
      line = diagram
×
398
        .append('path')
×
399
        .attr(
×
400
          'd',
×
401
          'M ' +
×
402
            startx +
×
403
            ',' +
×
404
            lineStartY +
×
405
            ' C ' +
×
406
            (startx + 60) +
×
407
            ',' +
×
408
            (lineStartY - 10) +
×
409
            ' ' +
×
410
            (startx + 60) +
×
411
            ',' +
×
412
            (lineStartY + 30) +
×
413
            ' ' +
×
414
            startx +
×
415
            ',' +
×
416
            (lineStartY + 20)
×
417
        );
×
418
    }
×
419
  } else {
31✔
420
    line = diagram.append('line');
31✔
421
    line.attr('x1', startx);
31✔
422
    line.attr('y1', lineStartY);
31✔
423
    line.attr('x2', stopx);
31✔
424
    line.attr('y2', lineStartY);
31✔
425
  }
31✔
426
  // Make an SVG Container
31✔
427
  // Draw the line
31✔
428
  if (
31✔
429
    type === diagObj.db.LINETYPE.DOTTED ||
31✔
430
    type === diagObj.db.LINETYPE.DOTTED_CROSS ||
31✔
431
    type === diagObj.db.LINETYPE.DOTTED_POINT ||
31✔
432
    type === diagObj.db.LINETYPE.DOTTED_OPEN
31✔
433
  ) {
31✔
434
    line.style('stroke-dasharray', '3, 3');
4✔
435
    line.attr('class', 'messageLine1');
4✔
436
  } else {
31✔
437
    line.attr('class', 'messageLine0');
27✔
438
  }
27✔
439

31✔
440
  let url = '';
31✔
441
  if (conf.arrowMarkerAbsolute) {
31!
442
    url =
×
443
      window.location.protocol +
×
444
      '//' +
×
445
      window.location.host +
×
446
      window.location.pathname +
×
447
      window.location.search;
×
448
    url = url.replace(/\(/g, '\\(');
×
449
    url = url.replace(/\)/g, '\\)');
×
450
  }
×
451

31✔
452
  line.attr('stroke-width', 2);
31✔
453
  line.attr('stroke', 'none'); // handled by theme/css anyway
31✔
454
  line.style('fill', 'none'); // remove any fill colour
31✔
455
  if (type === diagObj.db.LINETYPE.SOLID || type === diagObj.db.LINETYPE.DOTTED) {
31✔
456
    line.attr('marker-end', 'url(' + url + '#arrowhead)');
8✔
457
  }
8✔
458
  if (type === diagObj.db.LINETYPE.SOLID_POINT || type === diagObj.db.LINETYPE.DOTTED_POINT) {
31!
459
    line.attr('marker-end', 'url(' + url + '#filled-head)');
×
460
  }
×
461

31✔
462
  if (type === diagObj.db.LINETYPE.SOLID_CROSS || type === diagObj.db.LINETYPE.DOTTED_CROSS) {
31!
463
    line.attr('marker-end', 'url(' + url + '#crosshead)');
×
464
  }
×
465

31✔
466
  // add node number
31✔
467
  if (sequenceVisible || conf.showSequenceNumbers) {
31✔
468
    line.attr('marker-start', 'url(' + url + '#sequencenumber)');
4✔
469
    diagram
4✔
470
      .append('text')
4✔
471
      .attr('x', startx)
4✔
472
      .attr('y', lineStartY + 4)
4✔
473
      .attr('font-family', 'sans-serif')
4✔
474
      .attr('font-size', '12px')
4✔
475
      .attr('text-anchor', 'middle')
4✔
476
      .attr('class', 'sequenceNumber')
4✔
477
      .text(sequenceIndex);
4✔
478
  }
4✔
479
};
31✔
480

1✔
481
export const drawActors = function (
1✔
482
  diagram,
41✔
483
  actors,
41✔
484
  actorKeys,
41✔
485
  verticalPos,
41✔
486
  configuration,
41✔
487
  messages,
41✔
488
  isFooter
41✔
489
) {
41✔
490
  if (configuration.hideUnusedParticipants === true) {
41!
491
    const newActors = new Set();
×
492
    messages.forEach((message) => {
×
493
      newActors.add(message.from);
×
494
      newActors.add(message.to);
×
495
    });
×
496
    actorKeys = actorKeys.filter((actorKey) => newActors.has(actorKey));
×
497
  }
×
498

41✔
499
  // Draw the actors
41✔
500
  let prevWidth = 0;
41✔
501
  let prevMargin = 0;
41✔
502
  let maxHeight = 0;
41✔
503
  let prevBox = undefined;
41✔
504

41✔
505
  for (const actorKey of actorKeys) {
41✔
506
    const actor = actors[actorKey];
63✔
507
    const box = actor.box;
63✔
508

63✔
509
    // end of box
63✔
510
    if (prevBox && prevBox != box) {
63!
511
      if (!isFooter) {
×
512
        bounds.models.addBox(prevBox);
×
513
      }
×
514
      prevMargin += conf.boxMargin + prevBox.margin;
×
515
    }
×
516

63✔
517
    // new box
63✔
518
    if (box && box != prevBox) {
63✔
519
      if (!isFooter) {
1✔
520
        box.x = prevWidth + prevMargin;
1✔
521
        box.y = verticalPos;
1✔
522
      }
1✔
523
      prevMargin += box.margin;
1✔
524
    }
1✔
525

63✔
526
    // Add some rendering data to the object
63✔
527
    actor.width = actor.width || conf.width;
63!
528
    actor.height = common.getMax(actor.height || conf.height, conf.height);
63!
529
    actor.margin = actor.margin || conf.actorMargin;
63✔
530

63✔
531
    actor.x = prevWidth + prevMargin;
63✔
532
    actor.y = bounds.getVerticalPos();
63✔
533

63✔
534
    // Draw the box with the attached line
63✔
535
    const height = svgDraw.drawActor(diagram, actor, conf, isFooter);
63✔
536
    maxHeight = common.getMax(maxHeight, height);
63✔
537
    bounds.insert(actor.x, verticalPos, actor.x + actor.width, actor.height);
63✔
538

63✔
539
    prevWidth += actor.width + prevMargin;
63✔
540
    if (actor.box) {
63✔
541
      actor.box.width = prevWidth + box.margin - actor.box.x;
2✔
542
    }
2✔
543
    prevMargin = actor.margin;
63✔
544
    prevBox = actor.box;
63✔
545
    bounds.models.addActor(actor);
63✔
546
  }
63✔
547

41✔
548
  // end of box
41✔
549
  if (prevBox && !isFooter) {
41✔
550
    bounds.models.addBox(prevBox);
1✔
551
  }
1✔
552

41✔
553
  // Add a margin between the actor boxes and the first arrow
41✔
554
  bounds.bumpVerticalPos(maxHeight);
41✔
555
};
41✔
556

1✔
557
export const drawActorsPopup = function (diagram, actors, actorKeys, doc) {
1✔
558
  let maxHeight = 0;
31✔
559
  let maxWidth = 0;
31✔
560
  for (const actorKey of actorKeys) {
31✔
561
    const actor = actors[actorKey];
49✔
562
    const minMenuWidth = getRequiredPopupWidth(actor);
49✔
563
    const menuDimensions = svgDraw.drawPopup(
49✔
564
      diagram,
49✔
565
      actor,
49✔
566
      minMenuWidth,
49✔
567
      conf,
49✔
568
      conf.forceMenus,
49✔
569
      doc
49✔
570
    );
49✔
571
    if (menuDimensions.height > maxHeight) {
49!
572
      maxHeight = menuDimensions.height;
×
573
    }
×
574
    if (menuDimensions.width + actor.x > maxWidth) {
49✔
575
      maxWidth = menuDimensions.width + actor.x;
19✔
576
    }
19✔
577
  }
49✔
578

31✔
579
  return { maxHeight: maxHeight, maxWidth: maxWidth };
31✔
580
};
31✔
581

1✔
582
export const setConf = function (cnf) {
1✔
583
  assignWithDepth(conf, cnf);
53✔
584

53✔
585
  if (cnf.fontFamily) {
53✔
586
    conf.actorFontFamily = conf.noteFontFamily = conf.messageFontFamily = cnf.fontFamily;
53✔
587
  }
53✔
588
  if (cnf.fontSize) {
53✔
589
    conf.actorFontSize = conf.noteFontSize = conf.messageFontSize = cnf.fontSize;
53✔
590
  }
53✔
591
  if (cnf.fontWeight) {
53!
592
    conf.actorFontWeight = conf.noteFontWeight = conf.messageFontWeight = cnf.fontWeight;
×
593
  }
×
594
};
53✔
595

1✔
596
const actorActivations = function (actor) {
1✔
597
  return bounds.activations.filter(function (activation) {
62✔
598
    return activation.actor === actor;
×
599
  });
62✔
600
};
62✔
601

1✔
602
const activationBounds = function (actor, actors) {
1✔
603
  // handle multiple stacked activations for same actor
62✔
604
  const actorObj = actors[actor];
62✔
605
  const activations = actorActivations(actor);
62✔
606

62✔
607
  const left = activations.reduce(function (acc, activation) {
62✔
608
    return common.getMin(acc, activation.startx);
×
609
  }, actorObj.x + actorObj.width / 2);
62✔
610
  const right = activations.reduce(function (acc, activation) {
62✔
611
    return common.getMax(acc, activation.stopx);
×
612
  }, actorObj.x + actorObj.width / 2);
62✔
613
  return [left, right];
62✔
614
};
62✔
615

1✔
616
function adjustLoopHeightForWrap(loopWidths, msg, preMargin, postMargin, addLoopFn) {
2✔
617
  bounds.bumpVerticalPos(preMargin);
2✔
618
  let heightAdjust = postMargin;
2✔
619
  if (msg.id && msg.message && loopWidths[msg.id]) {
2✔
620
    const loopWidth = loopWidths[msg.id].width;
1✔
621
    const textConf = messageFont(conf);
1✔
622
    msg.message = utils.wrapLabel(`[${msg.message}]`, loopWidth - 2 * conf.wrapPadding, textConf);
1✔
623
    msg.width = loopWidth;
1✔
624
    msg.wrap = true;
1✔
625

1✔
626
    // const lines = common.splitBreaks(msg.message).length;
1✔
627
    const textDims = utils.calculateTextDimensions(msg.message, textConf);
1✔
628
    const totalOffset = common.getMax(textDims.height, conf.labelBoxHeight);
1✔
629
    heightAdjust = postMargin + totalOffset;
1✔
630
    log.debug(`${totalOffset} - ${msg.message}`);
1✔
631
  }
1✔
632
  addLoopFn(msg);
2✔
633
  bounds.bumpVerticalPos(heightAdjust);
2✔
634
}
2✔
635

1✔
636
/**
1✔
637
 * Draws a sequenceDiagram in the tag with id: id based on the graph definition in text.
1✔
638
 *
1✔
639
 * @param _text - The text of the diagram
1✔
640
 * @param id - The id of the diagram which will be used as a DOM element id¨
1✔
641
 * @param _version - Mermaid version from package.json
1✔
642
 * @param diagObj - A standard diagram containing the db and the text and type etc of the diagram
1✔
643
 */
1✔
644
export const draw = function (_text: string, id: string, _version: string, diagObj: Diagram) {
1✔
645
  const { securityLevel, sequence } = configApi.getConfig();
31✔
646
  conf = sequence;
31✔
647
  diagObj.db.clear();
31✔
648
  // Parse the graph definition
31✔
649
  diagObj.parser.parse(_text);
31✔
650
  // Handle root and Document for when rendering in sandbox mode
31✔
651
  let sandboxElement;
31✔
652
  if (securityLevel === 'sandbox') {
31!
653
    sandboxElement = select('#i' + id);
×
654
  }
×
655

31✔
656
  const root =
31✔
657
    securityLevel === 'sandbox'
31!
658
      ? select(sandboxElement.nodes()[0].contentDocument.body)
×
659
      : select('body');
31✔
660
  const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document;
31!
661
  bounds.init();
31✔
662
  log.debug(diagObj.db);
31✔
663

31✔
664
  const diagram =
31✔
665
    securityLevel === 'sandbox' ? root.select(`[id="${id}"]`) : select(`[id="${id}"]`);
31!
666

31✔
667
  // Fetch data from the parsing
31✔
668
  const actors = diagObj.db.getActors();
31✔
669
  const boxes = diagObj.db.getBoxes();
31✔
670
  const actorKeys = diagObj.db.getActorKeys();
31✔
671
  const messages = diagObj.db.getMessages();
31✔
672
  const title = diagObj.db.getDiagramTitle();
31✔
673
  const hasBoxes = diagObj.db.hasAtLeastOneBox();
31✔
674
  const hasBoxTitles = diagObj.db.hasAtLeastOneBoxWithTitle();
31✔
675
  const maxMessageWidthPerActor = getMaxMessageWidthPerActor(actors, messages, diagObj);
31✔
676
  conf.height = calculateActorMargins(actors, maxMessageWidthPerActor, boxes);
31✔
677

31✔
678
  svgDraw.insertComputerIcon(diagram);
31✔
679
  svgDraw.insertDatabaseIcon(diagram);
31✔
680
  svgDraw.insertClockIcon(diagram);
31✔
681

31✔
682
  if (hasBoxes) {
31✔
683
    bounds.bumpVerticalPos(conf.boxMargin);
1✔
684
    if (hasBoxTitles) {
1✔
685
      bounds.bumpVerticalPos(boxes[0].textMaxHeight);
1✔
686
    }
1✔
687
  }
1✔
688

31✔
689
  drawActors(diagram, actors, actorKeys, 0, conf, messages, false);
31✔
690
  const loopWidths = calculateLoopBounds(messages, actors, maxMessageWidthPerActor, diagObj);
31✔
691

31✔
692
  // The arrow head definition is attached to the svg once
31✔
693
  svgDraw.insertArrowHead(diagram);
31✔
694
  svgDraw.insertArrowCrossHead(diagram);
31✔
695
  svgDraw.insertArrowFilledHead(diagram);
31✔
696
  svgDraw.insertSequenceNumber(diagram);
31✔
697

31✔
698
  /**
31✔
699
   * @param msg - The message to draw.
31✔
700
   * @param verticalPos - The vertical position of the message.
31✔
701
   */
31✔
702
  function activeEnd(msg: any, verticalPos: number) {
31✔
703
    const activationData = bounds.endActivation(msg);
×
704
    if (activationData.starty + 18 > verticalPos) {
×
705
      activationData.starty = verticalPos - 6;
×
706
      verticalPos += 12;
×
707
    }
×
708
    svgDraw.drawActivation(
×
709
      diagram,
×
710
      activationData,
×
711
      verticalPos,
×
712
      conf,
×
713
      actorActivations(msg.from.actor).length
×
714
    );
×
715

×
716
    bounds.insert(activationData.startx, verticalPos - 10, activationData.stopx, verticalPos);
×
717
  }
×
718

31✔
719
  // Draw the messages/signals
31✔
720
  let sequenceIndex = 1;
31✔
721
  let sequenceIndexStep = 1;
31✔
722
  const messagesToDraw = [];
31✔
723
  messages.forEach(function (msg) {
31✔
724
    let loopModel, noteModel, msgModel;
52✔
725

52✔
726
    switch (msg.type) {
52✔
727
      case diagObj.db.LINETYPE.NOTE:
52✔
728
        bounds.resetVerticalPos();
15✔
729
        noteModel = msg.noteModel;
15✔
730
        drawNote(diagram, noteModel);
15✔
731
        break;
15✔
732
      case diagObj.db.LINETYPE.ACTIVE_START:
52!
733
        bounds.newActivation(msg, diagram, actors);
×
734
        break;
×
735
      case diagObj.db.LINETYPE.ACTIVE_END:
52!
736
        activeEnd(msg, bounds.getVerticalPos());
×
737
        break;
×
738
      case diagObj.db.LINETYPE.LOOP_START:
52✔
739
        adjustLoopHeightForWrap(
1✔
740
          loopWidths,
1✔
741
          msg,
1✔
742
          conf.boxMargin,
1✔
743
          conf.boxMargin + conf.boxTextMargin,
1✔
744
          (message) => bounds.newLoop(message)
1✔
745
        );
1✔
746
        break;
1✔
747
      case diagObj.db.LINETYPE.LOOP_END:
52✔
748
        loopModel = bounds.endLoop();
1✔
749
        svgDraw.drawLoop(diagram, loopModel, 'loop', conf);
1✔
750
        bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
1✔
751
        bounds.models.addLoop(loopModel);
1✔
752
        break;
1✔
753
      case diagObj.db.LINETYPE.RECT_START:
52✔
754
        adjustLoopHeightForWrap(loopWidths, msg, conf.boxMargin, conf.boxMargin, (message) =>
1✔
755
          bounds.newLoop(undefined, message.message)
1✔
756
        );
1✔
757
        break;
1✔
758
      case diagObj.db.LINETYPE.RECT_END:
52✔
759
        loopModel = bounds.endLoop();
1✔
760
        svgDraw.drawBackgroundRect(diagram, loopModel);
1✔
761
        bounds.models.addLoop(loopModel);
1✔
762
        bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
1✔
763
        break;
1✔
764
      case diagObj.db.LINETYPE.OPT_START:
52!
765
        adjustLoopHeightForWrap(
×
766
          loopWidths,
×
767
          msg,
×
768
          conf.boxMargin,
×
769
          conf.boxMargin + conf.boxTextMargin,
×
770
          (message) => bounds.newLoop(message)
×
771
        );
×
772
        break;
×
773
      case diagObj.db.LINETYPE.OPT_END:
52!
774
        loopModel = bounds.endLoop();
×
775
        svgDraw.drawLoop(diagram, loopModel, 'opt', conf);
×
776
        bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
×
777
        bounds.models.addLoop(loopModel);
×
778
        break;
×
779
      case diagObj.db.LINETYPE.ALT_START:
52!
780
        adjustLoopHeightForWrap(
×
781
          loopWidths,
×
782
          msg,
×
783
          conf.boxMargin,
×
784
          conf.boxMargin + conf.boxTextMargin,
×
785
          (message) => bounds.newLoop(message)
×
786
        );
×
787
        break;
×
788
      case diagObj.db.LINETYPE.ALT_ELSE:
52!
789
        adjustLoopHeightForWrap(
×
790
          loopWidths,
×
791
          msg,
×
792
          conf.boxMargin + conf.boxTextMargin,
×
793
          conf.boxMargin,
×
794
          (message) => bounds.addSectionToLoop(message)
×
795
        );
×
796
        break;
×
797
      case diagObj.db.LINETYPE.ALT_END:
52!
798
        loopModel = bounds.endLoop();
×
799
        svgDraw.drawLoop(diagram, loopModel, 'alt', conf);
×
800
        bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
×
801
        bounds.models.addLoop(loopModel);
×
802
        break;
×
803
      case diagObj.db.LINETYPE.PAR_START:
52!
804
      case diagObj.db.LINETYPE.PAR_OVER_START:
52!
805
        adjustLoopHeightForWrap(
×
806
          loopWidths,
×
807
          msg,
×
808
          conf.boxMargin,
×
809
          conf.boxMargin + conf.boxTextMargin,
×
810
          (message) => bounds.newLoop(message)
×
811
        );
×
812
        bounds.saveVerticalPos();
×
813
        break;
×
814
      case diagObj.db.LINETYPE.PAR_AND:
52!
815
        adjustLoopHeightForWrap(
×
816
          loopWidths,
×
817
          msg,
×
818
          conf.boxMargin + conf.boxTextMargin,
×
819
          conf.boxMargin,
×
820
          (message) => bounds.addSectionToLoop(message)
×
821
        );
×
822
        break;
×
823
      case diagObj.db.LINETYPE.PAR_END:
52!
824
        loopModel = bounds.endLoop();
×
825
        svgDraw.drawLoop(diagram, loopModel, 'par', conf);
×
826
        bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
×
827
        bounds.models.addLoop(loopModel);
×
828
        break;
×
829
      case diagObj.db.LINETYPE.AUTONUMBER:
52✔
830
        sequenceIndex = msg.message.start || sequenceIndex;
2✔
831
        sequenceIndexStep = msg.message.step || sequenceIndexStep;
2✔
832
        if (msg.message.visible) {
2✔
833
          diagObj.db.enableSequenceNumbers();
2✔
834
        } else {
2!
835
          diagObj.db.disableSequenceNumbers();
×
836
        }
×
837
        break;
2✔
838
      case diagObj.db.LINETYPE.CRITICAL_START:
52!
839
        adjustLoopHeightForWrap(
×
840
          loopWidths,
×
841
          msg,
×
842
          conf.boxMargin,
×
843
          conf.boxMargin + conf.boxTextMargin,
×
844
          (message) => bounds.newLoop(message)
×
845
        );
×
846
        break;
×
847
      case diagObj.db.LINETYPE.CRITICAL_OPTION:
52!
848
        adjustLoopHeightForWrap(
×
849
          loopWidths,
×
850
          msg,
×
851
          conf.boxMargin + conf.boxTextMargin,
×
852
          conf.boxMargin,
×
853
          (message) => bounds.addSectionToLoop(message)
×
854
        );
×
855
        break;
×
856
      case diagObj.db.LINETYPE.CRITICAL_END:
52!
857
        loopModel = bounds.endLoop();
×
858
        svgDraw.drawLoop(diagram, loopModel, 'critical', conf);
×
859
        bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
×
860
        bounds.models.addLoop(loopModel);
×
861
        break;
×
862
      case diagObj.db.LINETYPE.BREAK_START:
52!
863
        adjustLoopHeightForWrap(
×
864
          loopWidths,
×
865
          msg,
×
866
          conf.boxMargin,
×
867
          conf.boxMargin + conf.boxTextMargin,
×
868
          (message) => bounds.newLoop(message)
×
869
        );
×
870
        break;
×
871
      case diagObj.db.LINETYPE.BREAK_END:
52!
872
        loopModel = bounds.endLoop();
×
873
        svgDraw.drawLoop(diagram, loopModel, 'break', conf);
×
874
        bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
×
875
        bounds.models.addLoop(loopModel);
×
876
        break;
×
877
      default:
52✔
878
        try {
31✔
879
          // lastMsg = msg
31✔
880
          bounds.resetVerticalPos();
31✔
881
          msgModel = msg.msgModel;
31✔
882
          msgModel.starty = bounds.getVerticalPos();
31✔
883
          msgModel.sequenceIndex = sequenceIndex;
31✔
884
          msgModel.sequenceVisible = diagObj.db.showSequenceNumbers();
31✔
885
          const lineStartY = boundMessage(diagram, msgModel);
31✔
886
          messagesToDraw.push({ messageModel: msgModel, lineStartY: lineStartY });
31✔
887
          bounds.models.addMessage(msgModel);
31✔
888
        } catch (e) {
31!
889
          log.error('error while drawing message', e);
×
890
        }
×
891
    }
52✔
892

52✔
893
    // Increment sequence counter if msg.type is a line (and not another event like activation or note, etc)
52✔
894
    if (
52✔
895
      [
52✔
896
        diagObj.db.LINETYPE.SOLID_OPEN,
52✔
897
        diagObj.db.LINETYPE.DOTTED_OPEN,
52✔
898
        diagObj.db.LINETYPE.SOLID,
52✔
899
        diagObj.db.LINETYPE.DOTTED,
52✔
900
        diagObj.db.LINETYPE.SOLID_CROSS,
52✔
901
        diagObj.db.LINETYPE.DOTTED_CROSS,
52✔
902
        diagObj.db.LINETYPE.SOLID_POINT,
52✔
903
        diagObj.db.LINETYPE.DOTTED_POINT,
52✔
904
      ].includes(msg.type)
52✔
905
    ) {
52✔
906
      sequenceIndex = sequenceIndex + sequenceIndexStep;
31✔
907
    }
31✔
908
  });
31✔
909

31✔
910
  messagesToDraw.forEach((e) => drawMessage(diagram, e.messageModel, e.lineStartY, diagObj));
31✔
911

31✔
912
  if (conf.mirrorActors) {
31✔
913
    // Draw actors below diagram
10✔
914
    bounds.bumpVerticalPos(conf.boxMargin * 2);
10✔
915
    drawActors(diagram, actors, actorKeys, bounds.getVerticalPos(), conf, messages, true);
10✔
916
    bounds.bumpVerticalPos(conf.boxMargin);
10✔
917
    fixLifeLineHeights(diagram, bounds.getVerticalPos());
10✔
918
  }
10✔
919

31✔
920
  bounds.models.boxes.forEach(function (box) {
31✔
921
    box.height = bounds.getVerticalPos() - box.y;
1✔
922
    bounds.insert(box.x, box.y, box.x + box.width, box.height);
1✔
923
    box.startx = box.x;
1✔
924
    box.starty = box.y;
1✔
925
    box.stopx = box.startx + box.width;
1✔
926
    box.stopy = box.starty + box.height;
1✔
927
    box.stroke = 'rgb(0,0,0, 0.5)';
1✔
928
    svgDraw.drawBox(diagram, box, conf);
1✔
929
  });
31✔
930

31✔
931
  if (hasBoxes) {
31✔
932
    bounds.bumpVerticalPos(conf.boxMargin);
1✔
933
  }
1✔
934

31✔
935
  // only draw popups for the top row of actors.
31✔
936
  const requiredBoxSize = drawActorsPopup(diagram, actors, actorKeys, doc);
31✔
937

31✔
938
  const { bounds: box } = bounds.getBounds();
31✔
939

31✔
940
  // Adjust line height of actor lines now that the height of the diagram is known
31✔
941
  log.debug('For line height fix Querying: #' + id + ' .actor-line');
31✔
942
  const actorLines = selectAll('#' + id + ' .actor-line');
31✔
943
  actorLines.attr('y2', box.stopy);
31✔
944

31✔
945
  // Make sure the height of the diagram supports long menus.
31✔
946
  let boxHeight = box.stopy - box.starty;
31✔
947
  if (boxHeight < requiredBoxSize.maxHeight) {
31!
948
    boxHeight = requiredBoxSize.maxHeight;
×
949
  }
×
950

31✔
951
  let height = boxHeight + 2 * conf.diagramMarginY;
31✔
952
  if (conf.mirrorActors) {
31✔
953
    height = height - conf.boxMargin + conf.bottomMarginAdj;
10✔
954
  }
10✔
955

31✔
956
  // Make sure the width of the diagram supports wide menus.
31✔
957
  let boxWidth = box.stopx - box.startx;
31✔
958
  if (boxWidth < requiredBoxSize.maxWidth) {
31!
959
    boxWidth = requiredBoxSize.maxWidth;
×
960
  }
×
961
  const width = boxWidth + 2 * conf.diagramMarginX;
31✔
962

31✔
963
  if (title) {
31!
964
    diagram
×
965
      .append('text')
×
966
      .text(title)
×
967
      .attr('x', (box.stopx - box.startx) / 2 - 2 * conf.diagramMarginX)
×
968
      .attr('y', -25);
×
969
  }
×
970

31✔
971
  configureSvgSize(diagram, height, width, conf.useMaxWidth);
31✔
972

31✔
973
  const extraVertForTitle = title ? 40 : 0;
31!
974
  diagram.attr(
31✔
975
    'viewBox',
31✔
976
    box.startx -
31✔
977
      conf.diagramMarginX +
31✔
978
      ' -' +
31✔
979
      (conf.diagramMarginY + extraVertForTitle) +
31✔
980
      ' ' +
31✔
981
      width +
31✔
982
      ' ' +
31✔
983
      (height + extraVertForTitle)
31✔
984
  );
31✔
985

31✔
986
  log.debug(`models:`, bounds.models);
31✔
987
};
31✔
988

1✔
989
/**
1✔
990
 * Retrieves the max message width of each actor, supports signals (messages, loops) and notes.
1✔
991
 *
1✔
992
 * It will enumerate each given message, and will determine its text width, in relation to the actor
1✔
993
 * it originates from, and destined to.
1✔
994
 *
1✔
995
 * @param actors - The actors map
1✔
996
 * @param messages - A list of message objects to iterate
1✔
997
 * @param diagObj - The diagram object.
1✔
998
 * @returns The max message width of each actor.
1✔
999
 */
1✔
1000
function getMaxMessageWidthPerActor(
31✔
1001
  actors: { [id: string]: any },
31✔
1002
  messages: any[],
31✔
1003
  diagObj: Diagram
31✔
1004
): { [id: string]: number } {
31✔
1005
  const maxMessageWidthPerActor = {};
31✔
1006

31✔
1007
  messages.forEach(function (msg) {
31✔
1008
    if (actors[msg.to] && actors[msg.from]) {
52✔
1009
      const actor = actors[msg.to];
46✔
1010

46✔
1011
      // If this is the first actor, and the message is left of it, no need to calculate the margin
46✔
1012
      if (msg.placement === diagObj.db.PLACEMENT.LEFTOF && !actor.prevActor) {
46!
1013
        return;
×
1014
      }
×
1015

46✔
1016
      // If this is the last actor, and the message is right of it, no need to calculate the margin
46✔
1017
      if (msg.placement === diagObj.db.PLACEMENT.RIGHTOF && !actor.nextActor) {
46✔
1018
        return;
5✔
1019
      }
5✔
1020

41✔
1021
      const isNote = msg.placement !== undefined;
41✔
1022
      const isMessage = !isNote;
41✔
1023

41✔
1024
      const textFont = isNote ? noteFont(conf) : messageFont(conf);
46✔
1025
      const wrappedMessage = msg.wrap
46✔
1026
        ? utils.wrapLabel(msg.message, conf.width - 2 * conf.wrapPadding, textFont)
31✔
1027
        : msg.message;
10✔
1028
      const messageDimensions = utils.calculateTextDimensions(wrappedMessage, textFont);
46✔
1029
      const messageWidth = messageDimensions.width + 2 * conf.wrapPadding;
46✔
1030

46✔
1031
      /*
46✔
1032
       * The following scenarios should be supported:
46✔
1033
       *
46✔
1034
       * - There's a message (non-note) between fromActor and toActor
46✔
1035
       *   - If fromActor is on the right and toActor is on the left, we should
46✔
1036
       *     define the toActor's margin
46✔
1037
       *   - If fromActor is on the left and toActor is on the right, we should
46✔
1038
       *     define the fromActor's margin
46✔
1039
       * - There's a note, in which case fromActor == toActor
46✔
1040
       *   - If the note is to the left of the actor, we should define the previous actor
46✔
1041
       *     margin
46✔
1042
       *   - If the note is on the actor, we should define both the previous and next actor
46✔
1043
       *     margins, each being the half of the note size
46✔
1044
       *   - If the note is on the right of the actor, we should define the current actor
46✔
1045
       *     margin
46✔
1046
       */
46✔
1047
      if (isMessage && msg.from === actor.nextActor) {
46✔
1048
        maxMessageWidthPerActor[msg.to] = common.getMax(
13✔
1049
          maxMessageWidthPerActor[msg.to] || 0,
13!
1050
          messageWidth
13✔
1051
        );
13✔
1052
      } else if (isMessage && msg.from === actor.prevActor) {
46✔
1053
        maxMessageWidthPerActor[msg.from] = common.getMax(
18✔
1054
          maxMessageWidthPerActor[msg.from] || 0,
18✔
1055
          messageWidth
18✔
1056
        );
18✔
1057
      } else if (isMessage && msg.from === msg.to) {
28!
1058
        maxMessageWidthPerActor[msg.from] = common.getMax(
×
1059
          maxMessageWidthPerActor[msg.from] || 0,
×
1060
          messageWidth / 2
×
1061
        );
×
1062

×
1063
        maxMessageWidthPerActor[msg.to] = common.getMax(
×
1064
          maxMessageWidthPerActor[msg.to] || 0,
×
1065
          messageWidth / 2
×
1066
        );
×
1067
      } else if (msg.placement === diagObj.db.PLACEMENT.RIGHTOF) {
10✔
1068
        maxMessageWidthPerActor[msg.from] = common.getMax(
1✔
1069
          maxMessageWidthPerActor[msg.from] || 0,
1✔
1070
          messageWidth
1✔
1071
        );
1✔
1072
      } else if (msg.placement === diagObj.db.PLACEMENT.LEFTOF) {
10✔
1073
        maxMessageWidthPerActor[actor.prevActor] = common.getMax(
6✔
1074
          maxMessageWidthPerActor[actor.prevActor] || 0,
6✔
1075
          messageWidth
6✔
1076
        );
6✔
1077
      } else if (msg.placement === diagObj.db.PLACEMENT.OVER) {
9✔
1078
        if (actor.prevActor) {
3✔
1079
          maxMessageWidthPerActor[actor.prevActor] = common.getMax(
3✔
1080
            maxMessageWidthPerActor[actor.prevActor] || 0,
3✔
1081
            messageWidth / 2
3✔
1082
          );
3✔
1083
        }
3✔
1084

3✔
1085
        if (actor.nextActor) {
3✔
1086
          maxMessageWidthPerActor[msg.from] = common.getMax(
2✔
1087
            maxMessageWidthPerActor[msg.from] || 0,
2!
1088
            messageWidth / 2
2✔
1089
          );
2✔
1090
        }
2✔
1091
      }
3✔
1092
    }
46✔
1093
  });
31✔
1094

31✔
1095
  log.debug('maxMessageWidthPerActor:', maxMessageWidthPerActor);
31✔
1096
  return maxMessageWidthPerActor;
31✔
1097
}
31✔
1098

1✔
1099
const getRequiredPopupWidth = function (actor) {
1✔
1100
  let requiredPopupWidth = 0;
49✔
1101
  const textFont = actorFont(conf);
49✔
1102
  for (const key in actor.links) {
49!
1103
    const labelDimensions = utils.calculateTextDimensions(key, textFont);
×
1104
    const labelWidth = labelDimensions.width + 2 * conf.wrapPadding + 2 * conf.boxMargin;
×
1105
    if (requiredPopupWidth < labelWidth) {
×
1106
      requiredPopupWidth = labelWidth;
×
1107
    }
×
1108
  }
×
1109

49✔
1110
  return requiredPopupWidth;
49✔
1111
};
49✔
1112

1✔
1113
/**
1✔
1114
 * This will calculate the optimal margin for each given actor,
1✔
1115
 * for a given actor → messageWidth map.
1✔
1116
 *
1✔
1117
 * An actor's margin is determined by the width of the actor, the width of the largest message that
1✔
1118
 * originates from it, and the configured conf.actorMargin.
1✔
1119
 *
1✔
1120
 * @param actors - The actors map to calculate margins for
1✔
1121
 * @param actorToMessageWidth - A map of actor key → max message width it holds
1✔
1122
 * @param boxes - The boxes around the actors if any
1✔
1123
 */
1✔
1124
function calculateActorMargins(
31✔
1125
  actors: { [id: string]: any },
31✔
1126
  actorToMessageWidth: ReturnType<typeof getMaxMessageWidthPerActor>,
31✔
1127
  boxes
31✔
1128
) {
31✔
1129
  let maxHeight = 0;
31✔
1130
  Object.keys(actors).forEach((prop) => {
31✔
1131
    const actor = actors[prop];
49✔
1132
    if (actor.wrap) {
49!
1133
      actor.description = utils.wrapLabel(
×
1134
        actor.description,
×
1135
        conf.width - 2 * conf.wrapPadding,
×
1136
        actorFont(conf)
×
1137
      );
×
1138
    }
×
1139
    const actDims = utils.calculateTextDimensions(actor.description, actorFont(conf));
49✔
1140
    actor.width = actor.wrap
49!
1141
      ? conf.width
×
1142
      : common.getMax(conf.width, actDims.width + 2 * conf.wrapPadding);
49✔
1143

49✔
1144
    actor.height = actor.wrap ? common.getMax(actDims.height, conf.height) : conf.height;
49!
1145
    maxHeight = common.getMax(maxHeight, actor.height);
49✔
1146
  });
31✔
1147

31✔
1148
  for (const actorKey in actorToMessageWidth) {
31✔
1149
    const actor = actors[actorKey];
27✔
1150

27✔
1151
    if (!actor) {
27!
1152
      continue;
×
1153
    }
×
1154

27✔
1155
    const nextActor = actors[actor.nextActor];
27✔
1156

27✔
1157
    // No need to space out an actor that doesn't have a next link
27✔
1158
    if (!nextActor) {
27✔
1159
      const messageWidth = actorToMessageWidth[actorKey];
6✔
1160
      const actorWidth = messageWidth + conf.actorMargin - actor.width / 2;
6✔
1161
      actor.margin = common.getMax(actorWidth, conf.actorMargin);
6✔
1162
      continue;
6✔
1163
    }
6✔
1164

21✔
1165
    const messageWidth = actorToMessageWidth[actorKey];
21✔
1166
    const actorWidth = messageWidth + conf.actorMargin - actor.width / 2 - nextActor.width / 2;
21✔
1167

21✔
1168
    actor.margin = common.getMax(actorWidth, conf.actorMargin);
21✔
1169
  }
21✔
1170

31✔
1171
  let maxBoxHeight = 0;
31✔
1172
  boxes.forEach((box) => {
31✔
1173
    const textFont = messageFont(conf);
1✔
1174
    let totalWidth = box.actorKeys.reduce((total, aKey) => {
1✔
1175
      return (total += actors[aKey].width + (actors[aKey].margin || 0));
2✔
1176
    }, 0);
1✔
1177

1✔
1178
    totalWidth -= 2 * conf.boxTextMargin;
1✔
1179
    if (box.wrap) {
1!
1180
      box.name = utils.wrapLabel(box.name, totalWidth - 2 * conf.wrapPadding, textFont);
×
1181
    }
×
1182

1✔
1183
    const boxMsgDimensions = utils.calculateTextDimensions(box.name, textFont);
1✔
1184
    maxBoxHeight = common.getMax(boxMsgDimensions.height, maxBoxHeight);
1✔
1185
    const minWidth = common.getMax(totalWidth, boxMsgDimensions.width + 2 * conf.wrapPadding);
1✔
1186
    box.margin = conf.boxTextMargin;
1✔
1187
    if (totalWidth < minWidth) {
1!
1188
      const missing = (minWidth - totalWidth) / 2;
×
1189
      box.margin += missing;
×
1190
    }
×
1191
  });
31✔
1192
  boxes.forEach((box) => (box.textMaxHeight = maxBoxHeight));
31✔
1193

31✔
1194
  return common.getMax(maxHeight, conf.height);
31✔
1195
}
31✔
1196

1✔
1197
const buildNoteModel = function (msg, actors, diagObj) {
1✔
1198
  const startx = actors[msg.from].x;
15✔
1199
  const stopx = actors[msg.to].x;
15✔
1200
  const shouldWrap = msg.wrap && msg.message;
15✔
1201

15✔
1202
  let textDimensions = utils.calculateTextDimensions(
15✔
1203
    shouldWrap ? utils.wrapLabel(msg.message, conf.width, noteFont(conf)) : msg.message,
15✔
1204
    noteFont(conf)
15✔
1205
  );
15✔
1206
  const noteModel = {
15✔
1207
    width: shouldWrap
15✔
1208
      ? conf.width
10✔
1209
      : common.getMax(conf.width, textDimensions.width + 2 * conf.noteMargin),
5✔
1210
    height: 0,
15✔
1211
    startx: actors[msg.from].x,
15✔
1212
    stopx: 0,
15✔
1213
    starty: 0,
15✔
1214
    stopy: 0,
15✔
1215
    message: msg.message,
15✔
1216
  };
15✔
1217
  if (msg.placement === diagObj.db.PLACEMENT.RIGHTOF) {
15✔
1218
    noteModel.width = shouldWrap
6✔
1219
      ? common.getMax(conf.width, textDimensions.width)
3✔
1220
      : common.getMax(
3✔
1221
          actors[msg.from].width / 2 + actors[msg.to].width / 2,
3✔
1222
          textDimensions.width + 2 * conf.noteMargin
3✔
1223
        );
3✔
1224
    noteModel.startx = startx + (actors[msg.from].width + conf.actorMargin) / 2;
6✔
1225
  } else if (msg.placement === diagObj.db.PLACEMENT.LEFTOF) {
15✔
1226
    noteModel.width = shouldWrap
6✔
1227
      ? common.getMax(conf.width, textDimensions.width + 2 * conf.noteMargin)
5✔
1228
      : common.getMax(
1✔
1229
          actors[msg.from].width / 2 + actors[msg.to].width / 2,
1✔
1230
          textDimensions.width + 2 * conf.noteMargin
1✔
1231
        );
1✔
1232
    noteModel.startx = startx - noteModel.width + (actors[msg.from].width - conf.actorMargin) / 2;
6✔
1233
  } else if (msg.to === msg.from) {
9✔
1234
    textDimensions = utils.calculateTextDimensions(
1✔
1235
      shouldWrap
1!
1236
        ? utils.wrapLabel(
×
1237
            msg.message,
×
1238
            common.getMax(conf.width, actors[msg.from].width),
×
1239
            noteFont(conf)
×
1240
          )
×
1241
        : msg.message,
1✔
1242
      noteFont(conf)
1✔
1243
    );
1✔
1244
    noteModel.width = shouldWrap
1!
1245
      ? common.getMax(conf.width, actors[msg.from].width)
×
1246
      : common.getMax(
1✔
1247
          actors[msg.from].width,
1✔
1248
          conf.width,
1✔
1249
          textDimensions.width + 2 * conf.noteMargin
1✔
1250
        );
1✔
1251
    noteModel.startx = startx + (actors[msg.from].width - noteModel.width) / 2;
1✔
1252
  } else {
3✔
1253
    noteModel.width =
2✔
1254
      Math.abs(startx + actors[msg.from].width / 2 - (stopx + actors[msg.to].width / 2)) +
2✔
1255
      conf.actorMargin;
2✔
1256
    noteModel.startx =
2✔
1257
      startx < stopx
2✔
1258
        ? startx + actors[msg.from].width / 2 - conf.actorMargin / 2
1✔
1259
        : stopx + actors[msg.to].width / 2 - conf.actorMargin / 2;
1✔
1260
  }
2✔
1261
  if (shouldWrap) {
15✔
1262
    noteModel.message = utils.wrapLabel(
10✔
1263
      msg.message,
10✔
1264
      noteModel.width - 2 * conf.wrapPadding,
10✔
1265
      noteFont(conf)
10✔
1266
    );
10✔
1267
  }
10✔
1268
  log.debug(
15✔
1269
    `NM:[${noteModel.startx},${noteModel.stopx},${noteModel.starty},${noteModel.stopy}:${noteModel.width},${noteModel.height}=${msg.message}]`
15✔
1270
  );
15✔
1271
  return noteModel;
15✔
1272
};
15✔
1273

1✔
1274
const buildMessageModel = function (msg, actors, diagObj) {
1✔
1275
  let process = false;
37✔
1276
  if (
37✔
1277
    [
37✔
1278
      diagObj.db.LINETYPE.SOLID_OPEN,
37✔
1279
      diagObj.db.LINETYPE.DOTTED_OPEN,
37✔
1280
      diagObj.db.LINETYPE.SOLID,
37✔
1281
      diagObj.db.LINETYPE.DOTTED,
37✔
1282
      diagObj.db.LINETYPE.SOLID_CROSS,
37✔
1283
      diagObj.db.LINETYPE.DOTTED_CROSS,
37✔
1284
      diagObj.db.LINETYPE.SOLID_POINT,
37✔
1285
      diagObj.db.LINETYPE.DOTTED_POINT,
37✔
1286
    ].includes(msg.type)
37✔
1287
  ) {
37✔
1288
    process = true;
31✔
1289
  }
31✔
1290
  if (!process) {
37✔
1291
    return {};
6✔
1292
  }
6✔
1293
  const fromBounds = activationBounds(msg.from, actors);
31✔
1294
  const toBounds = activationBounds(msg.to, actors);
31✔
1295
  const fromIdx = fromBounds[0] <= toBounds[0] ? 1 : 0;
37✔
1296
  const toIdx = fromBounds[0] < toBounds[0] ? 0 : 1;
37✔
1297
  const allBounds = [...fromBounds, ...toBounds];
37✔
1298
  const boundedWidth = Math.abs(toBounds[toIdx] - fromBounds[fromIdx]);
37✔
1299
  if (msg.wrap && msg.message) {
37✔
1300
    msg.message = utils.wrapLabel(
24✔
1301
      msg.message,
24✔
1302
      common.getMax(boundedWidth + 2 * conf.wrapPadding, conf.width),
24✔
1303
      messageFont(conf)
24✔
1304
    );
24✔
1305
  }
24✔
1306
  const msgDims = utils.calculateTextDimensions(msg.message, messageFont(conf));
31✔
1307

31✔
1308
  return {
31✔
1309
    width: common.getMax(
31✔
1310
      msg.wrap ? 0 : msgDims.width + 2 * conf.wrapPadding,
37✔
1311
      boundedWidth + 2 * conf.wrapPadding,
37✔
1312
      conf.width
37✔
1313
    ),
37✔
1314
    height: 0,
37✔
1315
    startx: fromBounds[fromIdx],
37✔
1316
    stopx: toBounds[toIdx],
37✔
1317
    starty: 0,
37✔
1318
    stopy: 0,
37✔
1319
    message: msg.message,
37✔
1320
    type: msg.type,
37✔
1321
    wrap: msg.wrap,
37✔
1322
    fromBounds: Math.min.apply(null, allBounds),
37✔
1323
    toBounds: Math.max.apply(null, allBounds),
37✔
1324
  };
37✔
1325
};
37✔
1326

1✔
1327
const calculateLoopBounds = function (messages, actors, _maxWidthPerActor, diagObj) {
1✔
1328
  const loops = {};
31✔
1329
  const stack = [];
31✔
1330
  let current, noteModel, msgModel;
31✔
1331

31✔
1332
  messages.forEach(function (msg) {
31✔
1333
    msg.id = utils.random({ length: 10 });
52✔
1334
    switch (msg.type) {
52✔
1335
      case diagObj.db.LINETYPE.LOOP_START:
52✔
1336
      case diagObj.db.LINETYPE.ALT_START:
52✔
1337
      case diagObj.db.LINETYPE.OPT_START:
52✔
1338
      case diagObj.db.LINETYPE.PAR_START:
52✔
1339
      case diagObj.db.LINETYPE.PAR_OVER_START:
52✔
1340
      case diagObj.db.LINETYPE.CRITICAL_START:
52✔
1341
      case diagObj.db.LINETYPE.BREAK_START:
52✔
1342
        stack.push({
1✔
1343
          id: msg.id,
1✔
1344
          msg: msg.message,
1✔
1345
          from: Number.MAX_SAFE_INTEGER,
1✔
1346
          to: Number.MIN_SAFE_INTEGER,
1✔
1347
          width: 0,
1✔
1348
        });
1✔
1349
        break;
1✔
1350
      case diagObj.db.LINETYPE.ALT_ELSE:
52!
1351
      case diagObj.db.LINETYPE.PAR_AND:
52!
1352
      case diagObj.db.LINETYPE.CRITICAL_OPTION:
52!
1353
        if (msg.message) {
×
1354
          current = stack.pop();
×
1355
          loops[current.id] = current;
×
1356
          loops[msg.id] = current;
×
1357
          stack.push(current);
×
1358
        }
×
1359
        break;
×
1360
      case diagObj.db.LINETYPE.LOOP_END:
52✔
1361
      case diagObj.db.LINETYPE.ALT_END:
52✔
1362
      case diagObj.db.LINETYPE.OPT_END:
52✔
1363
      case diagObj.db.LINETYPE.PAR_END:
52✔
1364
      case diagObj.db.LINETYPE.CRITICAL_END:
52✔
1365
      case diagObj.db.LINETYPE.BREAK_END:
52✔
1366
        current = stack.pop();
1✔
1367
        loops[current.id] = current;
1✔
1368
        break;
1✔
1369
      case diagObj.db.LINETYPE.ACTIVE_START:
52!
1370
        {
×
1371
          const actorRect = actors[msg.from ? msg.from.actor : msg.to.actor];
×
1372
          const stackedSize = actorActivations(msg.from ? msg.from.actor : msg.to.actor).length;
×
1373
          const x =
×
1374
            actorRect.x + actorRect.width / 2 + ((stackedSize - 1) * conf.activationWidth) / 2;
×
1375
          const toAdd = {
×
1376
            startx: x,
×
1377
            stopx: x + conf.activationWidth,
×
1378
            actor: msg.from.actor,
×
1379
            enabled: true,
×
1380
          };
×
1381
          bounds.activations.push(toAdd);
×
1382
        }
×
1383
        break;
×
1384
      case diagObj.db.LINETYPE.ACTIVE_END:
52!
1385
        {
×
1386
          const lastActorActivationIdx = bounds.activations
×
1387
            .map((a) => a.actor)
×
1388
            .lastIndexOf(msg.from.actor);
×
1389
          delete bounds.activations.splice(lastActorActivationIdx, 1)[0];
×
1390
        }
×
1391
        break;
×
1392
    }
52✔
1393
    const isNote = msg.placement !== undefined;
52✔
1394
    if (isNote) {
52✔
1395
      noteModel = buildNoteModel(msg, actors, diagObj);
15✔
1396
      msg.noteModel = noteModel;
15✔
1397
      stack.forEach((stk) => {
15✔
1398
        current = stk;
×
1399
        current.from = common.getMin(current.from, noteModel.startx);
×
1400
        current.to = common.getMax(current.to, noteModel.startx + noteModel.width);
×
1401
        current.width =
×
1402
          common.getMax(current.width, Math.abs(current.from - current.to)) - conf.labelBoxWidth;
×
1403
      });
15✔
1404
    } else {
52✔
1405
      msgModel = buildMessageModel(msg, actors, diagObj);
37✔
1406
      msg.msgModel = msgModel;
37✔
1407
      if (msgModel.startx && msgModel.stopx && stack.length > 0) {
37✔
1408
        stack.forEach((stk) => {
1✔
1409
          current = stk;
1✔
1410
          if (msgModel.startx === msgModel.stopx) {
1!
1411
            const from = actors[msg.from];
×
1412
            const to = actors[msg.to];
×
1413
            current.from = common.getMin(
×
1414
              from.x - msgModel.width / 2,
×
1415
              from.x - from.width / 2,
×
1416
              current.from
×
1417
            );
×
1418
            current.to = common.getMax(
×
1419
              to.x + msgModel.width / 2,
×
1420
              to.x + from.width / 2,
×
1421
              current.to
×
1422
            );
×
1423
            current.width =
×
1424
              common.getMax(current.width, Math.abs(current.to - current.from)) -
×
1425
              conf.labelBoxWidth;
×
1426
          } else {
1✔
1427
            current.from = common.getMin(msgModel.startx, current.from);
1✔
1428
            current.to = common.getMax(msgModel.stopx, current.to);
1✔
1429
            current.width = common.getMax(current.width, msgModel.width) - conf.labelBoxWidth;
1✔
1430
          }
1✔
1431
        });
1✔
1432
      }
1✔
1433
    }
37✔
1434
  });
31✔
1435
  bounds.activations = [];
31✔
1436
  log.debug('Loop type widths:', loops);
31✔
1437
  return loops;
31✔
1438
};
31✔
1439

1✔
1440
export default {
1✔
1441
  bounds,
1✔
1442
  drawActors,
1✔
1443
  drawActorsPopup,
1✔
1444
  setConf,
1✔
1445
  draw,
1✔
1446
};
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