Compare commits

..

7 Commits

Author SHA1 Message Date
darshanr0107
ee0d3209af Merge branch 'develop' into 6777-er-relationship-label-optional 2025-11-12 15:33:36 +05:30
darshanr0107
6ac5e0e132 chore: update docs
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-11-12 13:54:04 +05:30
darshanr0107
fca17f3b10 chore: update ER diagram docs
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-03 13:10:50 +05:30
darshanr0107
94dfdf31b8 fix: add test case to endsure whitespace-only relationship labels are handled
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-02 11:40:35 +05:30
darshanr0107
f981d3d5b7 chore: updated changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-20 20:06:45 +05:30
darshanr0107
7139e1e5f7 chore: added changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-20 18:58:20 +05:30
darshanr0107
299226f8c2 fix: relationship label to be optional in ER diagram syntax
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-20 18:36:45 +05:30
10 changed files with 157 additions and 64 deletions

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Make relationship-label optional in ER diagrams

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: Correct tooltip placement to appear near hovered element

View File

@@ -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(
`

View File

@@ -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:

View File

@@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
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(/&lt;br\/&gt;/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);
});
};

View File

@@ -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;
};

View File

@@ -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

View File

@@ -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 () {

View File

@@ -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(/&lt;br\/&gt;/g, '<br/>'));
el.classed('hover', true);
})
.on('mouseout', (e: MouseEvent) => {

View File

@@ -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: