• 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

79.52
/packages/mermaid/src/diagrams/class/classDb.ts
1
// @ts-expect-error - d3 types issue
1✔
2
import { select, Selection } from 'd3';
1✔
3
import { log } from '../../logger.js';
1✔
4
import * as configApi from '../../config.js';
1✔
5
import common from '../common/common.js';
1✔
6
import utils from '../../utils.js';
1✔
7
import mermaidAPI from '../../mermaidAPI.js';
1✔
8
import {
1✔
9
  setAccTitle,
1✔
10
  getAccTitle,
1✔
11
  getAccDescription,
1✔
12
  setAccDescription,
1✔
13
  clear as commonClear,
1✔
14
  setDiagramTitle,
1✔
15
  getDiagramTitle,
1✔
16
} from '../../commonDb.js';
1✔
17
import {
1✔
18
  ClassRelation,
1✔
19
  ClassNode,
1✔
20
  ClassNote,
1✔
21
  ClassMap,
1✔
22
  NamespaceMap,
1✔
23
  NamespaceNode,
1✔
24
} from './classTypes.js';
1✔
25

1✔
26
const MERMAID_DOM_ID_PREFIX = 'classId-';
1✔
27

1✔
28
let relations: ClassRelation[] = [];
1✔
29
let classes: ClassMap = {};
1✔
30
let notes: ClassNote[] = [];
1✔
31
let classCounter = 0;
1✔
32
let namespaces: NamespaceMap = {};
1✔
33
let namespaceCounter = 0;
1✔
34

1✔
35
let functions: any[] = [];
1✔
36

1✔
37
const sanitizeText = (txt: string) => common.sanitizeText(txt, configApi.getConfig());
1✔
38

1✔
39
export const parseDirective = function (statement: string, context: string, type: string) {
1✔
40
  // @ts-ignore Don't wanna mess it up
×
41
  mermaidAPI.parseDirective(this, statement, context, type);
×
42
};
×
43

1✔
44
const splitClassNameAndType = function (id: string) {
1✔
45
  let genericType = '';
605✔
46
  let className = id;
605✔
47

605✔
48
  if (id.indexOf('~') > 0) {
605✔
49
    const split = id.split('~');
19✔
50
    className = sanitizeText(split[0]);
19✔
51
    genericType = sanitizeText(split[1]);
19✔
52
  }
19✔
53

605✔
54
  return { className: className, type: genericType };
605✔
55
};
605✔
56

1✔
57
export const setClassLabel = function (id: string, label: string) {
1✔
58
  if (label) {
56✔
59
    label = sanitizeText(label);
56✔
60
  }
56✔
61

56✔
62
  const { className } = splitClassNameAndType(id);
56✔
63
  classes[className].label = label;
56✔
64
};
56✔
65

1✔
66
/**
1✔
67
 * Function called by parser when a node definition has been found.
1✔
68
 *
1✔
69
 * @param id - Id of the class to add
1✔
70
 * @public
1✔
71
 */
1✔
72
export const addClass = function (id: string) {
1✔
73
  const classId = splitClassNameAndType(id);
259✔
74
  // Only add class if not exists
259✔
75
  if (classes[classId.className] !== undefined) {
259✔
76
    return;
32✔
77
  }
32✔
78

227✔
79
  classes[classId.className] = {
227✔
80
    id: classId.className,
227✔
81
    type: classId.type,
227✔
82
    label: classId.className,
227✔
83
    cssClasses: [],
227✔
84
    methods: [],
227✔
85
    members: [],
227✔
86
    annotations: [],
227✔
87
    domId: MERMAID_DOM_ID_PREFIX + classId.className + '-' + classCounter,
227✔
88
  } as ClassNode;
227✔
89

227✔
90
  classCounter++;
227✔
91
};
227✔
92

1✔
93
/**
1✔
94
 * Function to lookup domId from id in the graph definition.
1✔
95
 *
1✔
96
 * @param id - class ID to lookup
1✔
97
 * @public
1✔
98
 */
1✔
99
export const lookUpDomId = function (id: string): string {
1✔
100
  if (id in classes) {
×
101
    return classes[id].domId;
×
102
  }
×
103
  throw new Error('Class not found: ' + id);
×
104
};
×
105

1✔
106
export const clear = function () {
1✔
107
  relations = [];
126✔
108
  classes = {};
126✔
109
  notes = [];
126✔
110
  functions = [];
126✔
111
  functions.push(setupToolTips);
126✔
112
  namespaces = {};
126✔
113
  namespaceCounter = 0;
126✔
114
  commonClear();
126✔
115
};
126✔
116

1✔
117
export const getClass = function (id: string) {
1✔
118
  return classes[id];
106✔
119
};
106✔
120

1✔
121
export const getClasses = function () {
1✔
122
  return classes;
2✔
123
};
2✔
124

1✔
125
export const getRelations = function (): ClassRelation[] {
1✔
126
  return relations;
10✔
127
};
10✔
128

1✔
129
export const getNotes = function () {
1✔
130
  return notes;
×
131
};
×
132

1✔
133
export const addRelation = function (relation: ClassRelation) {
1✔
134
  log.debug('Adding relation: ' + JSON.stringify(relation));
58✔
135
  addClass(relation.id1);
58✔
136
  addClass(relation.id2);
58✔
137

58✔
138
  relation.id1 = splitClassNameAndType(relation.id1).className;
58✔
139
  relation.id2 = splitClassNameAndType(relation.id2).className;
58✔
140

58✔
141
  relation.relationTitle1 = common.sanitizeText(
58✔
142
    relation.relationTitle1.trim(),
58✔
143
    configApi.getConfig()
58✔
144
  );
58✔
145

58✔
146
  relation.relationTitle2 = common.sanitizeText(
58✔
147
    relation.relationTitle2.trim(),
58✔
148
    configApi.getConfig()
58✔
149
  );
58✔
150

58✔
151
  relations.push(relation);
58✔
152
};
58✔
153

1✔
154
/**
1✔
155
 * Adds an annotation to the specified class Annotations mark special properties of the given type
1✔
156
 * (like 'interface' or 'service')
1✔
157
 *
1✔
158
 * @param className - The class name
1✔
159
 * @param annotation - The name of the annotation without any brackets
1✔
160
 * @public
1✔
161
 */
1✔
162
export const addAnnotation = function (className: string, annotation: string) {
1✔
163
  const validatedClassName = splitClassNameAndType(className).className;
5✔
164
  classes[validatedClassName].annotations.push(annotation);
5✔
165
};
5✔
166

1✔
167
/**
1✔
168
 * Adds a member to the specified class
1✔
169
 *
1✔
170
 * @param className - The class name
1✔
171
 * @param member - The full name of the member. If the member is enclosed in `<<brackets>>` it is
1✔
172
 *   treated as an annotation If the member is ending with a closing bracket ) it is treated as a
1✔
173
 *   method Otherwise the member will be treated as a normal property
1✔
174
 * @public
1✔
175
 */
1✔
176
export const addMember = function (className: string, member: string) {
1✔
177
  const validatedClassName = splitClassNameAndType(className).className;
169✔
178
  const theClass = classes[validatedClassName];
169✔
179

169✔
180
  if (typeof member === 'string') {
169✔
181
    // Member can contain white spaces, we trim them out
169✔
182
    const memberString = member.trim();
169✔
183

169✔
184
    if (memberString.startsWith('<<') && memberString.endsWith('>>')) {
169✔
185
      // its an annotation
6✔
186
      theClass.annotations.push(sanitizeText(memberString.substring(2, memberString.length - 2)));
6✔
187
    } else if (memberString.indexOf(')') > 0) {
169✔
188
      //its a method
70✔
189
      theClass.methods.push(sanitizeText(memberString));
70✔
190
    } else if (memberString) {
161✔
191
      theClass.members.push(sanitizeText(memberString));
87✔
192
    }
87✔
193
  }
169✔
194
};
169✔
195

1✔
196
export const addMembers = function (className: string, members: string[]) {
1✔
197
  if (Array.isArray(members)) {
42✔
198
    members.reverse();
42✔
199
    members.forEach((member) => addMember(className, member));
42✔
200
  }
42✔
201
};
42✔
202

1✔
203
export const addNote = function (text: string, className: string) {
1✔
204
  const note = {
2✔
205
    id: `note${notes.length}`,
2✔
206
    class: className,
2✔
207
    text: text,
2✔
208
  };
2✔
209
  notes.push(note);
2✔
210
};
2✔
211

1✔
212
export const cleanupLabel = function (label: string) {
1✔
213
  if (label.startsWith(':')) {
56✔
214
    label = label.substring(1);
56✔
215
  }
56✔
216
  return sanitizeText(label.trim());
56✔
217
};
56✔
218

1✔
219
/**
1✔
220
 * Called by parser when a special node is found, e.g. a clickable element.
1✔
221
 *
1✔
222
 * @param ids - Comma separated list of ids
1✔
223
 * @param className - Class to add
1✔
224
 */
1✔
225
export const setCssClass = function (ids: string, className: string) {
1✔
226
  ids.split(',').forEach(function (_id) {
34✔
227
    let id = _id;
37✔
228
    if (_id[0].match(/\d/)) {
37!
229
      id = MERMAID_DOM_ID_PREFIX + id;
×
230
    }
×
231
    if (classes[id] !== undefined) {
37✔
232
      classes[id].cssClasses.push(className);
37✔
233
    }
37✔
234
  });
34✔
235
};
34✔
236

1✔
237
/**
1✔
238
 * Called by parser when a tooltip is found, e.g. a clickable element.
1✔
239
 *
1✔
240
 * @param ids - Comma separated list of ids
1✔
241
 * @param tooltip - Tooltip to add
1✔
242
 */
1✔
243
const setTooltip = function (ids: string, tooltip?: string) {
1✔
244
  ids.split(',').forEach(function (id) {
9✔
245
    if (tooltip !== undefined) {
9✔
246
      classes[id].tooltip = sanitizeText(tooltip);
9✔
247
    }
9✔
248
  });
9✔
249
};
9✔
250

1✔
251
export const getTooltip = function (id: string, namespace?: string) {
1✔
252
  if (namespace) {
×
253
    return namespaces[namespace].classes[id].tooltip;
×
254
  }
×
255

×
256
  return classes[id].tooltip;
×
257
};
×
258
/**
1✔
259
 * Called by parser when a link is found. Adds the URL to the vertex data.
1✔
260
 *
1✔
261
 * @param ids - Comma separated list of ids
1✔
262
 * @param linkStr - URL to create a link for
1✔
263
 * @param target - Target of the link, _blank by default as originally defined in the svgDraw.js file
1✔
264
 */
1✔
265
export const setLink = function (ids: string, linkStr: string, target: string) {
1✔
266
  const config = configApi.getConfig();
11✔
267
  ids.split(',').forEach(function (_id) {
11✔
268
    let id = _id;
11✔
269
    if (_id[0].match(/\d/)) {
11!
270
      id = MERMAID_DOM_ID_PREFIX + id;
×
271
    }
×
272
    if (classes[id] !== undefined) {
11✔
273
      classes[id].link = utils.formatUrl(linkStr, config);
11✔
274
      if (config.securityLevel === 'sandbox') {
11!
275
        classes[id].linkTarget = '_top';
×
276
      } else if (typeof target === 'string') {
11✔
277
        classes[id].linkTarget = sanitizeText(target);
4✔
278
      } else {
11✔
279
        classes[id].linkTarget = '_blank';
7✔
280
      }
7✔
281
    }
11✔
282
  });
11✔
283
  setCssClass(ids, 'clickable');
11✔
284
};
11✔
285

1✔
286
/**
1✔
287
 * Called by parser when a click definition is found. Registers an event handler.
1✔
288
 *
1✔
289
 * @param ids - Comma separated list of ids
1✔
290
 * @param functionName - Function to be called on click
1✔
291
 * @param functionArgs - Function args the function should be called with
1✔
292
 */
1✔
293
export const setClickEvent = function (ids: string, functionName: string, functionArgs: string) {
1✔
294
  ids.split(',').forEach(function (id) {
8✔
295
    setClickFunc(id, functionName, functionArgs);
8✔
296
    classes[id].haveCallback = true;
8✔
297
  });
8✔
298
  setCssClass(ids, 'clickable');
8✔
299
};
8✔
300

1✔
301
const setClickFunc = function (domId: string, functionName: string, functionArgs: string) {
1✔
302
  const config = configApi.getConfig();
8✔
303
  if (config.securityLevel !== 'loose') {
8✔
304
    return;
8✔
305
  }
8!
306
  if (functionName === undefined) {
×
307
    return;
×
308
  }
×
309

×
310
  const id = domId;
×
311
  if (classes[id] !== undefined) {
×
312
    const elemId = lookUpDomId(id);
×
313
    let argList: string[] = [];
×
314
    if (typeof functionArgs === 'string') {
×
315
      /* Splits functionArgs by ',', ignoring all ',' in double quoted strings */
×
316
      argList = functionArgs.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/);
×
317
      for (let i = 0; i < argList.length; i++) {
×
318
        let item = argList[i].trim();
×
319
        /* Removes all double quotes at the start and end of an argument */
×
320
        /* This preserves all starting and ending whitespace inside */
×
321
        if (item.charAt(0) === '"' && item.charAt(item.length - 1) === '"') {
×
322
          item = item.substr(1, item.length - 2);
×
323
        }
×
324
        argList[i] = item;
×
325
      }
×
326
    }
×
327

×
328
    /* if no arguments passed into callback, default to passing in id */
×
329
    if (argList.length === 0) {
×
330
      argList.push(elemId);
×
331
    }
×
332

×
333
    functions.push(function () {
×
334
      const elem = document.querySelector(`[id="${elemId}"]`);
×
335
      if (elem !== null) {
×
336
        elem.addEventListener(
×
337
          'click',
×
338
          function () {
×
339
            utils.runFunc(functionName, ...argList);
×
340
          },
×
341
          false
×
342
        );
×
343
      }
×
344
    });
×
345
  }
×
346
};
8✔
347

1✔
348
export const bindFunctions = function (element: Element) {
1✔
349
  functions.forEach(function (fun) {
×
350
    fun(element);
×
351
  });
×
352
};
×
353

1✔
354
export const lineType = {
1✔
355
  LINE: 0,
1✔
356
  DOTTED_LINE: 1,
1✔
357
};
1✔
358

1✔
359
export const relationType = {
1✔
360
  AGGREGATION: 0,
1✔
361
  EXTENSION: 1,
1✔
362
  COMPOSITION: 2,
1✔
363
  DEPENDENCY: 3,
1✔
364
  LOLLIPOP: 4,
1✔
365
};
1✔
366

1✔
367
const setupToolTips = function (element: Element) {
1✔
368
  let tooltipElem: Selection<HTMLDivElement, unknown, HTMLElement, unknown> =
×
369
    select('.mermaidTooltip');
×
370
  // @ts-ignore - _groups is a dynamic property
×
371
  if ((tooltipElem._groups || tooltipElem)[0][0] === null) {
×
372
    tooltipElem = select('body').append('div').attr('class', 'mermaidTooltip').style('opacity', 0);
×
373
  }
×
374

×
375
  const svg = select(element).select('svg');
×
376

×
377
  const nodes = svg.selectAll('g.node');
×
378
  nodes
×
379
    .on('mouseover', function () {
×
380
      // @ts-expect-error - select is not part of the d3 type definition
×
381
      const el = select(this);
×
382
      const title = el.attr('title');
×
383
      // Don't try to draw a tooltip if no data is provided
×
384
      if (title === null) {
×
385
        return;
×
386
      }
×
387
      // @ts-ignore - getBoundingClientRect is not part of the d3 type definition
×
388
      const rect = this.getBoundingClientRect();
×
389

×
390
      tooltipElem.transition().duration(200).style('opacity', '.9');
×
391
      tooltipElem
×
392
        .text(el.attr('title'))
×
393
        .style('left', window.scrollX + rect.left + (rect.right - rect.left) / 2 + 'px')
×
394
        .style('top', window.scrollY + rect.top - 14 + document.body.scrollTop + 'px');
×
395
      tooltipElem.html(tooltipElem.html().replace(/&lt;br\/&gt;/g, '<br/>'));
×
396
      el.classed('hover', true);
×
397
    })
×
398
    .on('mouseout', function () {
×
399
      tooltipElem.transition().duration(500).style('opacity', 0);
×
400
      // @ts-expect-error - select is not part of the d3 type definition
×
401
      const el = select(this);
×
402
      el.classed('hover', false);
×
403
    });
×
404
};
×
405
functions.push(setupToolTips);
1✔
406

1✔
407
let direction = 'TB';
1✔
408
const getDirection = () => direction;
1✔
409
const setDirection = (dir: string) => {
1✔
410
  direction = dir;
1✔
411
};
1✔
412

1✔
413
/**
1✔
414
 * Function called by parser when a namespace definition has been found.
1✔
415
 *
1✔
416
 * @param id - Id of the namespace to add
1✔
417
 * @public
1✔
418
 */
1✔
419
export const addNamespace = function (id: string) {
1✔
420
  if (namespaces[id] !== undefined) {
5!
421
    return;
×
422
  }
×
423

5✔
424
  namespaces[id] = {
5✔
425
    id: id,
5✔
426
    classes: {},
5✔
427
    children: {},
5✔
428
    domId: MERMAID_DOM_ID_PREFIX + id + '-' + namespaceCounter,
5✔
429
  } as NamespaceNode;
5✔
430

5✔
431
  namespaceCounter++;
5✔
432
};
5✔
433

1✔
434
const getNamespace = function (name: string): NamespaceNode {
1✔
435
  return namespaces[name];
1✔
436
};
1✔
437

1✔
438
const getNamespaces = function (): NamespaceMap {
1✔
439
  return namespaces;
×
440
};
×
441

1✔
442
/**
1✔
443
 * Function called by parser when a namespace definition has been found.
1✔
444
 *
1✔
445
 * @param id - Id of the namespace to add
1✔
446
 * @param classNames - Ids of the class to add
1✔
447
 * @public
1✔
448
 */
1✔
449
export const addClassesToNamespace = function (id: string, classNames: string[]) {
1✔
450
  if (namespaces[id] !== undefined) {
5✔
451
    classNames.map((className) => {
5✔
452
      namespaces[id].classes[className] = classes[className];
7✔
453
      delete classes[className];
7✔
454
      classCounter--;
7✔
455
    });
5✔
456
  }
5✔
457
};
5✔
458

1✔
459
export default {
1✔
460
  parseDirective,
1✔
461
  setAccTitle,
1✔
462
  getAccTitle,
1✔
463
  getAccDescription,
1✔
464
  setAccDescription,
1✔
465
  getConfig: () => configApi.getConfig().class,
1✔
466
  addClass,
1✔
467
  bindFunctions,
1✔
468
  clear,
1✔
469
  getClass,
1✔
470
  getClasses,
1✔
471
  getNotes,
1✔
472
  addAnnotation,
1✔
473
  addNote,
1✔
474
  getRelations,
1✔
475
  addRelation,
1✔
476
  getDirection,
1✔
477
  setDirection,
1✔
478
  addMember,
1✔
479
  addMembers,
1✔
480
  cleanupLabel,
1✔
481
  lineType,
1✔
482
  relationType,
1✔
483
  setClickEvent,
1✔
484
  setCssClass,
1✔
485
  setLink,
1✔
486
  getTooltip,
1✔
487
  setTooltip,
1✔
488
  lookUpDomId,
1✔
489
  setDiagramTitle,
1✔
490
  getDiagramTitle,
1✔
491
  setClassLabel,
1✔
492
  addNamespace,
1✔
493
  addClassesToNamespace,
1✔
494
  getNamespace,
1✔
495
  getNamespaces,
1✔
496
};
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