mirror of
				https://github.com/mermaid-js/mermaid.git
				synced 2025-11-04 04:44:08 +01:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			6777-er-re
			...
			sidv/neste
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					6359ab504f | 
@@ -1,5 +0,0 @@
 | 
			
		||||
---
 | 
			
		||||
'mermaid': patch
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
fix: Make relationship-label optional in ER diagrams
 | 
			
		||||
@@ -322,18 +322,6 @@ 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(
 | 
			
		||||
      `
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@
 | 
			
		||||
 | 
			
		||||
> **addDirective**(`directive`): `void`
 | 
			
		||||
 | 
			
		||||
Defined in: [packages/mermaid/src/config.ts:188](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L188)
 | 
			
		||||
Defined in: [packages/mermaid/src/config.ts:202](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L202)
 | 
			
		||||
 | 
			
		||||
Pushes in a directive to the configuration
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@
 | 
			
		||||
 | 
			
		||||
> **reset**(`config`): `void`
 | 
			
		||||
 | 
			
		||||
Defined in: [packages/mermaid/src/config.ts:221](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L221)
 | 
			
		||||
Defined in: [packages/mermaid/src/config.ts:235](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L235)
 | 
			
		||||
 | 
			
		||||
## reset
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@
 | 
			
		||||
 | 
			
		||||
# Function: sanitize()
 | 
			
		||||
 | 
			
		||||
> **sanitize**(`options`): `void`
 | 
			
		||||
> **sanitize**(`options`, `path`): `void`
 | 
			
		||||
 | 
			
		||||
Defined in: [packages/mermaid/src/config.ts:146](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L146)
 | 
			
		||||
 | 
			
		||||
@@ -31,6 +31,10 @@ options in-place
 | 
			
		||||
 | 
			
		||||
The potential setConfig parameter
 | 
			
		||||
 | 
			
		||||
### path
 | 
			
		||||
 | 
			
		||||
`string`\[] = `[]`
 | 
			
		||||
 | 
			
		||||
## Returns
 | 
			
		||||
 | 
			
		||||
`void`
 | 
			
		||||
 
 | 
			
		||||
@@ -135,44 +135,6 @@ erDiagram
 | 
			
		||||
    "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
 | 
			
		||||
 | 
			
		||||
The `relationship` part of each statement can be broken down into three sub-components:
 | 
			
		||||
 
 | 
			
		||||
@@ -34,6 +34,92 @@ describe('when working with site config', () => {
 | 
			
		||||
    expect(cfg.fontSize).toBe(config_0.fontSize);
 | 
			
		||||
    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', () => {
 | 
			
		||||
    const defaultConfig = configApi.getConfig();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -143,17 +143,29 @@ export const getConfig = (): MermaidConfig => {
 | 
			
		||||
 *
 | 
			
		||||
 * @param options - The potential setConfig parameter
 | 
			
		||||
 */
 | 
			
		||||
export const sanitize = (options: any) => {
 | 
			
		||||
export const sanitize = (options: any, path: string[] = []) => {
 | 
			
		||||
  if (!options) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Checking that options are not in the list of excluded options
 | 
			
		||||
  ['secure', ...(siteConfig.secure ?? [])].forEach((key) => {
 | 
			
		||||
    if (Object.hasOwn(options, key)) {
 | 
			
		||||
      // DO NOT attempt to print options[key] within `${}` as a malicious script
 | 
			
		||||
      // can exploit the logger's attempt to stringify the value and execute arbitrary code
 | 
			
		||||
      log.debug(`Denied attempt to modify a secure key ${key}`, options[key]);
 | 
			
		||||
      delete options[key];
 | 
			
		||||
  ['secure', ...(siteConfig.secure ?? [])].forEach((secureKey) => {
 | 
			
		||||
    const securePath = secureKey.split('.');
 | 
			
		||||
 | 
			
		||||
    // Check if current path matches the secure key path
 | 
			
		||||
    if (path.length >= securePath.length - 1) {
 | 
			
		||||
      const targetKey = securePath[securePath.length - 1];
 | 
			
		||||
      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];
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
@@ -163,6 +175,7 @@ export const sanitize = (options: any) => {
 | 
			
		||||
      delete options[key];
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // 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
 | 
			
		||||
  Object.keys(options).forEach((key) => {
 | 
			
		||||
@@ -174,8 +187,9 @@ export const sanitize = (options: any) => {
 | 
			
		||||
    ) {
 | 
			
		||||
      delete options[key];
 | 
			
		||||
    }
 | 
			
		||||
    if (typeof options[key] === 'object') {
 | 
			
		||||
      sanitize(options[key]);
 | 
			
		||||
    if (typeof options[key] === 'object' && options[key] !== null) {
 | 
			
		||||
      // Recursively sanitize nested objects with updated path
 | 
			
		||||
      sanitize(options[key], [...path, key]);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -94,22 +94,6 @@ 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}
 | 
			
		||||
@@ -124,34 +108,32 @@ line
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
statement
 | 
			
		||||
    : entityName relSpec entityName maybeRole
 | 
			
		||||
    : entityName relSpec entityName COLON role
 | 
			
		||||
      {
 | 
			
		||||
          yy.addEntity($1);
 | 
			
		||||
          yy.addEntity($3);
 | 
			
		||||
          yy.addRelationship($1, $4, $3, $2);
 | 
			
		||||
          yy.addRelationship($1, $5, $3, $2);
 | 
			
		||||
      }
 | 
			
		||||
    | entityName STYLE_SEPARATOR idList relSpec entityName STYLE_SEPARATOR idList maybeRole
 | 
			
		||||
 | 
			
		||||
    | entityName STYLE_SEPARATOR idList relSpec entityName STYLE_SEPARATOR idList COLON role
 | 
			
		||||
      {
 | 
			
		||||
          yy.addEntity($1);
 | 
			
		||||
          yy.addEntity($5);
 | 
			
		||||
          yy.addRelationship($1, $8, $5, $4);
 | 
			
		||||
          yy.addRelationship($1, $9, $5, $4);
 | 
			
		||||
          yy.setClass([$1], $3);
 | 
			
		||||
          yy.setClass([$5], $7);
 | 
			
		||||
      }
 | 
			
		||||
    | entityName STYLE_SEPARATOR idList relSpec entityName maybeRole
 | 
			
		||||
 | 
			
		||||
    | entityName STYLE_SEPARATOR idList relSpec entityName COLON role
 | 
			
		||||
      {
 | 
			
		||||
          yy.addEntity($1);
 | 
			
		||||
          yy.addEntity($5);
 | 
			
		||||
          yy.addRelationship($1, $6, $5, $4);
 | 
			
		||||
          yy.addRelationship($1, $7, $5, $4);
 | 
			
		||||
          yy.setClass([$1], $3);
 | 
			
		||||
      }
 | 
			
		||||
    | entityName relSpec entityName STYLE_SEPARATOR idList maybeRole
 | 
			
		||||
    | entityName relSpec entityName STYLE_SEPARATOR idList COLON role
 | 
			
		||||
      {
 | 
			
		||||
          yy.addEntity($1);
 | 
			
		||||
          yy.addEntity($3);
 | 
			
		||||
          yy.addRelationship($1, $6, $3, $2);
 | 
			
		||||
          yy.addRelationship($1, $7, $3, $2);
 | 
			
		||||
          yy.setClass([$3], $5);
 | 
			
		||||
      }
 | 
			
		||||
    | entityName BLOCK_START attributes BLOCK_STOP
 | 
			
		||||
 
 | 
			
		||||
@@ -981,12 +981,6 @@ 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();
 | 
			
		||||
@@ -995,20 +989,6 @@ 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 () {
 | 
			
		||||
 
 | 
			
		||||
@@ -89,30 +89,6 @@ erDiagram
 | 
			
		||||
    "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
 | 
			
		||||
 | 
			
		||||
The `relationship` part of each statement can be broken down into three sub-components:
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user