• 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

31.98
/packages/mermaid/src/diagrams/class/svgDraw.js
1
import { line, curveBasis } from 'd3';
1✔
2
import utils from '../../utils.js';
1✔
3
import { log } from '../../logger.js';
1✔
4
import { parseGenericTypes } from '../common/common.js';
1✔
5

1✔
6
let edgeCount = 0;
1✔
7
export const drawEdge = function (elem, path, relation, conf, diagObj) {
1✔
8
  const getRelationType = function (type) {
×
9
    switch (type) {
×
10
      case diagObj.db.relationType.AGGREGATION:
×
11
        return 'aggregation';
×
12
      case diagObj.db.relationType.EXTENSION:
×
13
        return 'extension';
×
14
      case diagObj.db.relationType.COMPOSITION:
×
15
        return 'composition';
×
16
      case diagObj.db.relationType.DEPENDENCY:
×
17
        return 'dependency';
×
18
      case diagObj.db.relationType.LOLLIPOP:
×
19
        return 'lollipop';
×
20
    }
×
21
  };
×
22

×
23
  path.points = path.points.filter((p) => !Number.isNaN(p.y));
×
24

×
25
  // The data for our line
×
26
  const lineData = path.points;
×
27

×
28
  // This is the accessor function we talked about above
×
29
  const lineFunction = line()
×
30
    .x(function (d) {
×
31
      return d.x;
×
32
    })
×
33
    .y(function (d) {
×
34
      return d.y;
×
35
    })
×
36
    .curve(curveBasis);
×
37

×
38
  const svgPath = elem
×
39
    .append('path')
×
40
    .attr('d', lineFunction(lineData))
×
41
    .attr('id', 'edge' + edgeCount)
×
42
    .attr('class', 'relation');
×
43
  let url = '';
×
44
  if (conf.arrowMarkerAbsolute) {
×
45
    url =
×
46
      window.location.protocol +
×
47
      '//' +
×
48
      window.location.host +
×
49
      window.location.pathname +
×
50
      window.location.search;
×
51
    url = url.replace(/\(/g, '\\(');
×
52
    url = url.replace(/\)/g, '\\)');
×
53
  }
×
54

×
55
  if (relation.relation.lineType == 1) {
×
56
    svgPath.attr('class', 'relation dashed-line');
×
57
  }
×
58
  if (relation.relation.lineType == 10) {
×
59
    svgPath.attr('class', 'relation dotted-line');
×
60
  }
×
61
  if (relation.relation.type1 !== 'none') {
×
62
    svgPath.attr(
×
63
      'marker-start',
×
64
      'url(' + url + '#' + getRelationType(relation.relation.type1) + 'Start' + ')'
×
65
    );
×
66
  }
×
67
  if (relation.relation.type2 !== 'none') {
×
68
    svgPath.attr(
×
69
      'marker-end',
×
70
      'url(' + url + '#' + getRelationType(relation.relation.type2) + 'End' + ')'
×
71
    );
×
72
  }
×
73

×
74
  let x, y;
×
75
  const l = path.points.length;
×
76
  // Calculate Label position
×
77
  let labelPosition = utils.calcLabelPosition(path.points);
×
78
  x = labelPosition.x;
×
79
  y = labelPosition.y;
×
80

×
81
  let p1_card_x, p1_card_y;
×
82
  let p2_card_x, p2_card_y;
×
83

×
84
  if (l % 2 !== 0 && l > 1) {
×
85
    let cardinality_1_point = utils.calcCardinalityPosition(
×
86
      relation.relation.type1 !== 'none',
×
87
      path.points,
×
88
      path.points[0]
×
89
    );
×
90
    let cardinality_2_point = utils.calcCardinalityPosition(
×
91
      relation.relation.type2 !== 'none',
×
92
      path.points,
×
93
      path.points[l - 1]
×
94
    );
×
95

×
96
    log.debug('cardinality_1_point ' + JSON.stringify(cardinality_1_point));
×
97
    log.debug('cardinality_2_point ' + JSON.stringify(cardinality_2_point));
×
98

×
99
    p1_card_x = cardinality_1_point.x;
×
100
    p1_card_y = cardinality_1_point.y;
×
101
    p2_card_x = cardinality_2_point.x;
×
102
    p2_card_y = cardinality_2_point.y;
×
103
  }
×
104

×
105
  if (relation.title !== undefined) {
×
106
    const g = elem.append('g').attr('class', 'classLabel');
×
107
    const label = g
×
108
      .append('text')
×
109
      .attr('class', 'label')
×
110
      .attr('x', x)
×
111
      .attr('y', y)
×
112
      .attr('fill', 'red')
×
113
      .attr('text-anchor', 'middle')
×
114
      .text(relation.title);
×
115

×
116
    window.label = label;
×
117
    const bounds = label.node().getBBox();
×
118

×
119
    g.insert('rect', ':first-child')
×
120
      .attr('class', 'box')
×
121
      .attr('x', bounds.x - conf.padding / 2)
×
122
      .attr('y', bounds.y - conf.padding / 2)
×
123
      .attr('width', bounds.width + conf.padding)
×
124
      .attr('height', bounds.height + conf.padding);
×
125
  }
×
126

×
127
  log.info('Rendering relation ' + JSON.stringify(relation));
×
128
  if (relation.relationTitle1 !== undefined && relation.relationTitle1 !== 'none') {
×
129
    const g = elem.append('g').attr('class', 'cardinality');
×
130
    g.append('text')
×
131
      .attr('class', 'type1')
×
132
      .attr('x', p1_card_x)
×
133
      .attr('y', p1_card_y)
×
134
      .attr('fill', 'black')
×
135
      .attr('font-size', '6')
×
136
      .text(relation.relationTitle1);
×
137
  }
×
138
  if (relation.relationTitle2 !== undefined && relation.relationTitle2 !== 'none') {
×
139
    const g = elem.append('g').attr('class', 'cardinality');
×
140
    g.append('text')
×
141
      .attr('class', 'type2')
×
142
      .attr('x', p2_card_x)
×
143
      .attr('y', p2_card_y)
×
144
      .attr('fill', 'black')
×
145
      .attr('font-size', '6')
×
146
      .text(relation.relationTitle2);
×
147
  }
×
148

×
149
  edgeCount++;
×
150
};
×
151

1✔
152
/**
1✔
153
 * Renders a class diagram
1✔
154
 *
1✔
155
 * @param {SVGSVGElement} elem The element to draw it into
1✔
156
 * @param classDef
1✔
157
 * @param conf
1✔
158
 * @param diagObj
1✔
159
 * @todo Add more information in the JSDOC here
1✔
160
 */
1✔
161
export const drawClass = function (elem, classDef, conf, diagObj) {
1✔
162
  log.debug('Rendering class ', classDef, conf);
×
163

×
164
  const id = classDef.id;
×
165
  const classInfo = {
×
166
    id: id,
×
167
    label: classDef.id,
×
168
    width: 0,
×
169
    height: 0,
×
170
  };
×
171

×
172
  // add class group
×
173
  const g = elem.append('g').attr('id', diagObj.db.lookUpDomId(id)).attr('class', 'classGroup');
×
174

×
175
  // add title
×
176
  let title;
×
177
  if (classDef.link) {
×
178
    title = g
×
179
      .append('svg:a')
×
180
      .attr('xlink:href', classDef.link)
×
181
      .attr('target', classDef.linkTarget)
×
182
      .append('text')
×
183
      .attr('y', conf.textHeight + conf.padding)
×
184
      .attr('x', 0);
×
185
  } else {
×
186
    title = g
×
187
      .append('text')
×
188
      .attr('y', conf.textHeight + conf.padding)
×
189
      .attr('x', 0);
×
190
  }
×
191

×
192
  // add annotations
×
193
  let isFirst = true;
×
194
  classDef.annotations.forEach(function (member) {
×
195
    const titleText2 = title.append('tspan').text('«' + member + '»');
×
196
    if (!isFirst) {
×
197
      titleText2.attr('dy', conf.textHeight);
×
198
    }
×
199
    isFirst = false;
×
200
  });
×
201

×
202
  let classTitleString = getClassTitleString(classDef);
×
203

×
204
  const classTitle = title.append('tspan').text(classTitleString).attr('class', 'title');
×
205

×
206
  // If class has annotations the title needs to have an offset of the text height
×
207
  if (!isFirst) {
×
208
    classTitle.attr('dy', conf.textHeight);
×
209
  }
×
210

×
211
  const titleHeight = title.node().getBBox().height;
×
212

×
213
  const membersLine = g
×
214
    .append('line') // text label for the x axis
×
215
    .attr('x1', 0)
×
216
    .attr('y1', conf.padding + titleHeight + conf.dividerMargin / 2)
×
217
    .attr('y2', conf.padding + titleHeight + conf.dividerMargin / 2);
×
218

×
219
  const members = g
×
220
    .append('text') // text label for the x axis
×
221
    .attr('x', conf.padding)
×
222
    .attr('y', titleHeight + conf.dividerMargin + conf.textHeight)
×
223
    .attr('fill', 'white')
×
224
    .attr('class', 'classText');
×
225

×
226
  isFirst = true;
×
227
  classDef.members.forEach(function (member) {
×
228
    addTspan(members, member, isFirst, conf);
×
229
    isFirst = false;
×
230
  });
×
231

×
232
  const membersBox = members.node().getBBox();
×
233

×
234
  const methodsLine = g
×
235
    .append('line') // text label for the x axis
×
236
    .attr('x1', 0)
×
237
    .attr('y1', conf.padding + titleHeight + conf.dividerMargin + membersBox.height)
×
238
    .attr('y2', conf.padding + titleHeight + conf.dividerMargin + membersBox.height);
×
239

×
240
  const methods = g
×
241
    .append('text') // text label for the x axis
×
242
    .attr('x', conf.padding)
×
243
    .attr('y', titleHeight + 2 * conf.dividerMargin + membersBox.height + conf.textHeight)
×
244
    .attr('fill', 'white')
×
245
    .attr('class', 'classText');
×
246

×
247
  isFirst = true;
×
248

×
249
  classDef.methods.forEach(function (method) {
×
250
    addTspan(methods, method, isFirst, conf);
×
251
    isFirst = false;
×
252
  });
×
253

×
254
  const classBox = g.node().getBBox();
×
255
  var cssClassStr = ' ';
×
256

×
257
  if (classDef.cssClasses.length > 0) {
×
258
    cssClassStr = cssClassStr + classDef.cssClasses.join(' ');
×
259
  }
×
260

×
261
  const rect = g
×
262
    .insert('rect', ':first-child')
×
263
    .attr('x', 0)
×
264
    .attr('y', 0)
×
265
    .attr('width', classBox.width + 2 * conf.padding)
×
266
    .attr('height', classBox.height + conf.padding + 0.5 * conf.dividerMargin)
×
267
    .attr('class', cssClassStr);
×
268

×
269
  const rectWidth = rect.node().getBBox().width;
×
270

×
271
  // Center title
×
272
  // We subtract the width of each text element from the class box width and divide it by 2
×
273
  title.node().childNodes.forEach(function (x) {
×
274
    x.setAttribute('x', (rectWidth - x.getBBox().width) / 2);
×
275
  });
×
276

×
277
  if (classDef.tooltip) {
×
278
    title.insert('title').text(classDef.tooltip);
×
279
  }
×
280

×
281
  membersLine.attr('x2', rectWidth);
×
282
  methodsLine.attr('x2', rectWidth);
×
283

×
284
  classInfo.width = rectWidth;
×
285
  classInfo.height = classBox.height + conf.padding + 0.5 * conf.dividerMargin;
×
286

×
287
  return classInfo;
×
288
};
×
289

1✔
290
export const getClassTitleString = function (classDef) {
1✔
291
  let classTitleString = classDef.id;
1✔
292

1✔
293
  if (classDef.type) {
1✔
294
    classTitleString += '<' + classDef.type + '>';
1✔
295
  }
1✔
296

1✔
297
  return classTitleString;
1✔
298
};
1✔
299

1✔
300
/**
1✔
301
 * Renders a note diagram
1✔
302
 *
1✔
303
 * @param {SVGSVGElement} elem The element to draw it into
1✔
304
 * @param {{id: string; text: string; class: string;}} note
1✔
305
 * @param conf
1✔
306
 * @param diagObj
1✔
307
 * @todo Add more information in the JSDOC here
1✔
308
 */
1✔
309
export const drawNote = function (elem, note, conf, diagObj) {
1✔
310
  log.debug('Rendering note ', note, conf);
×
311

×
312
  const id = note.id;
×
313
  const noteInfo = {
×
314
    id: id,
×
315
    text: note.text,
×
316
    width: 0,
×
317
    height: 0,
×
318
  };
×
319

×
320
  // add class group
×
321
  const g = elem.append('g').attr('id', id).attr('class', 'classGroup');
×
322

×
323
  // add text
×
324
  let text = g
×
325
    .append('text')
×
326
    .attr('y', conf.textHeight + conf.padding)
×
327
    .attr('x', 0);
×
328

×
329
  const lines = JSON.parse(`"${note.text}"`).split('\n');
×
330

×
331
  lines.forEach(function (line) {
×
332
    log.debug(`Adding line: ${line}`);
×
333
    text.append('tspan').text(line).attr('class', 'title').attr('dy', conf.textHeight);
×
334
  });
×
335

×
336
  const noteBox = g.node().getBBox();
×
337

×
338
  const rect = g
×
339
    .insert('rect', ':first-child')
×
340
    .attr('x', 0)
×
341
    .attr('y', 0)
×
342
    .attr('width', noteBox.width + 2 * conf.padding)
×
343
    .attr(
×
344
      'height',
×
345
      noteBox.height + lines.length * conf.textHeight + conf.padding + 0.5 * conf.dividerMargin
×
346
    );
×
347

×
348
  const rectWidth = rect.node().getBBox().width;
×
349

×
350
  // Center title
×
351
  // We subtract the width of each text element from the class box width and divide it by 2
×
352
  text.node().childNodes.forEach(function (x) {
×
353
    x.setAttribute('x', (rectWidth - x.getBBox().width) / 2);
×
354
  });
×
355

×
356
  noteInfo.width = rectWidth;
×
357
  noteInfo.height =
×
358
    noteBox.height + lines.length * conf.textHeight + conf.padding + 0.5 * conf.dividerMargin;
×
359

×
360
  return noteInfo;
×
361
};
×
362

1✔
363
export const parseMember = function (text) {
1✔
364
  let displayText = '';
38✔
365
  let cssStyle = '';
38✔
366
  let returnType = '';
38✔
367

38✔
368
  let visibility = '';
38✔
369
  let firstChar = text.substring(0, 1);
38✔
370
  let lastChar = text.substring(text.length - 1, text.length);
38✔
371

38✔
372
  if (firstChar.match(/[#+~-]/)) {
38✔
373
    visibility = firstChar;
6✔
374
  }
6✔
375

38✔
376
  let noClassifierRe = /[\s\w)~]/;
38✔
377
  if (!lastChar.match(noClassifierRe)) {
38✔
378
    cssStyle = parseClassifier(lastChar);
11✔
379
  }
11✔
380

38✔
381
  const startIndex = visibility === '' ? 0 : 1;
38✔
382
  let endIndex = cssStyle === '' ? text.length : text.length - 1;
38✔
383
  text = text.substring(startIndex, endIndex);
38✔
384

38✔
385
  const methodStart = text.indexOf('(');
38✔
386
  const methodEnd = text.indexOf(')');
38✔
387
  const isMethod = methodStart > 1 && methodEnd > methodStart && methodEnd <= text.length;
38✔
388

38✔
389
  if (isMethod) {
38✔
390
    let methodName = text.substring(0, methodStart).trim();
25✔
391

25✔
392
    const parameters = text.substring(methodStart + 1, methodEnd);
25✔
393

25✔
394
    displayText = visibility + methodName + '(' + parseGenericTypes(parameters.trim()) + ')';
25✔
395

25✔
396
    if (methodEnd < text.length) {
25✔
397
      // special case: classifier after the closing parenthesis
25✔
398
      let potentialClassifier = text.substring(methodEnd + 1, methodEnd + 2);
25✔
399
      if (cssStyle === '' && !potentialClassifier.match(noClassifierRe)) {
25✔
400
        cssStyle = parseClassifier(potentialClassifier);
15✔
401
        returnType = text.substring(methodEnd + 2).trim();
15✔
402
      } else {
25✔
403
        returnType = text.substring(methodEnd + 1).trim();
10✔
404
      }
10✔
405

25✔
406
      if (returnType !== '') {
25✔
407
        if (returnType.charAt(0) === ':') {
11✔
408
          returnType = returnType.substring(1).trim();
3✔
409
        }
3✔
410
        returnType = ' : ' + parseGenericTypes(returnType);
11✔
411
        displayText += returnType;
11✔
412
      }
11✔
413
    }
25✔
414
  } else {
38✔
415
    // finally - if all else fails, just send the text back as written (other than parsing for generic types)
13✔
416
    displayText = visibility + parseGenericTypes(text);
13✔
417
  }
13✔
418

38✔
419
  return {
38✔
420
    displayText,
38✔
421
    cssStyle,
38✔
422
  };
38✔
423
};
38✔
424

1✔
425
/**
1✔
426
 * Adds a <tspan> for a member in a diagram
1✔
427
 *
1✔
428
 * @param {SVGElement} textEl The element to append to
1✔
429
 * @param {string} txt The member
1✔
430
 * @param {boolean} isFirst
1✔
431
 * @param {{ padding: string; textHeight: string }} conf The configuration for the member
1✔
432
 */
1✔
433
const addTspan = function (textEl, txt, isFirst, conf) {
1✔
434
  let member = parseMember(txt);
×
435

×
436
  const tSpan = textEl.append('tspan').attr('x', conf.padding).text(member.displayText);
×
437

×
438
  if (member.cssStyle !== '') {
×
439
    tSpan.attr('style', member.cssStyle);
×
440
  }
×
441

×
442
  if (!isFirst) {
×
443
    tSpan.attr('dy', conf.textHeight);
×
444
  }
×
445
};
×
446

1✔
447
/**
1✔
448
 * Gives the styles for a classifier
1✔
449
 *
1✔
450
 * @param {'+' | '-' | '#' | '~' | '*' | '$'} classifier The classifier string
1✔
451
 * @returns {string} Styling for the classifier
1✔
452
 */
1✔
453
const parseClassifier = function (classifier) {
1✔
454
  switch (classifier) {
26✔
455
    case '*':
26✔
456
      return 'font-style:italic;';
5✔
457
    case '$':
26✔
458
      return 'text-decoration:underline;';
8✔
459
    default:
26✔
460
      return '';
13✔
461
  }
26✔
462
};
26✔
463

1✔
464
export default {
1✔
465
  getClassTitleString,
1✔
466
  drawClass,
1✔
467
  drawEdge,
1✔
468
  drawNote,
1✔
469
  parseMember,
1✔
470
};
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