mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-10-09 17:19:45 +02:00
Compare commits
5 Commits
sidv/neste
...
6777-er-re
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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
|
@@ -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', () => {
|
it('should render relationship labels with line breaks', () => {
|
||||||
imgSnapshotTest(
|
imgSnapshotTest(
|
||||||
`
|
`
|
||||||
|
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
> **addDirective**(`directive`): `void`
|
> **addDirective**(`directive`): `void`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/config.ts:202](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L202)
|
Defined in: [packages/mermaid/src/config.ts:188](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L188)
|
||||||
|
|
||||||
Pushes in a directive to the configuration
|
Pushes in a directive to the configuration
|
||||||
|
|
||||||
|
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
> **reset**(`config`): `void`
|
> **reset**(`config`): `void`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/config.ts:235](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L235)
|
Defined in: [packages/mermaid/src/config.ts:221](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L221)
|
||||||
|
|
||||||
## reset
|
## reset
|
||||||
|
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
# Function: sanitize()
|
# Function: sanitize()
|
||||||
|
|
||||||
> **sanitize**(`options`, `path`): `void`
|
> **sanitize**(`options`): `void`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/config.ts:146](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L146)
|
Defined in: [packages/mermaid/src/config.ts:146](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L146)
|
||||||
|
|
||||||
@@ -31,10 +31,6 @@ options in-place
|
|||||||
|
|
||||||
The potential setConfig parameter
|
The potential setConfig parameter
|
||||||
|
|
||||||
### path
|
|
||||||
|
|
||||||
`string`\[] = `[]`
|
|
||||||
|
|
||||||
## Returns
|
## Returns
|
||||||
|
|
||||||
`void`
|
`void`
|
||||||
|
@@ -135,6 +135,44 @@ erDiagram
|
|||||||
"This **is** _Markdown_"
|
"This **is** _Markdown_"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Optional Relationship Labels
|
||||||
|
|
||||||
|
Starting from Mermaid version 11.11.0, 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
|
### Relationship Syntax
|
||||||
|
|
||||||
The `relationship` part of each statement can be broken down into three sub-components:
|
The `relationship` part of each statement can be broken down into three sub-components:
|
||||||
|
@@ -34,92 +34,6 @@ describe('when working with site config', () => {
|
|||||||
expect(cfg.fontSize).toBe(config_0.fontSize);
|
expect(cfg.fontSize).toBe(config_0.fontSize);
|
||||||
expect(cfg.securityLevel).toBe(config_0.securityLevel);
|
expect(cfg.securityLevel).toBe(config_0.securityLevel);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should respect nested secure keys when applying directives', () => {
|
|
||||||
const config_0: MermaidConfig = {
|
|
||||||
fontFamily: 'foo-font',
|
|
||||||
themeVariables: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontFamily: 'default-font',
|
|
||||||
},
|
|
||||||
secure: [
|
|
||||||
...configApi.defaultConfig.secure!,
|
|
||||||
'themeVariables.fontSize',
|
|
||||||
'themeVariables.fontFamily',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
configApi.setSiteConfig(config_0);
|
|
||||||
const directive: MermaidConfig = {
|
|
||||||
fontFamily: 'baf',
|
|
||||||
themeVariables: {
|
|
||||||
fontSize: 24, // shouldn't be changed
|
|
||||||
fontFamily: 'new-font', // shouldn't be changed
|
|
||||||
primaryColor: '#ff0000', // should be allowed
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const cfg: MermaidConfig = configApi.updateCurrentConfig(config_0, [directive]);
|
|
||||||
expect(cfg.fontFamily).toEqual(directive.fontFamily);
|
|
||||||
expect(cfg.themeVariables!.fontSize).toBe(config_0.themeVariables!.fontSize);
|
|
||||||
expect(cfg.themeVariables!.fontFamily).toBe(config_0.themeVariables!.fontFamily);
|
|
||||||
expect(cfg.themeVariables!.primaryColor).toBe(directive.themeVariables!.primaryColor);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle deeply nested secure keys', () => {
|
|
||||||
const config_0: MermaidConfig = {
|
|
||||||
flowchart: {
|
|
||||||
nodeSpacing: 50,
|
|
||||||
rankSpacing: 50,
|
|
||||||
curve: 'basis',
|
|
||||||
htmlLabels: true,
|
|
||||||
useMaxWidth: true,
|
|
||||||
diagramPadding: 8,
|
|
||||||
},
|
|
||||||
secure: [
|
|
||||||
...configApi.defaultConfig.secure!,
|
|
||||||
'flowchart.nodeSpacing',
|
|
||||||
'flowchart.rankSpacing',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
configApi.setSiteConfig(config_0);
|
|
||||||
const directive: MermaidConfig = {
|
|
||||||
flowchart: {
|
|
||||||
nodeSpacing: 100, // shouldn't be changed
|
|
||||||
rankSpacing: 100, // shouldn't be changed
|
|
||||||
curve: 'linear', // should be allowed
|
|
||||||
htmlLabels: false, // should be allowed
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const cfg: MermaidConfig = configApi.updateCurrentConfig(config_0, [directive]);
|
|
||||||
expect(cfg.flowchart!.nodeSpacing).toBe(config_0.flowchart!.nodeSpacing);
|
|
||||||
expect(cfg.flowchart!.rankSpacing).toBe(config_0.flowchart!.rankSpacing);
|
|
||||||
expect(cfg.flowchart!.curve).toBe(directive.flowchart!.curve);
|
|
||||||
expect(cfg.flowchart!.htmlLabels).toBe(directive.flowchart!.htmlLabels);
|
|
||||||
expect(cfg.flowchart!.diagramPadding).toBe(config_0.flowchart!.diagramPadding);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle mixed top-level and nested secure keys', () => {
|
|
||||||
const config_0: MermaidConfig = {
|
|
||||||
fontFamily: 'foo-font',
|
|
||||||
themeVariables: {
|
|
||||||
fontSize: 16,
|
|
||||||
primaryColor: '#000000',
|
|
||||||
},
|
|
||||||
secure: [...configApi.defaultConfig.secure!, 'fontFamily', 'themeVariables.fontSize'],
|
|
||||||
};
|
|
||||||
configApi.setSiteConfig(config_0);
|
|
||||||
const directive: MermaidConfig = {
|
|
||||||
fontFamily: 'new-font', // shouldn't be changed
|
|
||||||
themeVariables: {
|
|
||||||
fontSize: 24, // shouldn't be changed
|
|
||||||
primaryColor: '#ff0000', // should be allowed
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const cfg: MermaidConfig = configApi.updateCurrentConfig(config_0, [directive]);
|
|
||||||
expect(cfg.fontFamily).toBe(config_0.fontFamily);
|
|
||||||
expect(cfg.themeVariables!.fontSize).toBe(config_0.themeVariables!.fontSize);
|
|
||||||
expect(cfg.themeVariables!.primaryColor).toBe(directive.themeVariables!.primaryColor);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow setting partial options', () => {
|
it('should allow setting partial options', () => {
|
||||||
const defaultConfig = configApi.getConfig();
|
const defaultConfig = configApi.getConfig();
|
||||||
|
|
||||||
|
@@ -143,29 +143,17 @@ export const getConfig = (): MermaidConfig => {
|
|||||||
*
|
*
|
||||||
* @param options - The potential setConfig parameter
|
* @param options - The potential setConfig parameter
|
||||||
*/
|
*/
|
||||||
export const sanitize = (options: any, path: string[] = []) => {
|
export const sanitize = (options: any) => {
|
||||||
if (!options) {
|
if (!options) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checking that options are not in the list of excluded options
|
// Checking that options are not in the list of excluded options
|
||||||
['secure', ...(siteConfig.secure ?? [])].forEach((secureKey) => {
|
['secure', ...(siteConfig.secure ?? [])].forEach((key) => {
|
||||||
const securePath = secureKey.split('.');
|
if (Object.hasOwn(options, key)) {
|
||||||
|
// DO NOT attempt to print options[key] within `${}` as a malicious script
|
||||||
// Check if current path matches the secure key path
|
// can exploit the logger's attempt to stringify the value and execute arbitrary code
|
||||||
if (path.length >= securePath.length - 1) {
|
log.debug(`Denied attempt to modify a secure key ${key}`, options[key]);
|
||||||
const targetKey = securePath[securePath.length - 1];
|
delete options[key];
|
||||||
const pathSuffix = path.slice(-(securePath.length - 1));
|
|
||||||
const pathPrefix = securePath.slice(0, -1);
|
|
||||||
|
|
||||||
const isMatch =
|
|
||||||
securePath.length === 1 ? path.length === 0 : pathSuffix.join('.') === pathPrefix.join('.');
|
|
||||||
|
|
||||||
if (isMatch && Object.hasOwn(options, targetKey)) {
|
|
||||||
const fullPath = path.length > 0 ? `${path.join('.')}.${secureKey}` : secureKey;
|
|
||||||
log.debug(`Denied attempt to modify a secure key ${fullPath}`, options[targetKey]);
|
|
||||||
delete options[targetKey];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -175,7 +163,6 @@ export const sanitize = (options: any, path: string[] = []) => {
|
|||||||
delete options[key];
|
delete options[key];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check that there no attempts of xss, there should be no tags at all in the directive
|
// Check that there no attempts of xss, there should be no tags at all in the directive
|
||||||
// blocking data urls as base64 urls can contain svg's with inline script tags
|
// blocking data urls as base64 urls can contain svg's with inline script tags
|
||||||
Object.keys(options).forEach((key) => {
|
Object.keys(options).forEach((key) => {
|
||||||
@@ -187,9 +174,8 @@ export const sanitize = (options: any, path: string[] = []) => {
|
|||||||
) {
|
) {
|
||||||
delete options[key];
|
delete options[key];
|
||||||
}
|
}
|
||||||
if (typeof options[key] === 'object' && options[key] !== null) {
|
if (typeof options[key] === 'object') {
|
||||||
// Recursively sanitize nested objects with updated path
|
sanitize(options[key]);
|
||||||
sanitize(options[key], [...path, key]);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@@ -94,6 +94,22 @@ start
|
|||||||
: 'ER_DIAGRAM' document 'EOF' { /*console.log('finished parsing');*/ }
|
: '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
|
document
|
||||||
: /* empty */ { $$ = [] }
|
: /* empty */ { $$ = [] }
|
||||||
| document line {$1.push($2);$$ = $1}
|
| document line {$1.push($2);$$ = $1}
|
||||||
@@ -108,32 +124,34 @@ line
|
|||||||
|
|
||||||
|
|
||||||
statement
|
statement
|
||||||
: entityName relSpec entityName COLON role
|
: entityName relSpec entityName maybeRole
|
||||||
{
|
{
|
||||||
yy.addEntity($1);
|
yy.addEntity($1);
|
||||||
yy.addEntity($3);
|
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($1);
|
||||||
yy.addEntity($5);
|
yy.addEntity($5);
|
||||||
yy.addRelationship($1, $9, $5, $4);
|
yy.addRelationship($1, $8, $5, $4);
|
||||||
yy.setClass([$1], $3);
|
yy.setClass([$1], $3);
|
||||||
yy.setClass([$5], $7);
|
yy.setClass([$5], $7);
|
||||||
}
|
}
|
||||||
| entityName STYLE_SEPARATOR idList relSpec entityName COLON role
|
| entityName STYLE_SEPARATOR idList relSpec entityName maybeRole
|
||||||
|
|
||||||
{
|
{
|
||||||
yy.addEntity($1);
|
yy.addEntity($1);
|
||||||
yy.addEntity($5);
|
yy.addEntity($5);
|
||||||
yy.addRelationship($1, $7, $5, $4);
|
yy.addRelationship($1, $6, $5, $4);
|
||||||
yy.setClass([$1], $3);
|
yy.setClass([$1], $3);
|
||||||
}
|
}
|
||||||
| entityName relSpec entityName STYLE_SEPARATOR idList COLON role
|
| entityName relSpec entityName STYLE_SEPARATOR idList maybeRole
|
||||||
{
|
{
|
||||||
yy.addEntity($1);
|
yy.addEntity($1);
|
||||||
yy.addEntity($3);
|
yy.addEntity($3);
|
||||||
yy.addRelationship($1, $7, $3, $2);
|
yy.addRelationship($1, $6, $3, $2);
|
||||||
yy.setClass([$3], $5);
|
yy.setClass([$3], $5);
|
||||||
}
|
}
|
||||||
| entityName BLOCK_START attributes BLOCK_STOP
|
| entityName BLOCK_START attributes BLOCK_STOP
|
||||||
|
@@ -981,6 +981,12 @@ describe('when parsing ER diagram it...', function () {
|
|||||||
expect(rels[0].roleA).toBe('places');
|
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 () {
|
it('should represent parent-child relationship correctly', function () {
|
||||||
erDiagram.parser.parse('erDiagram\nPROJECT u--o{ TEAM_MEMBER : "parent"');
|
erDiagram.parser.parse('erDiagram\nPROJECT u--o{ TEAM_MEMBER : "parent"');
|
||||||
const rels = erDb.getRelationships();
|
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.cardB).toBe(erDb.Cardinality.MD_PARENT);
|
||||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
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 () {
|
describe('prototype properties', function () {
|
||||||
|
@@ -89,6 +89,30 @@ erDiagram
|
|||||||
"This **is** _Markdown_"
|
"This **is** _Markdown_"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Optional Relationship Labels
|
||||||
|
|
||||||
|
Starting from Mermaid version 11.11.0, 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
|
### Relationship Syntax
|
||||||
|
|
||||||
The `relationship` part of each statement can be broken down into three sub-components:
|
The `relationship` part of each statement can be broken down into three sub-components:
|
||||||
|
Reference in New Issue
Block a user