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

mermaid-js / mermaid / 4124356557

pending completion
4124356557

push

github

Knut Sveidqvist
Merge branch 'develop' into release/9.4.0

1565 of 1948 branches covered (80.34%)

Branch coverage included in aggregate %.

15432 of 30283 relevant lines covered (50.96%)

450.46 hits per line

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

76.73
/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
import { convert } from '../../tests/util';
1✔
14

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

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

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

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

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

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

153✔
132
    this.sequenceItems.forEach(updateFn());
153✔
133
    this.activations.forEach(updateFn('activation'));
153✔
134
  },
153✔
135
  insert: function (startx, starty, stopx, stopy) {
1✔
136
    const _startx = Math.min(startx, stopx);
153✔
137
    const _stopx = Math.max(startx, stopx);
153✔
138
    const _starty = Math.min(starty, stopy);
153✔
139
    const _stopy = Math.max(starty, stopy);
153✔
140

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

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

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

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

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

15✔
253
  const textElem = drawText(g, textObj);
15✔
254

15✔
255
  const textHeight = Math.round(
15✔
256
    textElem
15✔
257
      .map((te) => (te._groups || te)[0][0].getBBox().height)
15✔
258
      .reduce((acc, curr) => acc + curr)
15✔
259
  );
15✔
260

15✔
261
  rectElem.attr('height', textHeight + 2 * conf.noteMargin);
15✔
262
  noteModel.height += textHeight + 2 * conf.noteMargin;
15✔
263
  bounds.bumpVerticalPos(textHeight + 2 * conf.noteMargin);
15✔
264
  noteModel.stopy = noteModel.starty + textHeight + 2 * conf.noteMargin;
15✔
265
  noteModel.stopx = noteModel.startx + rect.width;
15✔
266
  bounds.insert(noteModel.startx, noteModel.starty, noteModel.stopx, noteModel.stopy);
15✔
267
  bounds.models.addNote(noteModel);
15✔
268
};
15✔
269

1✔
270
const messageFont = (cnf) => {
1✔
271
  return {
134✔
272
    fontFamily: cnf.messageFontFamily,
134✔
273
    fontSize: cnf.messageFontSize,
134✔
274
    fontWeight: cnf.messageFontWeight,
134✔
275
  };
134✔
276
};
134✔
277
const noteFont = (cnf) => {
1✔
278
  return {
32✔
279
    fontFamily: cnf.noteFontFamily,
32✔
280
    fontSize: cnf.noteFontSize,
32✔
281
    fontWeight: cnf.noteFontWeight,
32✔
282
  };
32✔
283
};
32✔
284
const actorFont = (cnf) => {
1✔
285
  return {
98✔
286
    fontFamily: cnf.actorFontFamily,
98✔
287
    fontSize: cnf.actorFontSize,
98✔
288
    fontWeight: cnf.actorFontWeight,
98✔
289
  };
98✔
290
};
98✔
291

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

31✔
309
  bounds.bumpVerticalPos(lineHeight);
31✔
310

31✔
311
  let lineStartY;
31✔
312
  let totalOffset = textDims.height - 10;
31✔
313
  const textWidth = textDims.width;
31✔
314

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

31✔
339
  return lineStartY;
31✔
340
}
31✔
341

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

31✔
368
  drawText(diagram, textObj);
31✔
369

31✔
370
  const textWidth = textDims.width;
31✔
371

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

31✔
427
  let url = '';
31✔
428
  if (conf.arrowMarkerAbsolute) {
31!
429
    url =
×
430
      window.location.protocol +
×
431
      '//' +
×
432
      window.location.host +
×
433
      window.location.pathname +
×
434
      window.location.search;
×
435
    url = url.replace(/\(/g, '\\(');
×
436
    url = url.replace(/\)/g, '\\)');
×
437
  }
×
438

31✔
439
  line.attr('stroke-width', 2);
31✔
440
  line.attr('stroke', 'none'); // handled by theme/css anyway
31✔
441
  line.style('fill', 'none'); // remove any fill colour
31✔
442
  if (type === diagObj.db.LINETYPE.SOLID || type === diagObj.db.LINETYPE.DOTTED) {
31✔
443
    line.attr('marker-end', 'url(' + url + '#arrowhead)');
8✔
444
  }
8✔
445
  if (type === diagObj.db.LINETYPE.SOLID_POINT || type === diagObj.db.LINETYPE.DOTTED_POINT) {
31!
446
    line.attr('marker-end', 'url(' + url + '#filled-head)');
×
447
  }
×
448

31✔
449
  if (type === diagObj.db.LINETYPE.SOLID_CROSS || type === diagObj.db.LINETYPE.DOTTED_CROSS) {
31!
450
    line.attr('marker-end', 'url(' + url + '#crosshead)');
×
451
  }
×
452

31✔
453
  // add node number
31✔
454
  if (sequenceVisible || conf.showSequenceNumbers) {
31✔
455
    line.attr('marker-start', 'url(' + url + '#sequencenumber)');
4✔
456
    diagram
4✔
457
      .append('text')
4✔
458
      .attr('x', startx)
4✔
459
      .attr('y', lineStartY + 4)
4✔
460
      .attr('font-family', 'sans-serif')
4✔
461
      .attr('font-size', '12px')
4✔
462
      .attr('text-anchor', 'middle')
4✔
463
      .attr('class', 'sequenceNumber')
4✔
464
      .text(sequenceIndex);
4✔
465
  }
4✔
466
};
31✔
467

1✔
468
export const drawActors = function (
1✔
469
  diagram,
41✔
470
  actors,
41✔
471
  actorKeys,
41✔
472
  verticalPos,
41✔
473
  configuration,
41✔
474
  messages,
41✔
475
  isFooter
41✔
476
) {
41✔
477
  if (configuration.hideUnusedParticipants === true) {
41!
478
    const newActors = new Set();
×
479
    messages.forEach((message) => {
×
480
      newActors.add(message.from);
×
481
      newActors.add(message.to);
×
482
    });
×
483
    actorKeys = actorKeys.filter((actorKey) => newActors.has(actorKey));
×
484
  }
×
485

41✔
486
  // Draw the actors
41✔
487
  let prevWidth = 0;
41✔
488
  let prevMargin = 0;
41✔
489
  let maxHeight = 0;
41✔
490
  let prevBox = undefined;
41✔
491

41✔
492
  for (const actorKey of actorKeys) {
41✔
493
    const actor = actors[actorKey];
63✔
494
    const box = actor.box;
63✔
495

63✔
496
    // end of box
63✔
497
    if (prevBox && prevBox != box) {
63!
498
      if (!isFooter) {
×
499
        bounds.models.addBox(prevBox);
×
500
      }
×
501
      prevMargin += conf.boxMargin + prevBox.margin;
×
502
    }
×
503

63✔
504
    // new box
63✔
505
    if (box && box != prevBox) {
63✔
506
      if (!isFooter) {
1✔
507
        box.x = prevWidth + prevMargin;
1✔
508
        box.y = verticalPos;
1✔
509
      }
1✔
510
      prevMargin += box.margin;
1✔
511
    }
1✔
512

63✔
513
    // Add some rendering data to the object
63✔
514
    actor.width = actor.width || conf.width;
63!
515
    actor.height = Math.max(actor.height || conf.height, conf.height);
63!
516
    actor.margin = actor.margin || conf.actorMargin;
63✔
517

63✔
518
    actor.x = prevWidth + prevMargin;
63✔
519
    actor.y = bounds.getVerticalPos();
63✔
520

63✔
521
    // Draw the box with the attached line
63✔
522
    const height = svgDraw.drawActor(diagram, actor, conf, isFooter);
63✔
523
    maxHeight = Math.max(maxHeight, height);
63✔
524
    bounds.insert(actor.x, verticalPos, actor.x + actor.width, actor.height);
63✔
525

63✔
526
    prevWidth += actor.width + prevMargin;
63✔
527
    if (actor.box) {
63✔
528
      actor.box.width = prevWidth + box.margin - actor.box.x;
2✔
529
    }
2✔
530
    prevMargin = actor.margin;
63✔
531
    prevBox = actor.box;
63✔
532
    bounds.models.addActor(actor);
63✔
533
  }
63✔
534

41✔
535
  // end of box
41✔
536
  if (prevBox && !isFooter) {
41✔
537
    bounds.models.addBox(prevBox);
1✔
538
  }
1✔
539

41✔
540
  // Add a margin between the actor boxes and the first arrow
41✔
541
  bounds.bumpVerticalPos(maxHeight);
41✔
542
};
1✔
543

1✔
544
export const drawActorsPopup = function (diagram, actors, actorKeys, doc) {
1✔
545
  let maxHeight = 0;
31✔
546
  let maxWidth = 0;
31✔
547
  for (const actorKey of actorKeys) {
31✔
548
    const actor = actors[actorKey];
49✔
549
    const minMenuWidth = getRequiredPopupWidth(actor);
49✔
550
    const menuDimensions = svgDraw.drawPopup(
49✔
551
      diagram,
49✔
552
      actor,
49✔
553
      minMenuWidth,
49✔
554
      conf,
49✔
555
      conf.forceMenus,
49✔
556
      doc
49✔
557
    );
49✔
558
    if (menuDimensions.height > maxHeight) {
49!
559
      maxHeight = menuDimensions.height;
×
560
    }
×
561
    if (menuDimensions.width + actor.x > maxWidth) {
49✔
562
      maxWidth = menuDimensions.width + actor.x;
19✔
563
    }
19✔
564
  }
49✔
565

31✔
566
  return { maxHeight: maxHeight, maxWidth: maxWidth };
31✔
567
};
1✔
568

1✔
569
export const setConf = function (cnf) {
1✔
570
  assignWithDepth(conf, cnf);
232✔
571

232✔
572
  if (cnf.fontFamily) {
232✔
573
    conf.actorFontFamily = conf.noteFontFamily = conf.messageFontFamily = cnf.fontFamily;
53✔
574
  }
53✔
575
  if (cnf.fontSize) {
232✔
576
    conf.actorFontSize = conf.noteFontSize = conf.messageFontSize = cnf.fontSize;
53✔
577
  }
53✔
578
  if (cnf.fontWeight) {
232!
579
    conf.actorFontWeight = conf.noteFontWeight = conf.messageFontWeight = cnf.fontWeight;
×
580
  }
×
581
};
1✔
582

1✔
583
const actorActivations = function (actor) {
1✔
584
  return bounds.activations.filter(function (activation) {
62✔
585
    return activation.actor === actor;
×
586
  });
62✔
587
};
62✔
588

1✔
589
const activationBounds = function (actor, actors) {
1✔
590
  // handle multiple stacked activations for same actor
62✔
591
  const actorObj = actors[actor];
62✔
592
  const activations = actorActivations(actor);
62✔
593

62✔
594
  const left = activations.reduce(function (acc, activation) {
62✔
595
    return Math.min(acc, activation.startx);
×
596
  }, actorObj.x + actorObj.width / 2);
62✔
597
  const right = activations.reduce(function (acc, activation) {
62✔
598
    return Math.max(acc, activation.stopx);
×
599
  }, actorObj.x + actorObj.width / 2);
62✔
600
  return [left, right];
62✔
601
};
62✔
602

1✔
603
function adjustLoopHeightForWrap(loopWidths, msg, preMargin, postMargin, addLoopFn) {
2✔
604
  bounds.bumpVerticalPos(preMargin);
2✔
605
  let heightAdjust = postMargin;
2✔
606
  if (msg.id && msg.message && loopWidths[msg.id]) {
2✔
607
    const loopWidth = loopWidths[msg.id].width;
1✔
608
    const textConf = messageFont(conf);
1✔
609
    msg.message = utils.wrapLabel(`[${msg.message}]`, loopWidth - 2 * conf.wrapPadding, textConf);
1✔
610
    msg.width = loopWidth;
1✔
611
    msg.wrap = true;
1✔
612

1✔
613
    // const lines = common.splitBreaks(msg.message).length;
1✔
614
    const textDims = utils.calculateTextDimensions(msg.message, textConf);
1✔
615
    const totalOffset = Math.max(textDims.height, conf.labelBoxHeight);
1✔
616
    heightAdjust = postMargin + totalOffset;
1✔
617
    log.debug(`${totalOffset} - ${msg.message}`);
1✔
618
  }
1✔
619
  addLoopFn(msg);
2✔
620
  bounds.bumpVerticalPos(heightAdjust);
2✔
621
}
2✔
622

1✔
623
/**
1✔
624
 * Draws a sequenceDiagram in the tag with id: id based on the graph definition in text.
1✔
625
 *
1✔
626
 * @param _text - The text of the diagram
1✔
627
 * @param id - The id of the diagram which will be used as a DOM element id¨
1✔
628
 * @param _version - Mermaid version from package.json
1✔
629
 * @param diagObj - A standard diagram containing the db and the text and type etc of the diagram
1✔
630
 */
1✔
631
export const draw = function (_text: string, id: string, _version: string, diagObj: Diagram) {
1✔
632
  const { securityLevel, sequence } = configApi.getConfig();
31✔
633
  conf = sequence;
31✔
634
  // Handle root and Document for when rendering in sandbox mode
31✔
635
  let sandboxElement;
31✔
636
  if (securityLevel === 'sandbox') {
31!
637
    sandboxElement = select('#i' + id);
×
638
  }
×
639

31✔
640
  const root =
31✔
641
    securityLevel === 'sandbox'
31!
642
      ? select(sandboxElement.nodes()[0].contentDocument.body)
×
643
      : select('body');
31✔
644
  const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document;
31!
645
  bounds.init();
31✔
646
  log.debug(diagObj.db);
31✔
647

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

31✔
651
  // Fetch data from the parsing
31✔
652
  const actors = diagObj.db.getActors();
31✔
653
  const boxes = diagObj.db.getBoxes();
31✔
654
  const actorKeys = diagObj.db.getActorKeys();
31✔
655
  const messages = diagObj.db.getMessages();
31✔
656
  const title = diagObj.db.getDiagramTitle();
31✔
657
  const hasBoxes = diagObj.db.hasAtLeastOneBox();
31✔
658
  const hasBoxTitles = diagObj.db.hasAtLeastOneBoxWithTitle();
31✔
659
  const maxMessageWidthPerActor = getMaxMessageWidthPerActor(actors, messages, diagObj);
31✔
660
  conf.height = calculateActorMargins(actors, maxMessageWidthPerActor, boxes);
31✔
661

31✔
662
  svgDraw.insertComputerIcon(diagram);
31✔
663
  svgDraw.insertDatabaseIcon(diagram);
31✔
664
  svgDraw.insertClockIcon(diagram);
31✔
665

31✔
666
  if (hasBoxes) {
31✔
667
    bounds.bumpVerticalPos(conf.boxMargin);
1✔
668
    if (hasBoxTitles) {
1✔
669
      bounds.bumpVerticalPos(boxes[0].textMaxHeight);
1✔
670
    }
1✔
671
  }
1✔
672

31✔
673
  drawActors(diagram, actors, actorKeys, 0, conf, messages, false);
31✔
674
  const loopWidths = calculateLoopBounds(messages, actors, maxMessageWidthPerActor, diagObj);
31✔
675

31✔
676
  // The arrow head definition is attached to the svg once
31✔
677
  svgDraw.insertArrowHead(diagram);
31✔
678
  svgDraw.insertArrowCrossHead(diagram);
31✔
679
  svgDraw.insertArrowFilledHead(diagram);
31✔
680
  svgDraw.insertSequenceNumber(diagram);
31✔
681

31✔
682
  /**
31✔
683
   * @param msg - The message to draw.
31✔
684
   * @param verticalPos - The vertical position of the message.
31✔
685
   */
31✔
686
  function activeEnd(msg: any, verticalPos: number) {
31✔
687
    const activationData = bounds.endActivation(msg);
×
688
    if (activationData.starty + 18 > verticalPos) {
×
689
      activationData.starty = verticalPos - 6;
×
690
      verticalPos += 12;
×
691
    }
×
692
    svgDraw.drawActivation(
×
693
      diagram,
×
694
      activationData,
×
695
      verticalPos,
×
696
      conf,
×
697
      actorActivations(msg.from.actor).length
×
698
    );
×
699

×
700
    bounds.insert(activationData.startx, verticalPos - 10, activationData.stopx, verticalPos);
×
701
  }
×
702

31✔
703
  // Draw the messages/signals
31✔
704
  let sequenceIndex = 1;
31✔
705
  let sequenceIndexStep = 1;
31✔
706
  const messagesToDraw = [];
31✔
707
  messages.forEach(function (msg) {
31✔
708
    let loopModel, noteModel, msgModel;
52✔
709

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

52✔
873
    // Increment sequence counter if msg.type is a line (and not another event like activation or note, etc)
52✔
874
    if (
52✔
875
      [
52✔
876
        diagObj.db.LINETYPE.SOLID_OPEN,
52✔
877
        diagObj.db.LINETYPE.DOTTED_OPEN,
52✔
878
        diagObj.db.LINETYPE.SOLID,
52✔
879
        diagObj.db.LINETYPE.DOTTED,
52✔
880
        diagObj.db.LINETYPE.SOLID_CROSS,
52✔
881
        diagObj.db.LINETYPE.DOTTED_CROSS,
52✔
882
        diagObj.db.LINETYPE.SOLID_POINT,
52✔
883
        diagObj.db.LINETYPE.DOTTED_POINT,
52✔
884
      ].includes(msg.type)
52✔
885
    ) {
52✔
886
      sequenceIndex = sequenceIndex + sequenceIndexStep;
31✔
887
    }
31✔
888
  });
31✔
889

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

31✔
892
  if (conf.mirrorActors) {
31✔
893
    // Draw actors below diagram
10✔
894
    bounds.bumpVerticalPos(conf.boxMargin * 2);
10✔
895
    drawActors(diagram, actors, actorKeys, bounds.getVerticalPos(), conf, messages, true);
10✔
896
    bounds.bumpVerticalPos(conf.boxMargin);
10✔
897
    fixLifeLineHeights(diagram, bounds.getVerticalPos());
10✔
898
  }
10✔
899

31✔
900
  bounds.models.boxes.forEach(function (box) {
31✔
901
    box.height = bounds.getVerticalPos() - box.y;
1✔
902
    bounds.insert(box.x, box.y, box.x + box.width, box.height);
1✔
903
    box.startx = box.x;
1✔
904
    box.starty = box.y;
1✔
905
    box.stopx = box.startx + box.width;
1✔
906
    box.stopy = box.starty + box.height;
1✔
907
    box.stroke = 'rgb(0,0,0, 0.5)';
1✔
908
    svgDraw.drawBox(diagram, box, conf);
1✔
909
  });
31✔
910

31✔
911
  if (hasBoxes) {
31✔
912
    bounds.bumpVerticalPos(conf.boxMargin);
1✔
913
  }
1✔
914

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

31✔
918
  const { bounds: box } = bounds.getBounds();
31✔
919

31✔
920
  // Adjust line height of actor lines now that the height of the diagram is known
31✔
921
  log.debug('For line height fix Querying: #' + id + ' .actor-line');
31✔
922
  const actorLines = selectAll('#' + id + ' .actor-line');
31✔
923
  actorLines.attr('y2', box.stopy);
31✔
924

31✔
925
  // Make sure the height of the diagram supports long menus.
31✔
926
  let boxHeight = box.stopy - box.starty;
31✔
927
  if (boxHeight < requiredBoxSize.maxHeight) {
31!
928
    boxHeight = requiredBoxSize.maxHeight;
×
929
  }
×
930

31✔
931
  let height = boxHeight + 2 * conf.diagramMarginY;
31✔
932
  if (conf.mirrorActors) {
31✔
933
    height = height - conf.boxMargin + conf.bottomMarginAdj;
10✔
934
  }
10✔
935

31✔
936
  // Make sure the width of the diagram supports wide menus.
31✔
937
  let boxWidth = box.stopx - box.startx;
31✔
938
  if (boxWidth < requiredBoxSize.maxWidth) {
31!
939
    boxWidth = requiredBoxSize.maxWidth;
×
940
  }
×
941
  const width = boxWidth + 2 * conf.diagramMarginX;
31✔
942

31✔
943
  if (title) {
31!
944
    diagram
×
945
      .append('text')
×
946
      .text(title)
×
947
      .attr('x', (box.stopx - box.startx) / 2 - 2 * conf.diagramMarginX)
×
948
      .attr('y', -25);
×
949
  }
×
950

31✔
951
  configureSvgSize(diagram, height, width, conf.useMaxWidth);
31✔
952

31✔
953
  const extraVertForTitle = title ? 40 : 0;
31!
954
  diagram.attr(
31✔
955
    'viewBox',
31✔
956
    box.startx -
31✔
957
      conf.diagramMarginX +
31✔
958
      ' -' +
31✔
959
      (conf.diagramMarginY + extraVertForTitle) +
31✔
960
      ' ' +
31✔
961
      width +
31✔
962
      ' ' +
31✔
963
      (height + extraVertForTitle)
31✔
964
  );
31✔
965

31✔
966
  log.debug(`models:`, bounds.models);
31✔
967
};
1✔
968

1✔
969
/**
1✔
970
 * Retrieves the max message width of each actor, supports signals (messages, loops) and notes.
1✔
971
 *
1✔
972
 * It will enumerate each given message, and will determine its text width, in relation to the actor
1✔
973
 * it originates from, and destined to.
1✔
974
 *
1✔
975
 * @param actors - The actors map
1✔
976
 * @param messages - A list of message objects to iterate
1✔
977
 * @param diagObj - The diagram object.
1✔
978
 * @returns The max message width of each actor.
1✔
979
 */
1✔
980
function getMaxMessageWidthPerActor(
31✔
981
  actors: { [id: string]: any },
31✔
982
  messages: any[],
31✔
983
  diagObj: Diagram
31✔
984
): { [id: string]: number } {
31✔
985
  const maxMessageWidthPerActor = {};
31✔
986

31✔
987
  messages.forEach(function (msg) {
31✔
988
    if (actors[msg.to] && actors[msg.from]) {
52✔
989
      const actor = actors[msg.to];
46✔
990

46✔
991
      // If this is the first actor, and the message is left of it, no need to calculate the margin
46✔
992
      if (msg.placement === diagObj.db.PLACEMENT.LEFTOF && !actor.prevActor) {
46!
993
        return;
×
994
      }
×
995

46✔
996
      // If this is the last actor, and the message is right of it, no need to calculate the margin
46✔
997
      if (msg.placement === diagObj.db.PLACEMENT.RIGHTOF && !actor.nextActor) {
46✔
998
        return;
5✔
999
      }
5✔
1000

41✔
1001
      const isNote = msg.placement !== undefined;
41✔
1002
      const isMessage = !isNote;
41✔
1003

41✔
1004
      const textFont = isNote ? noteFont(conf) : messageFont(conf);
46✔
1005
      const wrappedMessage = msg.wrap
46✔
1006
        ? utils.wrapLabel(msg.message, conf.width - 2 * conf.wrapPadding, textFont)
11✔
1007
        : msg.message;
30✔
1008
      const messageDimensions = utils.calculateTextDimensions(wrappedMessage, textFont);
46✔
1009
      const messageWidth = messageDimensions.width + 2 * conf.wrapPadding;
46✔
1010

46✔
1011
      /*
46✔
1012
       * The following scenarios should be supported:
46✔
1013
       *
46✔
1014
       * - There's a message (non-note) between fromActor and toActor
46✔
1015
       *   - If fromActor is on the right and toActor is on the left, we should
46✔
1016
       *     define the toActor's margin
46✔
1017
       *   - If fromActor is on the left and toActor is on the right, we should
46✔
1018
       *     define the fromActor's margin
46✔
1019
       * - There's a note, in which case fromActor == toActor
46✔
1020
       *   - If the note is to the left of the actor, we should define the previous actor
46✔
1021
       *     margin
46✔
1022
       *   - If the note is on the actor, we should define both the previous and next actor
46✔
1023
       *     margins, each being the half of the note size
46✔
1024
       *   - If the note is on the right of the actor, we should define the current actor
46✔
1025
       *     margin
46✔
1026
       */
46✔
1027
      if (isMessage && msg.from === actor.nextActor) {
46✔
1028
        maxMessageWidthPerActor[msg.to] = Math.max(
13✔
1029
          maxMessageWidthPerActor[msg.to] || 0,
13!
1030
          messageWidth
13✔
1031
        );
13✔
1032
      } else if (isMessage && msg.from === actor.prevActor) {
46✔
1033
        maxMessageWidthPerActor[msg.from] = Math.max(
18✔
1034
          maxMessageWidthPerActor[msg.from] || 0,
18✔
1035
          messageWidth
18✔
1036
        );
18✔
1037
      } else if (isMessage && msg.from === msg.to) {
28!
1038
        maxMessageWidthPerActor[msg.from] = Math.max(
×
1039
          maxMessageWidthPerActor[msg.from] || 0,
×
1040
          messageWidth / 2
×
1041
        );
×
1042

×
1043
        maxMessageWidthPerActor[msg.to] = Math.max(
×
1044
          maxMessageWidthPerActor[msg.to] || 0,
×
1045
          messageWidth / 2
×
1046
        );
×
1047
      } else if (msg.placement === diagObj.db.PLACEMENT.RIGHTOF) {
10✔
1048
        maxMessageWidthPerActor[msg.from] = Math.max(
1✔
1049
          maxMessageWidthPerActor[msg.from] || 0,
1✔
1050
          messageWidth
1✔
1051
        );
1✔
1052
      } else if (msg.placement === diagObj.db.PLACEMENT.LEFTOF) {
10✔
1053
        maxMessageWidthPerActor[actor.prevActor] = Math.max(
6✔
1054
          maxMessageWidthPerActor[actor.prevActor] || 0,
6✔
1055
          messageWidth
6✔
1056
        );
6✔
1057
      } else if (msg.placement === diagObj.db.PLACEMENT.OVER) {
9✔
1058
        if (actor.prevActor) {
3✔
1059
          maxMessageWidthPerActor[actor.prevActor] = Math.max(
3✔
1060
            maxMessageWidthPerActor[actor.prevActor] || 0,
3✔
1061
            messageWidth / 2
3✔
1062
          );
3✔
1063
        }
3✔
1064

3✔
1065
        if (actor.nextActor) {
3✔
1066
          maxMessageWidthPerActor[msg.from] = Math.max(
2✔
1067
            maxMessageWidthPerActor[msg.from] || 0,
2!
1068
            messageWidth / 2
2✔
1069
          );
2✔
1070
        }
2✔
1071
      }
3✔
1072
    }
46✔
1073
  });
31✔
1074

31✔
1075
  log.debug('maxMessageWidthPerActor:', maxMessageWidthPerActor);
31✔
1076
  return maxMessageWidthPerActor;
31✔
1077
}
31✔
1078

1✔
1079
const getRequiredPopupWidth = function (actor) {
1✔
1080
  let requiredPopupWidth = 0;
49✔
1081
  const textFont = actorFont(conf);
49✔
1082
  for (const key in actor.links) {
49!
1083
    const labelDimensions = utils.calculateTextDimensions(key, textFont);
×
1084
    const labelWidth = labelDimensions.width + 2 * conf.wrapPadding + 2 * conf.boxMargin;
×
1085
    if (requiredPopupWidth < labelWidth) {
×
1086
      requiredPopupWidth = labelWidth;
×
1087
    }
×
1088
  }
×
1089

49✔
1090
  return requiredPopupWidth;
49✔
1091
};
49✔
1092

1✔
1093
/**
1✔
1094
 * This will calculate the optimal margin for each given actor,
1✔
1095
 * for a given actor → messageWidth map.
1✔
1096
 *
1✔
1097
 * An actor's margin is determined by the width of the actor, the width of the largest message that
1✔
1098
 * originates from it, and the configured conf.actorMargin.
1✔
1099
 *
1✔
1100
 * @param actors - The actors map to calculate margins for
1✔
1101
 * @param actorToMessageWidth - A map of actor key → max message width it holds
1✔
1102
 * @param boxes - The boxes around the actors if any
1✔
1103
 */
1✔
1104
function calculateActorMargins(
31✔
1105
  actors: { [id: string]: any },
31✔
1106
  actorToMessageWidth: ReturnType<typeof getMaxMessageWidthPerActor>,
31✔
1107
  boxes
31✔
1108
) {
31✔
1109
  let maxHeight = 0;
31✔
1110
  Object.keys(actors).forEach((prop) => {
31✔
1111
    const actor = actors[prop];
49✔
1112
    if (actor.wrap) {
49!
1113
      actor.description = utils.wrapLabel(
×
1114
        actor.description,
×
1115
        conf.width - 2 * conf.wrapPadding,
×
1116
        actorFont(conf)
×
1117
      );
×
1118
    }
×
1119
    const actDims = utils.calculateTextDimensions(actor.description, actorFont(conf));
49✔
1120
    actor.width = actor.wrap
49!
1121
      ? conf.width
×
1122
      : Math.max(conf.width, actDims.width + 2 * conf.wrapPadding);
49✔
1123

49✔
1124
    actor.height = actor.wrap ? Math.max(actDims.height, conf.height) : conf.height;
49!
1125
    maxHeight = Math.max(maxHeight, actor.height);
49✔
1126
  });
31✔
1127

31✔
1128
  for (const actorKey in actorToMessageWidth) {
31✔
1129
    const actor = actors[actorKey];
27✔
1130

27✔
1131
    if (!actor) {
27!
1132
      continue;
×
1133
    }
×
1134

27✔
1135
    const nextActor = actors[actor.nextActor];
27✔
1136

27✔
1137
    // No need to space out an actor that doesn't have a next link
27✔
1138
    if (!nextActor) {
27✔
1139
      const messageWidth = actorToMessageWidth[actorKey];
6✔
1140
      const actorWidth = messageWidth + conf.actorMargin - actor.width / 2;
6✔
1141
      actor.margin = Math.max(actorWidth, conf.actorMargin);
6✔
1142
      continue;
6✔
1143
    }
6✔
1144

21✔
1145
    const messageWidth = actorToMessageWidth[actorKey];
21✔
1146
    const actorWidth = messageWidth + conf.actorMargin - actor.width / 2 - nextActor.width / 2;
21✔
1147

21✔
1148
    actor.margin = Math.max(actorWidth, conf.actorMargin);
21✔
1149
  }
21✔
1150

31✔
1151
  let maxBoxHeight = 0;
31✔
1152
  boxes.forEach((box) => {
31✔
1153
    const textFont = messageFont(conf);
1✔
1154
    let totalWidth = box.actorKeys.reduce((total, aKey) => {
1✔
1155
      return (total += actors[aKey].width + (actors[aKey].margin || 0));
2✔
1156
    }, 0);
1✔
1157

1✔
1158
    totalWidth -= 2 * conf.boxTextMargin;
1✔
1159
    if (box.wrap) {
1!
1160
      box.name = utils.wrapLabel(box.name, totalWidth - 2 * conf.wrapPadding, textFont);
×
1161
    }
×
1162

1✔
1163
    const boxMsgDimensions = utils.calculateTextDimensions(box.name, textFont);
1✔
1164
    maxBoxHeight = Math.max(boxMsgDimensions.height, maxBoxHeight);
1✔
1165
    const minWidth = Math.max(totalWidth, boxMsgDimensions.width + 2 * conf.wrapPadding);
1✔
1166
    box.margin = conf.boxTextMargin;
1✔
1167
    if (totalWidth < minWidth) {
1!
1168
      const missing = (minWidth - totalWidth) / 2;
×
1169
      box.margin += missing;
×
1170
    }
×
1171
  });
31✔
1172
  boxes.forEach((box) => (box.textMaxHeight = maxBoxHeight));
31✔
1173

31✔
1174
  return Math.max(maxHeight, conf.height);
31✔
1175
}
31✔
1176

1✔
1177
const buildNoteModel = function (msg, actors, diagObj) {
1✔
1178
  const startx = actors[msg.from].x;
15✔
1179
  const stopx = actors[msg.to].x;
15✔
1180
  const shouldWrap = msg.wrap && msg.message;
15✔
1181

15✔
1182
  let textDimensions = utils.calculateTextDimensions(
15✔
1183
    shouldWrap ? utils.wrapLabel(msg.message, conf.width, noteFont(conf)) : msg.message,
15✔
1184
    noteFont(conf)
15✔
1185
  );
15✔
1186
  const noteModel = {
15✔
1187
    width: shouldWrap
15✔
1188
      ? conf.width
3✔
1189
      : Math.max(conf.width, textDimensions.width + 2 * conf.noteMargin),
12✔
1190
    height: 0,
15✔
1191
    startx: actors[msg.from].x,
15✔
1192
    stopx: 0,
15✔
1193
    starty: 0,
15✔
1194
    stopy: 0,
15✔
1195
    message: msg.message,
15✔
1196
  };
15✔
1197
  if (msg.placement === diagObj.db.PLACEMENT.RIGHTOF) {
15✔
1198
    noteModel.width = shouldWrap
6!
1199
      ? Math.max(conf.width, textDimensions.width)
×
1200
      : Math.max(
6✔
1201
          actors[msg.from].width / 2 + actors[msg.to].width / 2,
6✔
1202
          textDimensions.width + 2 * conf.noteMargin
6✔
1203
        );
6✔
1204
    noteModel.startx = startx + (actors[msg.from].width + conf.actorMargin) / 2;
6✔
1205
  } else if (msg.placement === diagObj.db.PLACEMENT.LEFTOF) {
15✔
1206
    noteModel.width = shouldWrap
6✔
1207
      ? Math.max(conf.width, textDimensions.width + 2 * conf.noteMargin)
3✔
1208
      : Math.max(
3✔
1209
          actors[msg.from].width / 2 + actors[msg.to].width / 2,
3✔
1210
          textDimensions.width + 2 * conf.noteMargin
3✔
1211
        );
3✔
1212
    noteModel.startx = startx - noteModel.width + (actors[msg.from].width - conf.actorMargin) / 2;
6✔
1213
  } else if (msg.to === msg.from) {
9✔
1214
    textDimensions = utils.calculateTextDimensions(
1✔
1215
      shouldWrap
1!
1216
        ? utils.wrapLabel(msg.message, Math.max(conf.width, actors[msg.from].width), noteFont(conf))
×
1217
        : msg.message,
1✔
1218
      noteFont(conf)
1✔
1219
    );
1✔
1220
    noteModel.width = shouldWrap
1!
1221
      ? Math.max(conf.width, actors[msg.from].width)
×
1222
      : Math.max(actors[msg.from].width, conf.width, textDimensions.width + 2 * conf.noteMargin);
1✔
1223
    noteModel.startx = startx + (actors[msg.from].width - noteModel.width) / 2;
1✔
1224
  } else {
3✔
1225
    noteModel.width =
2✔
1226
      Math.abs(startx + actors[msg.from].width / 2 - (stopx + actors[msg.to].width / 2)) +
2✔
1227
      conf.actorMargin;
2✔
1228
    noteModel.startx =
2✔
1229
      startx < stopx
2✔
1230
        ? startx + actors[msg.from].width / 2 - conf.actorMargin / 2
1✔
1231
        : stopx + actors[msg.to].width / 2 - conf.actorMargin / 2;
1✔
1232
  }
2✔
1233
  if (shouldWrap) {
15✔
1234
    noteModel.message = utils.wrapLabel(
3✔
1235
      msg.message,
3✔
1236
      noteModel.width - 2 * conf.wrapPadding,
3✔
1237
      noteFont(conf)
3✔
1238
    );
3✔
1239
  }
3✔
1240
  log.debug(
15✔
1241
    `NM:[${noteModel.startx},${noteModel.stopx},${noteModel.starty},${noteModel.stopy}:${noteModel.width},${noteModel.height}=${msg.message}]`
15✔
1242
  );
15✔
1243
  return noteModel;
15✔
1244
};
15✔
1245

1✔
1246
const buildMessageModel = function (msg, actors, diagObj) {
1✔
1247
  let process = false;
37✔
1248
  if (
37✔
1249
    [
37✔
1250
      diagObj.db.LINETYPE.SOLID_OPEN,
37✔
1251
      diagObj.db.LINETYPE.DOTTED_OPEN,
37✔
1252
      diagObj.db.LINETYPE.SOLID,
37✔
1253
      diagObj.db.LINETYPE.DOTTED,
37✔
1254
      diagObj.db.LINETYPE.SOLID_CROSS,
37✔
1255
      diagObj.db.LINETYPE.DOTTED_CROSS,
37✔
1256
      diagObj.db.LINETYPE.SOLID_POINT,
37✔
1257
      diagObj.db.LINETYPE.DOTTED_POINT,
37✔
1258
    ].includes(msg.type)
37✔
1259
  ) {
37✔
1260
    process = true;
31✔
1261
  }
31✔
1262
  if (!process) {
37✔
1263
    return {};
6✔
1264
  }
6✔
1265
  const fromBounds = activationBounds(msg.from, actors);
31✔
1266
  const toBounds = activationBounds(msg.to, actors);
31✔
1267
  const fromIdx = fromBounds[0] <= toBounds[0] ? 1 : 0;
37✔
1268
  const toIdx = fromBounds[0] < toBounds[0] ? 0 : 1;
37✔
1269
  const allBounds = [...fromBounds, ...toBounds];
37✔
1270
  const boundedWidth = Math.abs(toBounds[toIdx] - fromBounds[fromIdx]);
37✔
1271
  if (msg.wrap && msg.message) {
37✔
1272
    msg.message = utils.wrapLabel(
8✔
1273
      msg.message,
8✔
1274
      Math.max(boundedWidth + 2 * conf.wrapPadding, conf.width),
8✔
1275
      messageFont(conf)
8✔
1276
    );
8✔
1277
  }
8✔
1278
  const msgDims = utils.calculateTextDimensions(msg.message, messageFont(conf));
31✔
1279

31✔
1280
  return {
31✔
1281
    width: Math.max(
31✔
1282
      msg.wrap ? 0 : msgDims.width + 2 * conf.wrapPadding,
37✔
1283
      boundedWidth + 2 * conf.wrapPadding,
37✔
1284
      conf.width
37✔
1285
    ),
37✔
1286
    height: 0,
37✔
1287
    startx: fromBounds[fromIdx],
37✔
1288
    stopx: toBounds[toIdx],
37✔
1289
    starty: 0,
37✔
1290
    stopy: 0,
37✔
1291
    message: msg.message,
37✔
1292
    type: msg.type,
37✔
1293
    wrap: msg.wrap,
37✔
1294
    fromBounds: Math.min.apply(null, allBounds),
37✔
1295
    toBounds: Math.max.apply(null, allBounds),
37✔
1296
  };
37✔
1297
};
37✔
1298

1✔
1299
const calculateLoopBounds = function (messages, actors, _maxWidthPerActor, diagObj) {
1✔
1300
  const loops = {};
31✔
1301
  const stack = [];
31✔
1302
  let current, noteModel, msgModel;
31✔
1303

31✔
1304
  messages.forEach(function (msg) {
31✔
1305
    msg.id = utils.random({ length: 10 });
52✔
1306
    switch (msg.type) {
52✔
1307
      case diagObj.db.LINETYPE.LOOP_START:
52✔
1308
      case diagObj.db.LINETYPE.ALT_START:
52✔
1309
      case diagObj.db.LINETYPE.OPT_START:
52✔
1310
      case diagObj.db.LINETYPE.PAR_START:
52✔
1311
      case diagObj.db.LINETYPE.CRITICAL_START:
52✔
1312
      case diagObj.db.LINETYPE.BREAK_START:
52✔
1313
        stack.push({
1✔
1314
          id: msg.id,
1✔
1315
          msg: msg.message,
1✔
1316
          from: Number.MAX_SAFE_INTEGER,
1✔
1317
          to: Number.MIN_SAFE_INTEGER,
1✔
1318
          width: 0,
1✔
1319
        });
1✔
1320
        break;
1✔
1321
      case diagObj.db.LINETYPE.ALT_ELSE:
52!
1322
      case diagObj.db.LINETYPE.PAR_AND:
52!
1323
      case diagObj.db.LINETYPE.CRITICAL_OPTION:
52!
1324
        if (msg.message) {
×
1325
          current = stack.pop();
×
1326
          loops[current.id] = current;
×
1327
          loops[msg.id] = current;
×
1328
          stack.push(current);
×
1329
        }
×
1330
        break;
×
1331
      case diagObj.db.LINETYPE.LOOP_END:
52✔
1332
      case diagObj.db.LINETYPE.ALT_END:
52✔
1333
      case diagObj.db.LINETYPE.OPT_END:
52✔
1334
      case diagObj.db.LINETYPE.PAR_END:
52✔
1335
      case diagObj.db.LINETYPE.CRITICAL_END:
52✔
1336
      case diagObj.db.LINETYPE.BREAK_END:
52✔
1337
        current = stack.pop();
1✔
1338
        loops[current.id] = current;
1✔
1339
        break;
1✔
1340
      case diagObj.db.LINETYPE.ACTIVE_START:
52!
1341
        {
×
1342
          const actorRect = actors[msg.from ? msg.from.actor : msg.to.actor];
×
1343
          const stackedSize = actorActivations(msg.from ? msg.from.actor : msg.to.actor).length;
×
1344
          const x =
×
1345
            actorRect.x + actorRect.width / 2 + ((stackedSize - 1) * conf.activationWidth) / 2;
×
1346
          const toAdd = {
×
1347
            startx: x,
×
1348
            stopx: x + conf.activationWidth,
×
1349
            actor: msg.from.actor,
×
1350
            enabled: true,
×
1351
          };
×
1352
          bounds.activations.push(toAdd);
×
1353
        }
×
1354
        break;
×
1355
      case diagObj.db.LINETYPE.ACTIVE_END:
52!
1356
        {
×
1357
          const lastActorActivationIdx = bounds.activations
×
1358
            .map((a) => a.actor)
×
1359
            .lastIndexOf(msg.from.actor);
×
1360
          delete bounds.activations.splice(lastActorActivationIdx, 1)[0];
×
1361
        }
×
1362
        break;
×
1363
    }
52✔
1364
    const isNote = msg.placement !== undefined;
52✔
1365
    if (isNote) {
52✔
1366
      noteModel = buildNoteModel(msg, actors, diagObj);
15✔
1367
      msg.noteModel = noteModel;
15✔
1368
      stack.forEach((stk) => {
15✔
1369
        current = stk;
×
1370
        current.from = Math.min(current.from, noteModel.startx);
×
1371
        current.to = Math.max(current.to, noteModel.startx + noteModel.width);
×
1372
        current.width =
×
1373
          Math.max(current.width, Math.abs(current.from - current.to)) - conf.labelBoxWidth;
×
1374
      });
15✔
1375
    } else {
52✔
1376
      msgModel = buildMessageModel(msg, actors, diagObj);
37✔
1377
      msg.msgModel = msgModel;
37✔
1378
      if (msgModel.startx && msgModel.stopx && stack.length > 0) {
37✔
1379
        stack.forEach((stk) => {
1✔
1380
          current = stk;
1✔
1381
          if (msgModel.startx === msgModel.stopx) {
1!
1382
            const from = actors[msg.from];
×
1383
            const to = actors[msg.to];
×
1384
            current.from = Math.min(
×
1385
              from.x - msgModel.width / 2,
×
1386
              from.x - from.width / 2,
×
1387
              current.from
×
1388
            );
×
1389
            current.to = Math.max(to.x + msgModel.width / 2, to.x + from.width / 2, current.to);
×
1390
            current.width =
×
1391
              Math.max(current.width, Math.abs(current.to - current.from)) - conf.labelBoxWidth;
×
1392
          } else {
1✔
1393
            current.from = Math.min(msgModel.startx, current.from);
1✔
1394
            current.to = Math.max(msgModel.stopx, current.to);
1✔
1395
            current.width = Math.max(current.width, msgModel.width) - conf.labelBoxWidth;
1✔
1396
          }
1✔
1397
        });
1✔
1398
      }
1✔
1399
    }
37✔
1400
  });
31✔
1401
  bounds.activations = [];
31✔
1402
  log.debug('Loop type widths:', loops);
31✔
1403
  return loops;
31✔
1404
};
31✔
1405

1✔
1406
export default {
1✔
1407
  bounds,
1✔
1408
  drawActors,
1✔
1409
  drawActorsPopup,
1✔
1410
  setConf,
1✔
1411
  draw,
1✔
1412
};
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