mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-12-24 05:06:28 +01:00
Compare commits
7 Commits
tooltip-po
...
6777-er-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee0d3209af | ||
|
|
6ac5e0e132 | ||
|
|
fca17f3b10 | ||
|
|
94dfdf31b8 | ||
|
|
f981d3d5b7 | ||
|
|
7139e1e5f7 | ||
|
|
299226f8c2 |
5
.changeset/angry-trains-fall.md
Normal file
5
.changeset/angry-trains-fall.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Make relationship-label optional in ER diagrams
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Correct tooltip placement to appear near hovered element
|
||||
@@ -322,6 +322,18 @@ ORDER ||--|{ LINE-ITEM : contains
|
||||
);
|
||||
});
|
||||
|
||||
it('should render an ER diagram without labels also', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
BOOK }|..|{ AUTHOR
|
||||
BOOK }|..|{ GENRE
|
||||
AUTHOR }|..|{ GENRE
|
||||
`,
|
||||
{ logLevel: 1 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render relationship labels with line breaks', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
|
||||
@@ -135,6 +135,44 @@ erDiagram
|
||||
"This **is** _Markdown_"
|
||||
```
|
||||
|
||||
#### Optional Relationship Labels (v\<MERMAID_RELEASE_VERSION>+)
|
||||
|
||||
The relationship label in ER diagrams is optional. You can define relationships without specifying a label, and the diagram will render correctly.
|
||||
|
||||
For example, the following is valid:
|
||||
|
||||
```mermaid-example
|
||||
erDiagram
|
||||
BOOK }|..|{ AUTHOR
|
||||
BOOK }|..|{ GENRE
|
||||
AUTHOR }|..|{ GENRE
|
||||
```
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
BOOK }|..|{ AUTHOR
|
||||
BOOK }|..|{ GENRE
|
||||
AUTHOR }|..|{ GENRE
|
||||
```
|
||||
|
||||
This will show the relationships between the entities without any labels on the connecting lines.
|
||||
|
||||
You can still add a label if you want to describe the relationship:
|
||||
|
||||
```mermaid-example
|
||||
erDiagram
|
||||
BOOK }|..|{ AUTHOR : written_by
|
||||
BOOK }|..|{ GENRE : categorized_as
|
||||
AUTHOR }|..|{ GENRE : specializes_in
|
||||
```
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
BOOK }|..|{ AUTHOR : written_by
|
||||
BOOK }|..|{ GENRE : categorized_as
|
||||
AUTHOR }|..|{ GENRE : specializes_in
|
||||
```
|
||||
|
||||
### Relationship Syntax
|
||||
|
||||
The `relationship` part of each statement can be broken down into three sub-components:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { select } from 'd3';
|
||||
import { select, type Selection } from 'd3';
|
||||
import { log } from '../../logger.js';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import common from '../common/common.js';
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
} from '../common/commonDb.js';
|
||||
import { createTooltip } from '../common/svgDrawCommon.js';
|
||||
import { ClassMember } from './classTypes.js';
|
||||
import type {
|
||||
ClassRelation,
|
||||
@@ -27,7 +26,6 @@ import type {
|
||||
} from './classTypes.js';
|
||||
import type { Node, Edge } from '../../rendering-util/types.js';
|
||||
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
const MERMAID_DOM_ID_PREFIX = 'classId-';
|
||||
let classCounter = 0;
|
||||
@@ -485,45 +483,43 @@ export class ClassDB implements DiagramDB {
|
||||
LOLLIPOP: 4,
|
||||
};
|
||||
|
||||
// Utility function to escape HTML meta-characters
|
||||
private escapeHtml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
private readonly setupToolTips = (element: Element) => {
|
||||
const tooltipElem = createTooltip();
|
||||
let tooltipElem: Selection<HTMLDivElement, unknown, HTMLElement, unknown> =
|
||||
select('.mermaidTooltip');
|
||||
// @ts-expect-error - Incorrect types
|
||||
if ((tooltipElem._groups || tooltipElem)[0][0] === null) {
|
||||
tooltipElem = select('body')
|
||||
.append('div')
|
||||
.attr('class', 'mermaidTooltip')
|
||||
.style('opacity', 0);
|
||||
}
|
||||
|
||||
const svg = select(element).select('svg');
|
||||
|
||||
const nodes = svg.selectAll('g').filter(function () {
|
||||
return select(this).attr('title') !== null;
|
||||
});
|
||||
|
||||
const nodes = svg.selectAll('g.node');
|
||||
nodes
|
||||
.on('mouseover', (event: MouseEvent) => {
|
||||
const el = select(event.currentTarget as HTMLElement);
|
||||
const title = el.attr('title');
|
||||
if (!title) {
|
||||
// Don't try to draw a tooltip if no data is provided
|
||||
if (title === null) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore - getBoundingClientRect is not part of the d3 type definition
|
||||
const rect = this.getBoundingClientRect();
|
||||
|
||||
const rect = (event.currentTarget as Element).getBoundingClientRect();
|
||||
tooltipElem.transition().duration(200).style('opacity', '.9');
|
||||
tooltipElem
|
||||
.html(DOMPurify.sanitize(title))
|
||||
.style('left', `${window.scrollX + rect.left + rect.width / 2}px`)
|
||||
.style('top', `${window.scrollY + rect.bottom + 4}px`);
|
||||
|
||||
.text(el.attr('title'))
|
||||
.style('left', window.scrollX + rect.left + (rect.right - rect.left) / 2 + 'px')
|
||||
.style('top', window.scrollY + rect.top - 14 + document.body.scrollTop + 'px');
|
||||
tooltipElem.html(tooltipElem.html().replace(/<br\/>/g, '<br/>'));
|
||||
el.classed('hover', true);
|
||||
})
|
||||
.on('mouseout', (event: MouseEvent) => {
|
||||
tooltipElem.transition().duration(500).style('opacity', 0);
|
||||
select(event.currentTarget as HTMLElement).classed('hover', false);
|
||||
const el = select(event.currentTarget as HTMLElement);
|
||||
el.classed('hover', false);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { sanitizeUrl } from '@braintree/sanitize-url';
|
||||
import { select } from 'd3';
|
||||
import type { SVG, SVGGroup } from '../../diagram-api/types.js';
|
||||
import { lineBreakRegex } from './common.js';
|
||||
import type {
|
||||
@@ -136,24 +135,3 @@ export const getTextObj = (): TextObject => {
|
||||
};
|
||||
return testObject;
|
||||
};
|
||||
|
||||
export const createTooltip = () => {
|
||||
let tooltipElem = select<HTMLDivElement, unknown>('.mermaidTooltip');
|
||||
if (tooltipElem.empty()) {
|
||||
tooltipElem = select('body')
|
||||
.append('div')
|
||||
.attr('class', 'mermaidTooltip')
|
||||
.style('opacity', 0)
|
||||
.style('position', 'absolute')
|
||||
.style('text-align', 'center')
|
||||
.style('max-width', '200px')
|
||||
.style('padding', '2px')
|
||||
.style('font-size', '12px')
|
||||
.style('background', '#ffffde')
|
||||
.style('border', '1px solid #333')
|
||||
.style('border-radius', '2px')
|
||||
.style('pointer-events', 'none')
|
||||
.style('z-index', '100');
|
||||
}
|
||||
return tooltipElem;
|
||||
};
|
||||
|
||||
@@ -99,6 +99,22 @@ start
|
||||
: 'ER_DIAGRAM' document 'EOF' { /*console.log('finished parsing');*/ }
|
||||
;
|
||||
|
||||
relationship
|
||||
: ENTITY relationType ENTITY maybeRole
|
||||
{
|
||||
yy.addRelationship($1, $4, $3, $2);
|
||||
};
|
||||
|
||||
maybeRole
|
||||
: COLON role
|
||||
{
|
||||
$$ = $2;
|
||||
}
|
||||
| /* empty */
|
||||
{
|
||||
$$ = '';
|
||||
};
|
||||
|
||||
document
|
||||
: /* empty */ { $$ = [] }
|
||||
| document line {$1.push($2);$$ = $1}
|
||||
@@ -113,32 +129,34 @@ line
|
||||
|
||||
|
||||
statement
|
||||
: entityName relSpec entityName COLON role
|
||||
: entityName relSpec entityName maybeRole
|
||||
{
|
||||
yy.addEntity($1);
|
||||
yy.addEntity($3);
|
||||
yy.addRelationship($1, $5, $3, $2);
|
||||
yy.addRelationship($1, $4, $3, $2);
|
||||
}
|
||||
| entityName STYLE_SEPARATOR idList relSpec entityName STYLE_SEPARATOR idList COLON role
|
||||
| entityName STYLE_SEPARATOR idList relSpec entityName STYLE_SEPARATOR idList maybeRole
|
||||
|
||||
{
|
||||
yy.addEntity($1);
|
||||
yy.addEntity($5);
|
||||
yy.addRelationship($1, $9, $5, $4);
|
||||
yy.addRelationship($1, $8, $5, $4);
|
||||
yy.setClass([$1], $3);
|
||||
yy.setClass([$5], $7);
|
||||
}
|
||||
| entityName STYLE_SEPARATOR idList relSpec entityName COLON role
|
||||
| entityName STYLE_SEPARATOR idList relSpec entityName maybeRole
|
||||
|
||||
{
|
||||
yy.addEntity($1);
|
||||
yy.addEntity($5);
|
||||
yy.addRelationship($1, $7, $5, $4);
|
||||
yy.addRelationship($1, $6, $5, $4);
|
||||
yy.setClass([$1], $3);
|
||||
}
|
||||
| entityName relSpec entityName STYLE_SEPARATOR idList COLON role
|
||||
| entityName relSpec entityName STYLE_SEPARATOR idList maybeRole
|
||||
{
|
||||
yy.addEntity($1);
|
||||
yy.addEntity($3);
|
||||
yy.addRelationship($1, $7, $3, $2);
|
||||
yy.addRelationship($1, $6, $3, $2);
|
||||
yy.setClass([$3], $5);
|
||||
}
|
||||
| entityName BLOCK_START attributes BLOCK_STOP
|
||||
|
||||
@@ -981,6 +981,12 @@ describe('when parsing ER diagram it...', function () {
|
||||
expect(rels[0].roleA).toBe('places');
|
||||
});
|
||||
|
||||
it('should allow label as optional', function () {
|
||||
erDiagram.parser.parse('erDiagram\nCUSTOMER ||--|{ ORDER');
|
||||
const rels = erDb.getRelationships();
|
||||
expect(rels[0].roleA).toBe('');
|
||||
});
|
||||
|
||||
it('should represent parent-child relationship correctly', function () {
|
||||
erDiagram.parser.parse('erDiagram\nPROJECT u--o{ TEAM_MEMBER : "parent"');
|
||||
const rels = erDb.getRelationships();
|
||||
@@ -989,6 +995,20 @@ describe('when parsing ER diagram it...', function () {
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.MD_PARENT);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
});
|
||||
|
||||
it('should handle whitespace-only relationship labels', function () {
|
||||
erDiagram.parser.parse('erDiagram\nBOOK }|..|{ AUTHOR : " "');
|
||||
let rels = erDb.getRelationships();
|
||||
expect(rels[rels.length - 1].roleA).toBe(' ');
|
||||
|
||||
erDiagram.parser.parse('erDiagram\nBOOK }|..|{ GENRE : "\t"');
|
||||
rels = erDb.getRelationships();
|
||||
expect(rels[rels.length - 1].roleA).toBe('\t');
|
||||
|
||||
erDiagram.parser.parse('erDiagram\nAUTHOR }|..|{ GENRE : " "');
|
||||
rels = erDb.getRelationships();
|
||||
expect(rels[rels.length - 1].roleA).toBe(' ');
|
||||
});
|
||||
});
|
||||
|
||||
describe('prototype properties', function () {
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
} from '../common/commonDb.js';
|
||||
import { createTooltip } from '../common/svgDrawCommon.js';
|
||||
import type {
|
||||
FlowClass,
|
||||
FlowEdge,
|
||||
@@ -27,7 +26,7 @@ import type {
|
||||
FlowVertex,
|
||||
FlowVertexTypeParam,
|
||||
} from './types.js';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
interface LinkData {
|
||||
id: string;
|
||||
}
|
||||
@@ -575,7 +574,15 @@ You have to call mermaid.initialize.`
|
||||
}
|
||||
|
||||
private setupToolTips(element: Element) {
|
||||
const tooltipElem = createTooltip();
|
||||
let tooltipElem = select('.mermaidTooltip');
|
||||
// @ts-ignore TODO: fix this
|
||||
if ((tooltipElem._groups || tooltipElem)[0][0] === null) {
|
||||
// @ts-ignore TODO: fix this
|
||||
tooltipElem = select('body')
|
||||
.append('div')
|
||||
.attr('class', 'mermaidTooltip')
|
||||
.style('opacity', 0);
|
||||
}
|
||||
|
||||
const svg = select(element).select('svg');
|
||||
|
||||
@@ -596,7 +603,7 @@ You have to call mermaid.initialize.`
|
||||
.text(el.attr('title'))
|
||||
.style('left', window.scrollX + rect.left + (rect.right - rect.left) / 2 + 'px')
|
||||
.style('top', window.scrollY + rect.bottom + 'px');
|
||||
tooltipElem.html(DOMPurify.sanitize(title));
|
||||
tooltipElem.html(tooltipElem.html().replace(/<br\/>/g, '<br/>'));
|
||||
el.classed('hover', true);
|
||||
})
|
||||
.on('mouseout', (e: MouseEvent) => {
|
||||
|
||||
@@ -89,6 +89,30 @@ erDiagram
|
||||
"This **is** _Markdown_"
|
||||
```
|
||||
|
||||
#### Optional Relationship Labels (v<MERMAID_RELEASE_VERSION>+)
|
||||
|
||||
The relationship label in ER diagrams is optional. You can define relationships without specifying a label, and the diagram will render correctly.
|
||||
|
||||
For example, the following is valid:
|
||||
|
||||
```mermaid-example
|
||||
erDiagram
|
||||
BOOK }|..|{ AUTHOR
|
||||
BOOK }|..|{ GENRE
|
||||
AUTHOR }|..|{ GENRE
|
||||
```
|
||||
|
||||
This will show the relationships between the entities without any labels on the connecting lines.
|
||||
|
||||
You can still add a label if you want to describe the relationship:
|
||||
|
||||
```mermaid-example
|
||||
erDiagram
|
||||
BOOK }|..|{ AUTHOR : written_by
|
||||
BOOK }|..|{ GENRE : categorized_as
|
||||
AUTHOR }|..|{ GENRE : specializes_in
|
||||
```
|
||||
|
||||
### Relationship Syntax
|
||||
|
||||
The `relationship` part of each statement can be broken down into three sub-components:
|
||||
|
||||
Reference in New Issue
Block a user