mirror of
				https://github.com/mermaid-js/mermaid.git
				synced 2025-11-03 20:34:20 +01:00 
			
		
		
		
	Compare commits
	
		
			121 Commits
		
	
	
		
			release/10
			...
			sidv/Remov
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					52f05552a1 | ||
| 
						 | 
					bb01b3972d | ||
| 
						 | 
					1a50a326cb | ||
| 
						 | 
					31c0a0cbab | ||
| 
						 | 
					f4c62436ea | ||
| 
						 | 
					21d0998db8 | ||
| 
						 | 
					0899f7918a | ||
| 
						 | 
					7a547abd89 | ||
| 
						 | 
					72fa3488b5 | ||
| 
						 | 
					cfffba2817 | ||
| 
						 | 
					e7ee3eb9ea | ||
| 
						 | 
					b85c011cd1 | ||
| 
						 | 
					aec97d68cc | ||
| 
						 | 
					7b7e281ec7 | ||
| 
						 | 
					35cc34d422 | ||
| 
						 | 
					edb6ceae43 | ||
| 
						 | 
					ef8b75a6da | ||
| 
						 | 
					02daf5417b | ||
| 
						 | 
					77d8e15dc4 | ||
| 
						 | 
					62142089f1 | ||
| 
						 | 
					defe40692a | ||
| 
						 | 
					337ff3c32b | ||
| 
						 | 
					744cc792f4 | ||
| 
						 | 
					99978da55b | ||
| 
						 | 
					50eb3cf1c9 | ||
| 
						 | 
					c1b9c54fc9 | ||
| 
						 | 
					0d179c501e | ||
| 
						 | 
					085e8f78b3 | ||
| 
						 | 
					f9d4a62609 | ||
| 
						 | 
					2a8374312f | ||
| 
						 | 
					b60410161d | ||
| 
						 | 
					d219f92a19 | ||
| 
						 | 
					3f3a7340e3 | ||
| 
						 | 
					cb5f70c139 | ||
| 
						 | 
					99c1758490 | ||
| 
						 | 
					43217e1395 | ||
| 
						 | 
					aa34b99203 | ||
| 
						 | 
					541ee1eade | ||
| 
						 | 
					f01ad644e3 | ||
| 
						 | 
					9538233573 | ||
| 
						 | 
					5a2ea7c297 | ||
| 
						 | 
					22b172d873 | ||
| 
						 | 
					92098e23eb | ||
| 
						 | 
					591cb794eb | ||
| 
						 | 
					f9d978859e | ||
| 
						 | 
					6170538c47 | ||
| 
						 | 
					c37c494a1e | ||
| 
						 | 
					8a8e062342 | ||
| 
						 | 
					1721282182 | ||
| 
						 | 
					d8dd68cad2 | ||
| 
						 | 
					120bdabee1 | ||
| 
						 | 
					a5a3ffc768 | ||
| 
						 | 
					dfeb25127b | ||
| 
						 | 
					396bda8d95 | ||
| 
						 | 
					cc70d37166 | ||
| 
						 | 
					3f5da06bb0 | ||
| 
						 | 
					be106befff | ||
| 
						 | 
					d5f11fc80a | ||
| 
						 | 
					c4113541e1 | ||
| 
						 | 
					fb49f25eef | ||
| 
						 | 
					a9681d1b1c | ||
| 
						 | 
					545d361d3f | ||
| 
						 | 
					3e598f4e8e | ||
| 
						 | 
					eb397fdb04 | ||
| 
						 | 
					6c1afb7a8d | ||
| 
						 | 
					fee6f459a8 | ||
| 
						 | 
					e128a11f3b | ||
| 
						 | 
					38dc17f426 | ||
| 
						 | 
					453c67e5ea | ||
| 
						 | 
					a69a97fdd9 | ||
| 
						 | 
					820cc48c11 | ||
| 
						 | 
					06e4a6398c | ||
| 
						 | 
					ca1cdb1d94 | ||
| 
						 | 
					5485517b27 | ||
| 
						 | 
					96380600d9 | ||
| 
						 | 
					9563b22132 | ||
| 
						 | 
					e6a18eea91 | ||
| 
						 | 
					71205f5bd6 | ||
| 
						 | 
					23b6d53f80 | ||
| 
						 | 
					f4671e4e3a | ||
| 
						 | 
					c954e0eb1d | ||
| 
						 | 
					de37efefd7 | ||
| 
						 | 
					bdfd8974d4 | ||
| 
						 | 
					aecf451ed1 | ||
| 
						 | 
					7d69ad2d5b | ||
| 
						 | 
					74fa9956a3 | ||
| 
						 | 
					e33340331a | ||
| 
						 | 
					760548335c | ||
| 
						 | 
					41c5152015 | ||
| 
						 | 
					796a761a7d | ||
| 
						 | 
					09c4a26509 | ||
| 
						 | 
					ce9d0e2e6a | ||
| 
						 | 
					ae8860eec3 | ||
| 
						 | 
					67d287f85e | ||
| 
						 | 
					9c2b95fc3c | ||
| 
						 | 
					3a22d4a501 | ||
| 
						 | 
					cecf759b0b | ||
| 
						 | 
					35c6b671de | ||
| 
						 | 
					c894c1f5b5 | ||
| 
						 | 
					a92571d588 | ||
| 
						 | 
					34a47706fd | ||
| 
						 | 
					906d909d87 | ||
| 
						 | 
					1d0aa763de | ||
| 
						 | 
					f6dc089ddf | ||
| 
						 | 
					95e01b4935 | ||
| 
						 | 
					9145a9e69e | ||
| 
						 | 
					6941814729 | ||
| 
						 | 
					32d3001e2a | ||
| 
						 | 
					452e543e77 | ||
| 
						 | 
					23a5832fc9 | ||
| 
						 | 
					8794fa0b38 | ||
| 
						 | 
					f2338f5b66 | ||
| 
						 | 
					5aba2fed8b | ||
| 
						 | 
					bd6795032f | ||
| 
						 | 
					c17b723295 | ||
| 
						 | 
					231a9630df | ||
| 
						 | 
					bdb967e0a8 | ||
| 
						 | 
					ea3fbbd58d | ||
| 
						 | 
					afea3e8f37 | ||
| 
						 | 
					4e7dbf76cc | ||
| 
						 | 
					a3901f691a | 
							
								
								
									
										15
									
								
								.github/release-drafter.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								.github/release-drafter.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,14 +1,27 @@
 | 
			
		||||
name-template: '$NEXT_PATCH_VERSION'
 | 
			
		||||
tag-template: '$NEXT_PATCH_VERSION'
 | 
			
		||||
categories:
 | 
			
		||||
  - title: '🚨 **Breaking Changes**'
 | 
			
		||||
    labels:
 | 
			
		||||
      - 'Breaking Change'
 | 
			
		||||
  - title: '🚀 Features'
 | 
			
		||||
    labels:
 | 
			
		||||
      - 'Type: Enhancement'
 | 
			
		||||
      - 'feature' # deprecated, new PRs shouldn't have this
 | 
			
		||||
  - title: '🐛 Bug Fixes'
 | 
			
		||||
    labels:
 | 
			
		||||
      - 'Type: Bug / Error'
 | 
			
		||||
      - 'fix' # deprecated, new PRs shouldn't have this
 | 
			
		||||
  - title: '🧰 Maintenance'
 | 
			
		||||
    label: 'Type: Other'
 | 
			
		||||
    labels:
 | 
			
		||||
      - 'Type: Other'
 | 
			
		||||
      - 'chore' # deprecated, new PRs shouldn't have this
 | 
			
		||||
  - title: '⚡️ Performance'
 | 
			
		||||
    labels:
 | 
			
		||||
      - 'Type: Performance'
 | 
			
		||||
  - title: '📚 Documentation'
 | 
			
		||||
    labels:
 | 
			
		||||
      - 'Area: Documentation'
 | 
			
		||||
change-template: '- $TITLE (#$NUMBER) @$AUTHOR'
 | 
			
		||||
sort-by: title
 | 
			
		||||
sort-direction: ascending
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										18
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
								
							@@ -62,8 +62,22 @@ jobs:
 | 
			
		||||
            ERROR_MESSAGE+=' `pnpm run --filter mermaid types:build-config`'
 | 
			
		||||
            ERROR_MESSAGE+=' on your local machine.'
 | 
			
		||||
            echo "::error title=Lint failure::${ERROR_MESSAGE}"
 | 
			
		||||
              # make sure to return an error exitcode so that GitHub actions shows a red-cross
 | 
			
		||||
              exit 1
 | 
			
		||||
            # make sure to return an error exitcode so that GitHub actions shows a red-cross
 | 
			
		||||
            exit 1
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
      - name: Verify no circular dependencies
 | 
			
		||||
        working-directory: ./packages/mermaid
 | 
			
		||||
        shell: bash
 | 
			
		||||
        run: |
 | 
			
		||||
          if ! pnpm run --filter mermaid checkCircle; then
 | 
			
		||||
            ERROR_MESSAGE='Circular dependency detected.'
 | 
			
		||||
            ERROR_MESSAGE+=' This should be fixed by removing the circular dependency.'
 | 
			
		||||
            ERROR_MESSAGE+=' Run `pnpm run --filter mermaid checkCircle` on your local machine'
 | 
			
		||||
            ERROR_MESSAGE+=' to see the circular dependency.'
 | 
			
		||||
            echo "::error title=Lint failure::${ERROR_MESSAGE}"
 | 
			
		||||
            # make sure to return an error exitcode so that GitHub actions shows a red-cross
 | 
			
		||||
            exit 1
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
      - name: Verify Docs
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -43,3 +43,6 @@ stats/
 | 
			
		||||
**/contributor-names.json
 | 
			
		||||
.pnpm-store
 | 
			
		||||
.nyc_output
 | 
			
		||||
 | 
			
		||||
demos/dev/**
 | 
			
		||||
!/demos/dev/example.html
 | 
			
		||||
 
 | 
			
		||||
@@ -26,9 +26,14 @@ Install required packages:
 | 
			
		||||
```bash
 | 
			
		||||
# npx is required for first install as volta support for pnpm is not added yet.
 | 
			
		||||
npx pnpm install
 | 
			
		||||
pnpm test
 | 
			
		||||
pnpm test # run unit tests
 | 
			
		||||
pnpm dev # starts a dev server
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Open <http://localhost:9000> in your browser after starting the dev server.
 | 
			
		||||
You can also duplicate the `example.html` file in `demos/dev`, rename it and add your own mermaid code to it.
 | 
			
		||||
That will be served at <http://localhost:9000/dev/your-file-name.html>.
 | 
			
		||||
 | 
			
		||||
### Docker
 | 
			
		||||
 | 
			
		||||
If you are using docker and docker-compose, you have self-documented `run` bash script, which is a convenient alias for docker-compose commands:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Mocked pie (picChart) diagram renderer
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { vi } from 'vitest';
 | 
			
		||||
 | 
			
		||||
export const draw = vi.fn().mockImplementation(() => {
 | 
			
		||||
  return '';
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  draw,
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										8
									
								
								__mocks__/pieRenderer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								__mocks__/pieRenderer.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Mocked pie (picChart) diagram renderer
 | 
			
		||||
 */
 | 
			
		||||
import { vi } from 'vitest';
 | 
			
		||||
 | 
			
		||||
const draw = vi.fn().mockImplementation(() => '');
 | 
			
		||||
 | 
			
		||||
export const renderer = { draw };
 | 
			
		||||
@@ -106,6 +106,7 @@
 | 
			
		||||
    "rects",
 | 
			
		||||
    "reda",
 | 
			
		||||
    "redmine",
 | 
			
		||||
    "regexes",
 | 
			
		||||
    "rehype",
 | 
			
		||||
    "roledescription",
 | 
			
		||||
    "rozhkov",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,89 +1,85 @@
 | 
			
		||||
import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts';
 | 
			
		||||
 | 
			
		||||
describe('Pie Chart', () => {
 | 
			
		||||
describe('pie chart', () => {
 | 
			
		||||
  it('should render a simple pie diagram', () => {
 | 
			
		||||
    imgSnapshotTest(
 | 
			
		||||
      `pie title Sports in Sweden
 | 
			
		||||
        "Bandy": 40
 | 
			
		||||
        "Ice-Hockey": 80
 | 
			
		||||
        "Football": 90
 | 
			
		||||
      `
 | 
			
		||||
    pie title Sports in Sweden
 | 
			
		||||
       "Bandy" : 40
 | 
			
		||||
       "Ice-Hockey" : 80
 | 
			
		||||
       "Football" : 90
 | 
			
		||||
      `,
 | 
			
		||||
      {}
 | 
			
		||||
    );
 | 
			
		||||
    cy.get('svg');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should render a simple pie diagram with long labels', () => {
 | 
			
		||||
    imgSnapshotTest(
 | 
			
		||||
      `pie title NETFLIX
 | 
			
		||||
        "Time spent looking for movie": 90
 | 
			
		||||
        "Time spent watching it": 10
 | 
			
		||||
      `
 | 
			
		||||
      pie title NETFLIX
 | 
			
		||||
         "Time spent looking for movie" : 90
 | 
			
		||||
         "Time spent watching it" : 10
 | 
			
		||||
        `,
 | 
			
		||||
      {}
 | 
			
		||||
    );
 | 
			
		||||
    cy.get('svg');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should render a simple pie diagram with capital letters for labels', () => {
 | 
			
		||||
    imgSnapshotTest(
 | 
			
		||||
      `pie title What Voldemort doesn't have?
 | 
			
		||||
        "FRIENDS": 2
 | 
			
		||||
        "FAMILY": 3
 | 
			
		||||
        "NOSE": 45
 | 
			
		||||
      `
 | 
			
		||||
      pie title What Voldemort doesn't have?
 | 
			
		||||
         "FRIENDS" : 2
 | 
			
		||||
         "FAMILY" : 3
 | 
			
		||||
         "NOSE" : 45
 | 
			
		||||
        `,
 | 
			
		||||
      {}
 | 
			
		||||
    );
 | 
			
		||||
    cy.get('svg');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should render a pie diagram when useMaxWidth is true (default)', () => {
 | 
			
		||||
    renderGraph(
 | 
			
		||||
      `
 | 
			
		||||
    pie title Sports in Sweden
 | 
			
		||||
       "Bandy" : 40
 | 
			
		||||
       "Ice-Hockey" : 80
 | 
			
		||||
       "Football" : 90
 | 
			
		||||
      `pie title Sports in Sweden
 | 
			
		||||
        "Bandy": 40
 | 
			
		||||
        "Ice-Hockey": 80
 | 
			
		||||
        "Football": 90
 | 
			
		||||
      `,
 | 
			
		||||
      { pie: { useMaxWidth: true } }
 | 
			
		||||
    );
 | 
			
		||||
    cy.get('svg').should((svg) => {
 | 
			
		||||
      expect(svg).to.have.attr('width', '100%');
 | 
			
		||||
      // expect(svg).to.have.attr('height');
 | 
			
		||||
      // const height = parseFloat(svg.attr('height'));
 | 
			
		||||
      // expect(height).to.eq(450);
 | 
			
		||||
      const style = svg.attr('style');
 | 
			
		||||
      expect(style).to.match(/^max-width: [\d.]+px;$/);
 | 
			
		||||
      const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join(''));
 | 
			
		||||
      expect(maxWidthValue).to.eq(984);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should render a pie diagram when useMaxWidth is false', () => {
 | 
			
		||||
    renderGraph(
 | 
			
		||||
      `
 | 
			
		||||
    pie title Sports in Sweden
 | 
			
		||||
       "Bandy" : 40
 | 
			
		||||
       "Ice-Hockey" : 80
 | 
			
		||||
       "Football" : 90
 | 
			
		||||
      `pie title Sports in Sweden
 | 
			
		||||
        "Bandy": 40
 | 
			
		||||
        "Ice-Hockey": 80
 | 
			
		||||
        "Football": 90
 | 
			
		||||
      `,
 | 
			
		||||
      { pie: { useMaxWidth: false } }
 | 
			
		||||
    );
 | 
			
		||||
    cy.get('svg').should((svg) => {
 | 
			
		||||
      // const height = parseFloat(svg.attr('height'));
 | 
			
		||||
      const width = parseFloat(svg.attr('width'));
 | 
			
		||||
      // expect(height).to.eq(450);
 | 
			
		||||
      expect(width).to.eq(984);
 | 
			
		||||
      expect(svg).to.not.have.attr('style');
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
  it('should render a pie diagram when textPosition is set', () => {
 | 
			
		||||
 | 
			
		||||
  it('should render a pie diagram when textPosition is setted', () => {
 | 
			
		||||
    imgSnapshotTest(
 | 
			
		||||
      `
 | 
			
		||||
        pie
 | 
			
		||||
          "Dogs": 50
 | 
			
		||||
          "Cats": 25
 | 
			
		||||
        `,
 | 
			
		||||
      `pie
 | 
			
		||||
        "Dogs": 50
 | 
			
		||||
        "Cats": 25
 | 
			
		||||
      `,
 | 
			
		||||
      { logLevel: 1, pie: { textPosition: 0.9 } }
 | 
			
		||||
    );
 | 
			
		||||
    cy.get('svg');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should render a pie diagram with showData', () => {
 | 
			
		||||
    imgSnapshotTest(
 | 
			
		||||
      `pie showData
 | 
			
		||||
        "Dogs": 50
 | 
			
		||||
        "Cats": 25
 | 
			
		||||
      `
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										34
									
								
								demos/dev/example.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								demos/dev/example.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
<!--Do not edit this file-->
 | 
			
		||||
<!--Duplicate this file to any name you like, run `pnpm dev`, open http://localhost:9000/dev/name.html to view-->
 | 
			
		||||
<html>
 | 
			
		||||
  <head>
 | 
			
		||||
    <title>Mermaid development page</title>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <pre id="diagram" class="mermaid">
 | 
			
		||||
graph TB
 | 
			
		||||
      a --> b
 | 
			
		||||
      a --> c
 | 
			
		||||
      b --> d
 | 
			
		||||
      c --> d
 | 
			
		||||
    </pre>
 | 
			
		||||
 | 
			
		||||
    <div id="dynamicDiagram"></div>
 | 
			
		||||
 | 
			
		||||
    <script type="module">
 | 
			
		||||
      import mermaid from '/mermaid.esm.mjs';
 | 
			
		||||
      mermaid.parseError = function (err, hash) {
 | 
			
		||||
        console.error('Mermaid error: ', err);
 | 
			
		||||
      };
 | 
			
		||||
      mermaid.initialize({
 | 
			
		||||
        startOnLoad: true,
 | 
			
		||||
        logLevel: 0,
 | 
			
		||||
      });
 | 
			
		||||
      const value = `graph TD\nHello --> World`;
 | 
			
		||||
      const el = document.getElementById('dynamicDiagram');
 | 
			
		||||
      const { svg } = await mermaid.render('dd', value);
 | 
			
		||||
      console.log(svg);
 | 
			
		||||
      el.innerHTML = svg;
 | 
			
		||||
    </script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
@@ -7,7 +7,6 @@
 | 
			
		||||
    <link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=" />
 | 
			
		||||
    <style>
 | 
			
		||||
      div.mermaid {
 | 
			
		||||
        /* font-family: 'trebuchet ms', verdana, arial; */
 | 
			
		||||
        font-family: 'Courier New', Courier, monospace !important;
 | 
			
		||||
      }
 | 
			
		||||
    </style>
 | 
			
		||||
@@ -17,37 +16,32 @@
 | 
			
		||||
    <h1>Pie chart demos</h1>
 | 
			
		||||
    <pre class="mermaid">
 | 
			
		||||
      pie title Pets adopted by volunteers
 | 
			
		||||
      accTitle: simple pie char demo
 | 
			
		||||
      accDescr: pie chart with 3 sections: dogs, cats, rats. Most are dogs.
 | 
			
		||||
    "Dogs" : 386
 | 
			
		||||
    "Cats" : 85
 | 
			
		||||
    "Rats" : 15
 | 
			
		||||
        accTitle: simple pie char demo
 | 
			
		||||
        accDescr: pie chart with 3 sections: dogs, cats, rats. Most are dogs.
 | 
			
		||||
        "Dogs": 386
 | 
			
		||||
        "Cats": 85
 | 
			
		||||
        "Rats": 15
 | 
			
		||||
    </pre>
 | 
			
		||||
 | 
			
		||||
    <hr />
 | 
			
		||||
    <pre class="mermaid">
 | 
			
		||||
    %%{init: {"pie": {"textPosition": 0.9}, "themeVariables": {"pieOuterStrokeWidth": "5px"}} }%%
 | 
			
		||||
    pie
 | 
			
		||||
      title Key elements in Product X
 | 
			
		||||
      %%{init: {"pie": {"textPosition": 0.9}, "themeVariables": {"pieOuterStrokeWidth": "5px"}}}%%
 | 
			
		||||
      pie
 | 
			
		||||
        title Key elements in Product X
 | 
			
		||||
        accTitle: Key elements in Product X
 | 
			
		||||
      accDescr: This is a pie chart showing the key elements in Product X.
 | 
			
		||||
      "Calcium" : 42.96
 | 
			
		||||
      "Potassium" : 50.05
 | 
			
		||||
      "Magnesium" : 10.01
 | 
			
		||||
      "Iron" :  5
 | 
			
		||||
        accDescr: This is a pie chart showing the key elements in Product X.
 | 
			
		||||
        "Calcium": 42.96
 | 
			
		||||
        "Potassium": 50.05
 | 
			
		||||
        "Magnesium": 10.01
 | 
			
		||||
        "Iron": 5
 | 
			
		||||
    </pre>
 | 
			
		||||
 | 
			
		||||
    <script type="module">
 | 
			
		||||
      import mermaid from './mermaid.esm.mjs';
 | 
			
		||||
      mermaid.initialize({
 | 
			
		||||
        theme: 'forest',
 | 
			
		||||
        // themeCSS: '.node rect { fill: red; }',
 | 
			
		||||
        logLevel: 3,
 | 
			
		||||
        securityLevel: 'loose',
 | 
			
		||||
        // flowchart: { curve: 'basis' },
 | 
			
		||||
        // gantt: { axisFormat: '%m/%d/%Y' },
 | 
			
		||||
        sequence: { actorMargin: 50 },
 | 
			
		||||
        // sequenceDiagram: { actorMargin: 300 } // deprecated
 | 
			
		||||
      });
 | 
			
		||||
    </script>
 | 
			
		||||
  </body>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
version: '3.9'
 | 
			
		||||
services:
 | 
			
		||||
  mermaid:
 | 
			
		||||
    image: node:18.17.0-alpine3.18
 | 
			
		||||
    image: node:18.17.1-alpine3.18
 | 
			
		||||
    stdin_open: true
 | 
			
		||||
    tty: true
 | 
			
		||||
    working_dir: /mermaid
 | 
			
		||||
@@ -17,7 +17,7 @@ services:
 | 
			
		||||
      - 9000:9000
 | 
			
		||||
      - 3333:3333
 | 
			
		||||
  cypress:
 | 
			
		||||
    image: cypress/included:12.17.2
 | 
			
		||||
    image: cypress/included:12.17.4
 | 
			
		||||
    stdin_open: true
 | 
			
		||||
    tty: true
 | 
			
		||||
    working_dir: /mermaid
 | 
			
		||||
 
 | 
			
		||||
@@ -70,7 +70,21 @@ pnpm test
 | 
			
		||||
 | 
			
		||||
The `test` script and others are in the top-level `package.json` file.
 | 
			
		||||
 | 
			
		||||
All tests should run successfully without any errors or failures. (You might see _lint_ or _formatting_ warnings; those are ok during this step.)
 | 
			
		||||
All tests should run successfully without any errors or failures. (You might see _lint_ or _formatting_ "warnings"; those are ok during this step.)
 | 
			
		||||
 | 
			
		||||
#### 4. Make your changes
 | 
			
		||||
 | 
			
		||||
Now you are ready to make your changes!
 | 
			
		||||
Edit whichever files in `src` as required.
 | 
			
		||||
 | 
			
		||||
#### 5. See your changes
 | 
			
		||||
 | 
			
		||||
Open <http://localhost:9000> in your browser, after starting the dev server.
 | 
			
		||||
There is a list of demos that can be used to see and test your changes.
 | 
			
		||||
 | 
			
		||||
If you need a specific diagram, you can duplicate the `example.html` file in `/demos/dev` and add your own mermaid code to your copy.
 | 
			
		||||
That will be served at <http://localhost:9000/dev/your-file-name.html>.
 | 
			
		||||
After making code changes, the dev server will rebuild the mermaid library. You will need to reload the browser page yourself to see the changes. (PRs for auto reload are welcome!)
 | 
			
		||||
 | 
			
		||||
### Docker
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -14,13 +14,13 @@
 | 
			
		||||
 | 
			
		||||
#### Defined in
 | 
			
		||||
 | 
			
		||||
[defaultConfig.ts:266](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L266)
 | 
			
		||||
[defaultConfig.ts:268](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L268)
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
### default
 | 
			
		||||
 | 
			
		||||
• `Const` **default**: `Partial`<`MermaidConfig`>
 | 
			
		||||
• `Const` **default**: `RequiredDeep`<`MermaidConfig`>
 | 
			
		||||
 | 
			
		||||
Default mermaid configuration options.
 | 
			
		||||
 | 
			
		||||
@@ -30,4 +30,4 @@ Non-JSON JS default values are listed in this file, e.g. functions, or
 | 
			
		||||
 | 
			
		||||
#### Defined in
 | 
			
		||||
 | 
			
		||||
[defaultConfig.ts:16](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L16)
 | 
			
		||||
[defaultConfig.ts:18](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L18)
 | 
			
		||||
 
 | 
			
		||||
@@ -14,8 +14,12 @@ It is a JavaScript based diagramming and charting tool that renders Markdown-ins
 | 
			
		||||
 | 
			
		||||
<img src="/header.png" alt="" />
 | 
			
		||||
 | 
			
		||||
<div class='badges'>
 | 
			
		||||
 | 
			
		||||
[](https://github.com/mermaid-js/mermaid/actions/workflows/build.yml) [](https://www.npmjs.com/package/mermaid) [](https://bundlephobia.com/package/mermaid) [](https://coveralls.io/github/mermaid-js/mermaid?branch=master) [](https://www.jsdelivr.com/package/npm/mermaid) [](https://www.npmjs.com/package/mermaid) [](https://join.slack.com/t/mermaid-talk/shared_invite/enQtNzc4NDIyNzk4OTAyLWVhYjQxOTI2OTg4YmE1ZmJkY2Y4MTU3ODliYmIwOTY3NDJlYjA0YjIyZTdkMDMyZTUwOGI0NjEzYmEwODcwOTE) [](https://twitter.com/mermaidjs_)
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- Mermaid book banner -->
 | 
			
		||||
 | 
			
		||||
[](https://mermaid-js.github.io/mermaid/landing/)
 | 
			
		||||
@@ -389,8 +393,12 @@ The above command generates files into the `dist` folder and publishes them to \
 | 
			
		||||
 | 
			
		||||
## Contributors
 | 
			
		||||
 | 
			
		||||
<div class='badges'>
 | 
			
		||||
 | 
			
		||||
[](https://github.com/mermaid-js/mermaid/issues?q=is%3Aissue+is%3Aopen+label%3A%22Good+first+issue%21%22) [](https://github.com/mermaid-js/mermaid/graphs/contributors) [](https://github.com/mermaid-js/mermaid/graphs/contributors)
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
Mermaid is a growing community and is always accepting new contributors. There's a lot of different ways to help out and we're always looking for extra hands! Look at [this issue](https://github.com/mermaid-js/mermaid/issues/866) if you want to know where to start helping out.
 | 
			
		||||
 | 
			
		||||
Detailed information about how to contribute can be found in the [contribution guide](https://github.com/mermaid-js/mermaid/blob/develop/CONTRIBUTING.md)
 | 
			
		||||
@@ -424,20 +432,14 @@ A quick note from Knut Sveidqvist:
 | 
			
		||||
_Mermaid was created by Knut Sveidqvist for easier documentation._
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
  #contributors + p,
 | 
			
		||||
  #about-mermaid + p + p + blockquote + img + p
 | 
			
		||||
  {
 | 
			
		||||
    display: flex
 | 
			
		||||
 .badges > p {
 | 
			
		||||
    display: flex;
 | 
			
		||||
  }
 | 
			
		||||
  .badges > p > a {
 | 
			
		||||
    margin: 0 0.5rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #contributors + p a,
 | 
			
		||||
  #about-mermaid + p + p + blockquote + img + p a
 | 
			
		||||
  {
 | 
			
		||||
    margin: 0 0.5rem
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .dark #VPContent > div > div > div.content > div > main > div > div > img
 | 
			
		||||
  {
 | 
			
		||||
  .dark #VPContent > div > div > div.content > div > main > div > div > img {
 | 
			
		||||
    filter: invert(1) hue-rotate(217deg)  contrast(0.72);
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -748,6 +748,48 @@ flowchart LR
 | 
			
		||||
  B1 --> B2
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### Limitation
 | 
			
		||||
 | 
			
		||||
If any of a subgraph's nodes are linked to the outside, subgraph direction will be ignored. Instead the subgraph will inherit the direction of the parent graph:
 | 
			
		||||
 | 
			
		||||
```mermaid-example
 | 
			
		||||
flowchart LR
 | 
			
		||||
    subgraph subgraph1
 | 
			
		||||
        direction TB
 | 
			
		||||
        top1[top] --> bottom1[bottom]
 | 
			
		||||
    end
 | 
			
		||||
    subgraph subgraph2
 | 
			
		||||
        direction TB
 | 
			
		||||
        top2[top] --> bottom2[bottom]
 | 
			
		||||
    end
 | 
			
		||||
    %% ^ These subgraphs are identical, except for the links to them:
 | 
			
		||||
 | 
			
		||||
    %% Link *to* subgraph1: subgraph1 direction is mantained
 | 
			
		||||
    outside --> subgraph1
 | 
			
		||||
    %% Link *within* subgraph2:
 | 
			
		||||
    %% subgraph2 inherits the direction of the top-level graph (LR)
 | 
			
		||||
    outside ---> top2
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```mermaid
 | 
			
		||||
flowchart LR
 | 
			
		||||
    subgraph subgraph1
 | 
			
		||||
        direction TB
 | 
			
		||||
        top1[top] --> bottom1[bottom]
 | 
			
		||||
    end
 | 
			
		||||
    subgraph subgraph2
 | 
			
		||||
        direction TB
 | 
			
		||||
        top2[top] --> bottom2[bottom]
 | 
			
		||||
    end
 | 
			
		||||
    %% ^ These subgraphs are identical, except for the links to them:
 | 
			
		||||
 | 
			
		||||
    %% Link *to* subgraph1: subgraph1 direction is mantained
 | 
			
		||||
    outside --> subgraph1
 | 
			
		||||
    %% Link *within* subgraph2:
 | 
			
		||||
    %% subgraph2 inherits the direction of the top-level graph (LR)
 | 
			
		||||
    outside ---> top2
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Markdown Strings
 | 
			
		||||
 | 
			
		||||
The "Markdown Strings" feature enhances flowcharts and mind maps by offering a more versatile string type, which supports text formatting options such as bold and italics, and automatically wraps text within labels.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								package.json
									
									
									
									
									
								
							@@ -4,7 +4,7 @@
 | 
			
		||||
  "version": "10.2.4",
 | 
			
		||||
  "description": "Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.",
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "packageManager": "pnpm@8.6.11",
 | 
			
		||||
  "packageManager": "pnpm@8.6.12",
 | 
			
		||||
  "keywords": [
 | 
			
		||||
    "diagram",
 | 
			
		||||
    "markdown",
 | 
			
		||||
@@ -78,15 +78,15 @@
 | 
			
		||||
    "@types/rollup-plugin-visualizer": "^4.2.1",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^5.59.0",
 | 
			
		||||
    "@typescript-eslint/parser": "^5.59.0",
 | 
			
		||||
    "@vitest/coverage-v8": "^0.33.0",
 | 
			
		||||
    "@vitest/spy": "^0.33.0",
 | 
			
		||||
    "@vitest/ui": "^0.33.0",
 | 
			
		||||
    "@vitest/coverage-v8": "^0.34.0",
 | 
			
		||||
    "@vitest/spy": "^0.34.0",
 | 
			
		||||
    "@vitest/ui": "^0.34.0",
 | 
			
		||||
    "ajv": "^8.12.0",
 | 
			
		||||
    "concurrently": "^8.0.1",
 | 
			
		||||
    "cors": "^2.8.5",
 | 
			
		||||
    "cypress": "^12.10.0",
 | 
			
		||||
    "cypress-image-snapshot": "^4.0.1",
 | 
			
		||||
    "esbuild": "^0.18.0",
 | 
			
		||||
    "esbuild": "^0.19.0",
 | 
			
		||||
    "eslint": "^8.39.0",
 | 
			
		||||
    "eslint-config-prettier": "^8.8.0",
 | 
			
		||||
    "eslint-plugin-cypress": "^2.13.2",
 | 
			
		||||
@@ -119,10 +119,10 @@
 | 
			
		||||
    "typescript": "^5.1.3",
 | 
			
		||||
    "vite": "^4.3.9",
 | 
			
		||||
    "vite-plugin-istanbul": "^4.1.0",
 | 
			
		||||
    "vitest": "^0.33.0"
 | 
			
		||||
    "vitest": "^0.34.0"
 | 
			
		||||
  },
 | 
			
		||||
  "volta": {
 | 
			
		||||
    "node": "18.17.0"
 | 
			
		||||
    "node": "18.17.1"
 | 
			
		||||
  },
 | 
			
		||||
  "nyc": {
 | 
			
		||||
    "report-dir": "coverage/cypress"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										22
									
								
								packages/mermaid/.madgerc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								packages/mermaid/.madgerc
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
{
 | 
			
		||||
  "detectiveOptions": {
 | 
			
		||||
    "ts": {
 | 
			
		||||
      "skipTypeImports": true
 | 
			
		||||
    },
 | 
			
		||||
    "es6": {
 | 
			
		||||
      "skipTypeImports": true
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "fileExtensions": [
 | 
			
		||||
    "js",
 | 
			
		||||
    "ts"
 | 
			
		||||
  ],
 | 
			
		||||
  "excludeRegExp": [
 | 
			
		||||
    "node_modules",
 | 
			
		||||
    "docs",
 | 
			
		||||
    "vitepress",
 | 
			
		||||
    "detector",
 | 
			
		||||
    "Detector"
 | 
			
		||||
  ],
 | 
			
		||||
  "tsConfig": "./tsconfig.json"
 | 
			
		||||
}
 | 
			
		||||
@@ -24,6 +24,7 @@
 | 
			
		||||
  ],
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "clean": "rimraf dist",
 | 
			
		||||
    "dev": "pnpm -w dev",
 | 
			
		||||
    "docs:code": "typedoc src/defaultConfig.ts src/config.ts src/mermaidAPI.ts && prettier --write ./src/docs/config/setup",
 | 
			
		||||
    "docs:build": "rimraf ../../docs && pnpm docs:spellcheck && pnpm docs:code && ts-node-esm scripts/docs.cli.mts",
 | 
			
		||||
    "docs:verify": "pnpm docs:spellcheck && pnpm docs:code && ts-node-esm scripts/docs.cli.mts --verify",
 | 
			
		||||
@@ -37,6 +38,7 @@
 | 
			
		||||
    "docs:verify-version": "ts-node-esm scripts/update-release-version.mts --verify",
 | 
			
		||||
    "types:build-config": "ts-node-esm --transpileOnly scripts/create-types-from-json-schema.mts",
 | 
			
		||||
    "types:verify-config": "ts-node-esm scripts/create-types-from-json-schema.mts --verify",
 | 
			
		||||
    "checkCircle": "npx madge --circular ./src",
 | 
			
		||||
    "release": "pnpm build",
 | 
			
		||||
    "prepublishOnly": "cpy '../../README.*' ./ --cwd=. && pnpm -w run build"
 | 
			
		||||
  },
 | 
			
		||||
@@ -83,7 +85,9 @@
 | 
			
		||||
    "@types/cytoscape": "^3.19.9",
 | 
			
		||||
    "@types/d3": "^7.4.0",
 | 
			
		||||
    "@types/d3-sankey": "^0.12.1",
 | 
			
		||||
    "@types/d3-scale": "^4.0.3",
 | 
			
		||||
    "@types/d3-selection": "^3.0.5",
 | 
			
		||||
    "@types/d3-shape": "^3.1.1",
 | 
			
		||||
    "@types/dompurify": "^3.0.2",
 | 
			
		||||
    "@types/jsdom": "^21.1.1",
 | 
			
		||||
    "@types/lodash-es": "^4.17.7",
 | 
			
		||||
@@ -112,6 +116,7 @@
 | 
			
		||||
    "remark-gfm": "^3.0.1",
 | 
			
		||||
    "rimraf": "^5.0.0",
 | 
			
		||||
    "start-server-and-test": "^2.0.0",
 | 
			
		||||
    "type-fest": "^4.1.0",
 | 
			
		||||
    "typedoc": "^0.24.5",
 | 
			
		||||
    "typedoc-plugin-markdown": "^3.15.2",
 | 
			
		||||
    "typescript": "^5.0.4",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,32 +1,36 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
/* eslint-disable @typescript-eslint/no-explicit-any */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @function assignWithDepth Extends the functionality of {@link ObjectConstructor.assign} with the
 | 
			
		||||
 * assignWithDepth Extends the functionality of {@link ObjectConstructor.assign} with the
 | 
			
		||||
 *   ability to merge arbitrary-depth objects For each key in src with path `k` (recursively)
 | 
			
		||||
 *   performs an Object.assign(dst[`k`], src[`k`]) with a slight change from the typical handling of
 | 
			
		||||
 *   undefined for dst[`k`]: instead of raising an error, dst[`k`] is auto-initialized to {} and
 | 
			
		||||
 *   undefined for dst[`k`]: instead of raising an error, dst[`k`] is auto-initialized to `{}` and
 | 
			
		||||
 *   effectively merged with src[`k`]<p> Additionally, dissimilar types will not clobber unless the
 | 
			
		||||
 *   config.clobber parameter === true. Example:
 | 
			
		||||
 *
 | 
			
		||||
 *   ```js
 | 
			
		||||
 *   let config_0 = { foo: { bar: 'bar' }, bar: 'foo' };
 | 
			
		||||
 *   let config_1 = { foo: 'foo', bar: 'bar' };
 | 
			
		||||
 *   let result = assignWithDepth(config_0, config_1);
 | 
			
		||||
 *   console.log(result);
 | 
			
		||||
 *   //-> result: { foo: { bar: 'bar' }, bar: 'bar' }
 | 
			
		||||
 *   ```
 | 
			
		||||
 * ```
 | 
			
		||||
 * const config_0 = { foo: { bar: 'bar' }, bar: 'foo' };
 | 
			
		||||
 * const config_1 = { foo: 'foo', bar: 'bar' };
 | 
			
		||||
 * const result = assignWithDepth(config_0, config_1);
 | 
			
		||||
 * console.log(result);
 | 
			
		||||
 * //-> result: { foo: { bar: 'bar' }, bar: 'bar' }
 | 
			
		||||
 * ```
 | 
			
		||||
 *
 | 
			
		||||
 *   Traditional Object.assign would have clobbered foo in config_0 with foo in config_1. If src is a
 | 
			
		||||
 *   destructured array of objects and dst is not an array, assignWithDepth will apply each element
 | 
			
		||||
 *   of src to dst in order.
 | 
			
		||||
 * @param {any} dst - The destination of the merge
 | 
			
		||||
 * @param {any} src - The source object(s) to merge into destination
 | 
			
		||||
 * @param {{ depth: number; clobber: boolean }} [config] - Depth: depth
 | 
			
		||||
 *   to traverse within src and dst for merging - clobber: should dissimilar types clobber (default:
 | 
			
		||||
 *   { depth: 2, clobber: false }). Default is `{ depth: 2, clobber: false }`
 | 
			
		||||
 * @returns {any}
 | 
			
		||||
 * @param dst - The destination of the merge
 | 
			
		||||
 * @param src - The source object(s) to merge into destination
 | 
			
		||||
 * @param config -
 | 
			
		||||
 * * depth: depth to traverse within src and dst for merging
 | 
			
		||||
 * * clobber: should dissimilar types clobber
 | 
			
		||||
 */
 | 
			
		||||
const assignWithDepth = function (dst, src, config) {
 | 
			
		||||
  const { depth, clobber } = Object.assign({ depth: 2, clobber: false }, config);
 | 
			
		||||
const assignWithDepth = (
 | 
			
		||||
  dst: any,
 | 
			
		||||
  src: any,
 | 
			
		||||
  { depth = 2, clobber = false }: { depth?: number; clobber?: boolean } = {}
 | 
			
		||||
): any => {
 | 
			
		||||
  const config: { depth: number; clobber: boolean } = { depth, clobber };
 | 
			
		||||
  if (Array.isArray(src) && !Array.isArray(dst)) {
 | 
			
		||||
    src.forEach((s) => assignWithDepth(dst, s, config));
 | 
			
		||||
    return dst;
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
import type { RequiredDeep } from 'type-fest';
 | 
			
		||||
 | 
			
		||||
import theme from './themes/index.js';
 | 
			
		||||
import { type MermaidConfig } from './config.type.js';
 | 
			
		||||
import type { MermaidConfig } from './config.type.js';
 | 
			
		||||
 | 
			
		||||
// Uses our custom Vite jsonSchemaPlugin to load only the default values from
 | 
			
		||||
// our JSON Schema
 | 
			
		||||
@@ -13,7 +15,7 @@ import defaultConfigJson from './schemas/config.schema.yaml?only-defaults=true';
 | 
			
		||||
 * Non-JSON JS default values are listed in this file, e.g. functions, or
 | 
			
		||||
 * `undefined` (explicitly set so that `configKeys` finds them).
 | 
			
		||||
 */
 | 
			
		||||
const config: Partial<MermaidConfig> = {
 | 
			
		||||
const config: RequiredDeep<MermaidConfig> = {
 | 
			
		||||
  ...defaultConfigJson,
 | 
			
		||||
  // Set, even though they're `undefined` so that `configKeys` finds these keys
 | 
			
		||||
  // TODO: Should we replace these with `null` so that they can go in the JSON Schema?
 | 
			
		||||
@@ -232,7 +234,7 @@ const config: Partial<MermaidConfig> = {
 | 
			
		||||
  },
 | 
			
		||||
  pie: {
 | 
			
		||||
    ...defaultConfigJson.pie,
 | 
			
		||||
    useWidth: undefined,
 | 
			
		||||
    useWidth: 984,
 | 
			
		||||
  },
 | 
			
		||||
  requirement: {
 | 
			
		||||
    ...defaultConfigJson.requirement,
 | 
			
		||||
 
 | 
			
		||||
@@ -6,14 +6,10 @@ import type {
 | 
			
		||||
  DiagramLoader,
 | 
			
		||||
  ExternalDiagramDefinition,
 | 
			
		||||
} from './types.js';
 | 
			
		||||
import { frontMatterRegex } from './frontmatter.js';
 | 
			
		||||
import { getDiagram, registerDiagram } from './diagramAPI.js';
 | 
			
		||||
import { anyCommentRegex, directiveRegex, frontMatterRegex } from './regexes.js';
 | 
			
		||||
import { UnknownDiagramError } from '../errors.js';
 | 
			
		||||
 | 
			
		||||
const directive = /%{2}{\s*(?:(\w+)\s*:|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi;
 | 
			
		||||
const anyComment = /\s*%%.*\n/gm;
 | 
			
		||||
 | 
			
		||||
const detectors: Record<string, DetectorRecord> = {};
 | 
			
		||||
export const detectors: Record<string, DetectorRecord> = {};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Detects the type of the graph text.
 | 
			
		||||
@@ -38,7 +34,10 @@ const detectors: Record<string, DetectorRecord> = {};
 | 
			
		||||
 * @returns A graph definition key
 | 
			
		||||
 */
 | 
			
		||||
export const detectType = function (text: string, config?: MermaidConfig): string {
 | 
			
		||||
  text = text.replace(frontMatterRegex, '').replace(directive, '').replace(anyComment, '\n');
 | 
			
		||||
  text = text
 | 
			
		||||
    .replace(frontMatterRegex, '')
 | 
			
		||||
    .replace(directiveRegex, '')
 | 
			
		||||
    .replace(anyCommentRegex, '\n');
 | 
			
		||||
  for (const [key, { detector }] of Object.entries(detectors)) {
 | 
			
		||||
    const diagram = detector(text, config);
 | 
			
		||||
    if (diagram) {
 | 
			
		||||
@@ -70,39 +69,6 @@ export const registerLazyLoadedDiagrams = (...diagrams: ExternalDiagramDefinitio
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const loadRegisteredDiagrams = async () => {
 | 
			
		||||
  log.debug(`Loading registered diagrams`);
 | 
			
		||||
  // Load all lazy loaded diagrams in parallel
 | 
			
		||||
  const results = await Promise.allSettled(
 | 
			
		||||
    Object.entries(detectors).map(async ([key, { detector, loader }]) => {
 | 
			
		||||
      if (loader) {
 | 
			
		||||
        try {
 | 
			
		||||
          getDiagram(key);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          try {
 | 
			
		||||
            // Register diagram if it is not already registered
 | 
			
		||||
            const { diagram, id } = await loader();
 | 
			
		||||
            registerDiagram(id, diagram, detector);
 | 
			
		||||
          } catch (err) {
 | 
			
		||||
            // Remove failed diagram from detectors
 | 
			
		||||
            log.error(`Failed to load external diagram with key ${key}. Removing from detectors.`);
 | 
			
		||||
            delete detectors[key];
 | 
			
		||||
            throw err;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  );
 | 
			
		||||
  const failed = results.filter((result) => result.status === 'rejected');
 | 
			
		||||
  if (failed.length > 0) {
 | 
			
		||||
    log.error(`Failed to load ${failed.length} external diagrams`);
 | 
			
		||||
    for (const res of failed) {
 | 
			
		||||
      log.error(res);
 | 
			
		||||
    }
 | 
			
		||||
    throw new Error(`Failed to load ${failed.length} external diagrams`);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const addDetector = (key: string, detector: DiagramDetector, loader?: DiagramLoader) => {
 | 
			
		||||
  if (detectors[key]) {
 | 
			
		||||
    log.error(`Detector with key ${key} already exists`);
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ import er from '../diagrams/er/erDetector.js';
 | 
			
		||||
import git from '../diagrams/git/gitGraphDetector.js';
 | 
			
		||||
import gantt from '../diagrams/gantt/ganttDetector.js';
 | 
			
		||||
import { info } from '../diagrams/info/infoDetector.js';
 | 
			
		||||
import pie from '../diagrams/pie/pieDetector.js';
 | 
			
		||||
import { pie } from '../diagrams/pie/pieDetector.js';
 | 
			
		||||
import quadrantChart from '../diagrams/quadrant-chart/quadrantDetector.js';
 | 
			
		||||
import requirement from '../diagrams/requirement/requirementDetector.js';
 | 
			
		||||
import sequence from '../diagrams/sequence/sequenceDetector.js';
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import { getConfig as _getConfig } from '../config.js';
 | 
			
		||||
import { sanitizeText as _sanitizeText } from '../diagrams/common/common.js';
 | 
			
		||||
import { setupGraphViewbox as _setupGraphViewbox } from '../setupGraphViewbox.js';
 | 
			
		||||
import { addStylesForDiagram } from '../styles.js';
 | 
			
		||||
import { DiagramDefinition, DiagramDetector } from './types.js';
 | 
			
		||||
import type { DiagramDefinition, DiagramDetector } from './types.js';
 | 
			
		||||
import * as _commonDb from '../commonDb.js';
 | 
			
		||||
import { parseDirective as _parseDirective } from '../directiveUtils.js';
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,8 @@
 | 
			
		||||
import { DiagramDB } from './types.js';
 | 
			
		||||
import type { DiagramDB } from './types.js';
 | 
			
		||||
import { frontMatterRegex } from './regexes.js';
 | 
			
		||||
// The "* as yaml" part is necessary for tree-shaking
 | 
			
		||||
import * as yaml from 'js-yaml';
 | 
			
		||||
 | 
			
		||||
// Match Jekyll-style front matter blocks (https://jekyllrb.com/docs/front-matter/).
 | 
			
		||||
// Based on regex used by Jekyll: https://github.com/jekyll/jekyll/blob/6dd3cc21c40b98054851846425af06c64f9fb466/lib/jekyll/document.rb#L10
 | 
			
		||||
// Note that JS doesn't support the "\A" anchor, which means we can't use
 | 
			
		||||
// multiline mode.
 | 
			
		||||
// Relevant YAML spec: https://yaml.org/spec/1.2.2/#914-explicit-documents
 | 
			
		||||
export const frontMatterRegex = /^-{3}\s*[\n\r](.*?)[\n\r]-{3}\s*[\n\r]+/s;
 | 
			
		||||
 | 
			
		||||
type FrontMatterMetadata = {
 | 
			
		||||
  title?: string;
 | 
			
		||||
  // Allows custom display modes. Currently used for compact mode in gantt charts.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										36
									
								
								packages/mermaid/src/diagram-api/loadDiagram.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								packages/mermaid/src/diagram-api/loadDiagram.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
import { log } from '../logger.js';
 | 
			
		||||
import { detectors } from './detectType.js';
 | 
			
		||||
import { getDiagram, registerDiagram } from './diagramAPI.js';
 | 
			
		||||
 | 
			
		||||
export const loadRegisteredDiagrams = async () => {
 | 
			
		||||
  log.debug(`Loading registered diagrams`);
 | 
			
		||||
  // Load all lazy loaded diagrams in parallel
 | 
			
		||||
  const results = await Promise.allSettled(
 | 
			
		||||
    Object.entries(detectors).map(async ([key, { detector, loader }]) => {
 | 
			
		||||
      if (loader) {
 | 
			
		||||
        try {
 | 
			
		||||
          getDiagram(key);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          try {
 | 
			
		||||
            // Register diagram if it is not already registered
 | 
			
		||||
            const { diagram, id } = await loader();
 | 
			
		||||
            registerDiagram(id, diagram, detector);
 | 
			
		||||
          } catch (err) {
 | 
			
		||||
            // Remove failed diagram from detectors
 | 
			
		||||
            log.error(`Failed to load external diagram with key ${key}. Removing from detectors.`);
 | 
			
		||||
            delete detectors[key];
 | 
			
		||||
            throw err;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  );
 | 
			
		||||
  const failed = results.filter((result) => result.status === 'rejected');
 | 
			
		||||
  if (failed.length > 0) {
 | 
			
		||||
    log.error(`Failed to load ${failed.length} external diagrams`);
 | 
			
		||||
    for (const res of failed) {
 | 
			
		||||
      log.error(res);
 | 
			
		||||
    }
 | 
			
		||||
    throw new Error(`Failed to load ${failed.length} external diagrams`);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										11
									
								
								packages/mermaid/src/diagram-api/regexes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								packages/mermaid/src/diagram-api/regexes.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
// Match Jekyll-style front matter blocks (https://jekyllrb.com/docs/front-matter/).
 | 
			
		||||
// Based on regex used by Jekyll: https://github.com/jekyll/jekyll/blob/6dd3cc21c40b98054851846425af06c64f9fb466/lib/jekyll/document.rb#L10
 | 
			
		||||
// Note that JS doesn't support the "\A" anchor, which means we can't use
 | 
			
		||||
// multiline mode.
 | 
			
		||||
// Relevant YAML spec: https://yaml.org/spec/1.2.2/#914-explicit-documents
 | 
			
		||||
export const frontMatterRegex = /^-{3}\s*[\n\r](.*?)[\n\r]-{3}\s*[\n\r]+/s;
 | 
			
		||||
 | 
			
		||||
export const directiveRegex =
 | 
			
		||||
  /%{2}{\s*(?:(\w+)\s*:|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi;
 | 
			
		||||
 | 
			
		||||
export const anyCommentRegex = /\s*%%.*\n/gm;
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { Diagram } from '../Diagram.js';
 | 
			
		||||
import type { MermaidConfig } from '../config.type.js';
 | 
			
		||||
import type { BaseDiagramConfig, MermaidConfig } from '../config.type.js';
 | 
			
		||||
import type * as d3 from 'd3';
 | 
			
		||||
 | 
			
		||||
export interface InjectUtils {
 | 
			
		||||
@@ -16,11 +16,19 @@ export interface InjectUtils {
 | 
			
		||||
 * Generic Diagram DB that may apply to any diagram type.
 | 
			
		||||
 */
 | 
			
		||||
export interface DiagramDB {
 | 
			
		||||
  // config
 | 
			
		||||
  getConfig?: () => BaseDiagramConfig | undefined;
 | 
			
		||||
 | 
			
		||||
  // db
 | 
			
		||||
  clear?: () => void;
 | 
			
		||||
  setDiagramTitle?: (title: string) => void;
 | 
			
		||||
  setDisplayMode?: (title: string) => void;
 | 
			
		||||
  getDiagramTitle?: () => string;
 | 
			
		||||
  setAccTitle?: (title: string) => void;
 | 
			
		||||
  getAccTitle?: () => string;
 | 
			
		||||
  setAccDescription?: (describetion: string) => void;
 | 
			
		||||
  getAccDescription?: () => string;
 | 
			
		||||
 | 
			
		||||
  setDisplayMode?: (title: string) => void;
 | 
			
		||||
  bindFunctions?: (element: Element) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import common from '../common/common.js';
 | 
			
		||||
import * as svgDrawCommon from '../common/svgDrawCommon';
 | 
			
		||||
import * as svgDrawCommon from '../common/svgDrawCommon.js';
 | 
			
		||||
import { sanitizeUrl } from '@braintree/sanitize-url';
 | 
			
		||||
 | 
			
		||||
export const drawRect = function (elem, rectData) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,15 @@
 | 
			
		||||
import { sanitizeText, removeScript, parseGenericTypes } from './common.js';
 | 
			
		||||
 | 
			
		||||
describe('when securityLevel is antiscript, all script must be removed', function () {
 | 
			
		||||
describe('when securityLevel is antiscript, all script must be removed', () => {
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {string} original The original text
 | 
			
		||||
   * @param {string} result The expected sanitized text
 | 
			
		||||
   * @param original - The original text
 | 
			
		||||
   * @param result - The expected sanitized text
 | 
			
		||||
   */
 | 
			
		||||
  function compareRemoveScript(original, result) {
 | 
			
		||||
  function compareRemoveScript(original: string, result: string) {
 | 
			
		||||
    expect(removeScript(original).trim()).toEqual(result);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  it('should remove all script block, script inline.', function () {
 | 
			
		||||
  it('should remove all script block, script inline.', () => {
 | 
			
		||||
    const labelString = `1
 | 
			
		||||
		Act1: Hello 1<script src="http://abc.com/script1.js"></script>1
 | 
			
		||||
		<b>Act2</b>:
 | 
			
		||||
@@ -25,7 +25,7 @@ describe('when securityLevel is antiscript, all script must be removed', functio
 | 
			
		||||
    compareRemoveScript(labelString, exactlyString);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should remove all javascript urls', function () {
 | 
			
		||||
  it('should remove all javascript urls', () => {
 | 
			
		||||
    compareRemoveScript(
 | 
			
		||||
      `This is a <a href="javascript:runHijackingScript();">clean link</a> + <a href="javascript:runHijackingScript();">clean link</a>
 | 
			
		||||
  and <a href="javascript:bipassedMining();">me too</a>`,
 | 
			
		||||
@@ -34,11 +34,11 @@ describe('when securityLevel is antiscript, all script must be removed', functio
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should detect malicious images', function () {
 | 
			
		||||
  it('should detect malicious images', () => {
 | 
			
		||||
    compareRemoveScript(`<img onerror="alert('hello');">`, `<img>`);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should detect iframes', function () {
 | 
			
		||||
  it('should detect iframes', () => {
 | 
			
		||||
    compareRemoveScript(
 | 
			
		||||
      `<iframe src="http://abc.com/script1.js"></iframe>
 | 
			
		||||
    <iframe src="http://example.com/iframeexample"></iframe>`,
 | 
			
		||||
@@ -47,8 +47,8 @@ describe('when securityLevel is antiscript, all script must be removed', functio
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('Sanitize text', function () {
 | 
			
		||||
  it('should remove script tag', function () {
 | 
			
		||||
describe('Sanitize text', () => {
 | 
			
		||||
  it('should remove script tag', () => {
 | 
			
		||||
    const maliciousStr = 'javajavascript:script:alert(1)';
 | 
			
		||||
    const result = sanitizeText(maliciousStr, {
 | 
			
		||||
      securityLevel: 'strict',
 | 
			
		||||
@@ -58,8 +58,8 @@ describe('Sanitize text', function () {
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('generic parser', function () {
 | 
			
		||||
  it('should parse generic types', function () {
 | 
			
		||||
describe('generic parser', () => {
 | 
			
		||||
  it('should parse generic types', () => {
 | 
			
		||||
    expect(parseGenericTypes('test~T~')).toEqual('test<T>');
 | 
			
		||||
    expect(parseGenericTypes('test~Array~Array~string~~~')).toEqual('test<Array<Array<string>>>');
 | 
			
		||||
    expect(parseGenericTypes('test~Array~Array~string[]~~~')).toEqual(
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import DOMPurify from 'dompurify';
 | 
			
		||||
import { MermaidConfig } from '../../config.type.js';
 | 
			
		||||
 | 
			
		||||
// Remove and ignore br:s
 | 
			
		||||
export const lineBreakRegex = /<br\s*\/?>/gi;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										58
									
								
								packages/mermaid/src/diagrams/common/commonTypes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								packages/mermaid/src/diagrams/common/commonTypes.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
export interface RectData {
 | 
			
		||||
  x: number;
 | 
			
		||||
  y: number;
 | 
			
		||||
  fill: string;
 | 
			
		||||
  width: number;
 | 
			
		||||
  height: number;
 | 
			
		||||
  stroke: string;
 | 
			
		||||
  class?: string;
 | 
			
		||||
  color?: string;
 | 
			
		||||
  rx?: number;
 | 
			
		||||
  ry?: number;
 | 
			
		||||
  attrs?: Record<string, string | number>;
 | 
			
		||||
  anchor?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Bound {
 | 
			
		||||
  startx: number;
 | 
			
		||||
  stopx: number;
 | 
			
		||||
  starty: number;
 | 
			
		||||
  stopy: number;
 | 
			
		||||
  fill: string;
 | 
			
		||||
  stroke: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface TextData {
 | 
			
		||||
  x: number;
 | 
			
		||||
  y: number;
 | 
			
		||||
  anchor: string;
 | 
			
		||||
  text: string;
 | 
			
		||||
  textMargin: number;
 | 
			
		||||
  class?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface TextObject {
 | 
			
		||||
  x: number;
 | 
			
		||||
  y: number;
 | 
			
		||||
  width: number;
 | 
			
		||||
  height: number;
 | 
			
		||||
  fill?: string;
 | 
			
		||||
  anchor?: string;
 | 
			
		||||
  'text-anchor': string;
 | 
			
		||||
  style: string;
 | 
			
		||||
  textMargin: number;
 | 
			
		||||
  rx: number;
 | 
			
		||||
  ry: number;
 | 
			
		||||
  tspan: boolean;
 | 
			
		||||
  valign?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type D3RectElement = d3.Selection<SVGRectElement, unknown, Element | null, unknown>;
 | 
			
		||||
 | 
			
		||||
export type D3UseElement = d3.Selection<SVGUseElement, unknown, Element | null, unknown>;
 | 
			
		||||
 | 
			
		||||
export type D3ImageElement = d3.Selection<SVGImageElement, unknown, Element | null, unknown>;
 | 
			
		||||
 | 
			
		||||
export type D3TextElement = d3.Selection<SVGTextElement, unknown, Element | null, unknown>;
 | 
			
		||||
 | 
			
		||||
export type D3TSpanElement = d3.Selection<SVGTSpanElement, unknown, Element | null, unknown>;
 | 
			
		||||
@@ -1,114 +0,0 @@
 | 
			
		||||
import { sanitizeUrl } from '@braintree/sanitize-url';
 | 
			
		||||
 | 
			
		||||
export const drawRect = function (elem, rectData) {
 | 
			
		||||
  const rectElem = elem.append('rect');
 | 
			
		||||
  rectElem.attr('x', rectData.x);
 | 
			
		||||
  rectElem.attr('y', rectData.y);
 | 
			
		||||
  rectElem.attr('fill', rectData.fill);
 | 
			
		||||
  rectElem.attr('stroke', rectData.stroke);
 | 
			
		||||
  rectElem.attr('width', rectData.width);
 | 
			
		||||
  rectElem.attr('height', rectData.height);
 | 
			
		||||
  rectElem.attr('rx', rectData.rx);
 | 
			
		||||
  rectElem.attr('ry', rectData.ry);
 | 
			
		||||
 | 
			
		||||
  if (rectData.attrs !== 'undefined' && rectData.attrs !== null) {
 | 
			
		||||
    for (let attrKey in rectData.attrs) {
 | 
			
		||||
      rectElem.attr(attrKey, rectData.attrs[attrKey]);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (rectData.class !== 'undefined') {
 | 
			
		||||
    rectElem.attr('class', rectData.class);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return rectElem;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Draws a background rectangle
 | 
			
		||||
 *
 | 
			
		||||
 * @param {any} elem Diagram (reference for bounds)
 | 
			
		||||
 * @param {any} bounds Shape of the rectangle
 | 
			
		||||
 */
 | 
			
		||||
export const drawBackgroundRect = function (elem, bounds) {
 | 
			
		||||
  const rectElem = drawRect(elem, {
 | 
			
		||||
    x: bounds.startx,
 | 
			
		||||
    y: bounds.starty,
 | 
			
		||||
    width: bounds.stopx - bounds.startx,
 | 
			
		||||
    height: bounds.stopy - bounds.starty,
 | 
			
		||||
    fill: bounds.fill,
 | 
			
		||||
    stroke: bounds.stroke,
 | 
			
		||||
    class: 'rect',
 | 
			
		||||
  });
 | 
			
		||||
  rectElem.lower();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const drawText = function (elem, textData) {
 | 
			
		||||
  // Remove and ignore br:s
 | 
			
		||||
  const nText = textData.text.replace(/<br\s*\/?>/gi, ' ');
 | 
			
		||||
 | 
			
		||||
  const textElem = elem.append('text');
 | 
			
		||||
  textElem.attr('x', textData.x);
 | 
			
		||||
  textElem.attr('y', textData.y);
 | 
			
		||||
  textElem.attr('class', 'legend');
 | 
			
		||||
 | 
			
		||||
  textElem.style('text-anchor', textData.anchor);
 | 
			
		||||
 | 
			
		||||
  if (textData.class !== undefined) {
 | 
			
		||||
    textElem.attr('class', textData.class);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const span = textElem.append('tspan');
 | 
			
		||||
  span.attr('x', textData.x + textData.textMargin * 2);
 | 
			
		||||
  span.text(nText);
 | 
			
		||||
 | 
			
		||||
  return textElem;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const drawImage = function (elem, x, y, link) {
 | 
			
		||||
  const imageElem = elem.append('image');
 | 
			
		||||
  imageElem.attr('x', x);
 | 
			
		||||
  imageElem.attr('y', y);
 | 
			
		||||
  var sanitizedLink = sanitizeUrl(link);
 | 
			
		||||
  imageElem.attr('xlink:href', sanitizedLink);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const drawEmbeddedImage = function (elem, x, y, link) {
 | 
			
		||||
  const imageElem = elem.append('use');
 | 
			
		||||
  imageElem.attr('x', x);
 | 
			
		||||
  imageElem.attr('y', y);
 | 
			
		||||
  const sanitizedLink = sanitizeUrl(link);
 | 
			
		||||
  imageElem.attr('xlink:href', '#' + sanitizedLink);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getNoteRect = function () {
 | 
			
		||||
  return {
 | 
			
		||||
    x: 0,
 | 
			
		||||
    y: 0,
 | 
			
		||||
    width: 100,
 | 
			
		||||
    height: 100,
 | 
			
		||||
    fill: '#EDF2AE',
 | 
			
		||||
    stroke: '#666',
 | 
			
		||||
    anchor: 'start',
 | 
			
		||||
    rx: 0,
 | 
			
		||||
    ry: 0,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getTextObj = function () {
 | 
			
		||||
  return {
 | 
			
		||||
    x: 0,
 | 
			
		||||
    y: 0,
 | 
			
		||||
    width: 100,
 | 
			
		||||
    height: 100,
 | 
			
		||||
    fill: undefined,
 | 
			
		||||
    anchor: undefined,
 | 
			
		||||
    'text-anchor': 'start',
 | 
			
		||||
    style: '#666',
 | 
			
		||||
    textMargin: 0,
 | 
			
		||||
    rx: 0,
 | 
			
		||||
    ry: 0,
 | 
			
		||||
    tspan: true,
 | 
			
		||||
    valign: undefined,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										126
									
								
								packages/mermaid/src/diagrams/common/svgDrawCommon.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								packages/mermaid/src/diagrams/common/svgDrawCommon.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,126 @@
 | 
			
		||||
import { sanitizeUrl } from '@braintree/sanitize-url';
 | 
			
		||||
import type { Group, SVG } from '../../diagram-api/types.js';
 | 
			
		||||
import type {
 | 
			
		||||
  Bound,
 | 
			
		||||
  D3ImageElement,
 | 
			
		||||
  D3RectElement,
 | 
			
		||||
  D3TSpanElement,
 | 
			
		||||
  D3TextElement,
 | 
			
		||||
  D3UseElement,
 | 
			
		||||
  RectData,
 | 
			
		||||
  TextData,
 | 
			
		||||
  TextObject,
 | 
			
		||||
} from './commonTypes.js';
 | 
			
		||||
import { lineBreakRegex } from './common.js';
 | 
			
		||||
 | 
			
		||||
export const drawRect = (element: SVG | Group, rectData: RectData): D3RectElement => {
 | 
			
		||||
  const rectElement: D3RectElement = element.append('rect');
 | 
			
		||||
  rectElement.attr('x', rectData.x);
 | 
			
		||||
  rectElement.attr('y', rectData.y);
 | 
			
		||||
  rectElement.attr('fill', rectData.fill);
 | 
			
		||||
  rectElement.attr('stroke', rectData.stroke);
 | 
			
		||||
  rectElement.attr('width', rectData.width);
 | 
			
		||||
  rectElement.attr('height', rectData.height);
 | 
			
		||||
  rectData.rx !== undefined && rectElement.attr('rx', rectData.rx);
 | 
			
		||||
  rectData.ry !== undefined && rectElement.attr('ry', rectData.ry);
 | 
			
		||||
 | 
			
		||||
  if (rectData.attrs !== undefined) {
 | 
			
		||||
    for (const attrKey in rectData.attrs) {
 | 
			
		||||
      rectElement.attr(attrKey, rectData.attrs[attrKey]);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  rectData.class !== undefined && rectElement.attr('class', rectData.class);
 | 
			
		||||
 | 
			
		||||
  return rectElement;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Draws a background rectangle
 | 
			
		||||
 *
 | 
			
		||||
 * @param element - Diagram (reference for bounds)
 | 
			
		||||
 * @param bounds - Shape of the rectangle
 | 
			
		||||
 */
 | 
			
		||||
export const drawBackgroundRect = (element: SVG | Group, bounds: Bound): void => {
 | 
			
		||||
  const rectData: RectData = {
 | 
			
		||||
    x: bounds.startx,
 | 
			
		||||
    y: bounds.starty,
 | 
			
		||||
    width: bounds.stopx - bounds.startx,
 | 
			
		||||
    height: bounds.stopy - bounds.starty,
 | 
			
		||||
    fill: bounds.fill,
 | 
			
		||||
    stroke: bounds.stroke,
 | 
			
		||||
    class: 'rect',
 | 
			
		||||
  };
 | 
			
		||||
  const rectElement: D3RectElement = drawRect(element, rectData);
 | 
			
		||||
  rectElement.lower();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const drawText = (element: SVG | Group, textData: TextData): D3TextElement => {
 | 
			
		||||
  const nText: string = textData.text.replace(lineBreakRegex, ' ');
 | 
			
		||||
 | 
			
		||||
  const textElem: D3TextElement = element.append('text');
 | 
			
		||||
  textElem.attr('x', textData.x);
 | 
			
		||||
  textElem.attr('y', textData.y);
 | 
			
		||||
  textElem.attr('class', 'legend');
 | 
			
		||||
 | 
			
		||||
  textElem.style('text-anchor', textData.anchor);
 | 
			
		||||
  textData.class !== undefined && textElem.attr('class', textData.class);
 | 
			
		||||
 | 
			
		||||
  const tspan: D3TSpanElement = textElem.append('tspan');
 | 
			
		||||
  tspan.attr('x', textData.x + textData.textMargin * 2);
 | 
			
		||||
  tspan.text(nText);
 | 
			
		||||
 | 
			
		||||
  return textElem;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const drawImage = (elem: SVG | Group, x: number, y: number, link: string): void => {
 | 
			
		||||
  const imageElement: D3ImageElement = elem.append('image');
 | 
			
		||||
  imageElement.attr('x', x);
 | 
			
		||||
  imageElement.attr('y', y);
 | 
			
		||||
  const sanitizedLink: string = sanitizeUrl(link);
 | 
			
		||||
  imageElement.attr('xlink:href', sanitizedLink);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const drawEmbeddedImage = (
 | 
			
		||||
  element: SVG | Group,
 | 
			
		||||
  x: number,
 | 
			
		||||
  y: number,
 | 
			
		||||
  link: string
 | 
			
		||||
): void => {
 | 
			
		||||
  const imageElement: D3UseElement = element.append('use');
 | 
			
		||||
  imageElement.attr('x', x);
 | 
			
		||||
  imageElement.attr('y', y);
 | 
			
		||||
  const sanitizedLink: string = sanitizeUrl(link);
 | 
			
		||||
  imageElement.attr('xlink:href', `#${sanitizedLink}`);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getNoteRect = (): RectData => {
 | 
			
		||||
  const noteRectData: RectData = {
 | 
			
		||||
    x: 0,
 | 
			
		||||
    y: 0,
 | 
			
		||||
    width: 100,
 | 
			
		||||
    height: 100,
 | 
			
		||||
    fill: '#EDF2AE',
 | 
			
		||||
    stroke: '#666',
 | 
			
		||||
    anchor: 'start',
 | 
			
		||||
    rx: 0,
 | 
			
		||||
    ry: 0,
 | 
			
		||||
  };
 | 
			
		||||
  return noteRectData;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getTextObj = (): TextObject => {
 | 
			
		||||
  const testObject: TextObject = {
 | 
			
		||||
    x: 0,
 | 
			
		||||
    y: 0,
 | 
			
		||||
    width: 100,
 | 
			
		||||
    height: 100,
 | 
			
		||||
    'text-anchor': 'start',
 | 
			
		||||
    style: '#666',
 | 
			
		||||
    textMargin: 0,
 | 
			
		||||
    rx: 0,
 | 
			
		||||
    ry: 0,
 | 
			
		||||
    tspan: true,
 | 
			
		||||
  };
 | 
			
		||||
  return testObject;
 | 
			
		||||
};
 | 
			
		||||
@@ -1,10 +0,0 @@
 | 
			
		||||
name,amounts
 | 
			
		||||
Foo, 33
 | 
			
		||||
Rishab, 12
 | 
			
		||||
Alexis, 41
 | 
			
		||||
Tom, 16
 | 
			
		||||
Courtney, 59
 | 
			
		||||
Christina, 38
 | 
			
		||||
Jack, 21
 | 
			
		||||
Mickey, 25
 | 
			
		||||
Paul, 30
 | 
			
		||||
		
		
			
  | 
@@ -1,132 +0,0 @@
 | 
			
		||||
import pieDb from '../pieDb.js';
 | 
			
		||||
import pie from './pie.jison';
 | 
			
		||||
import { setConfig } from '../../../config.js';
 | 
			
		||||
 | 
			
		||||
setConfig({
 | 
			
		||||
  securityLevel: 'strict',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('when parsing pie', function () {
 | 
			
		||||
  beforeEach(function () {
 | 
			
		||||
    pie.parser.yy = pieDb;
 | 
			
		||||
    pie.parser.yy.clear();
 | 
			
		||||
  });
 | 
			
		||||
  it('should handle very simple pie', function () {
 | 
			
		||||
    const res = pie.parser.parse(`pie
 | 
			
		||||
"ash" : 100
 | 
			
		||||
`);
 | 
			
		||||
    const sections = pieDb.getSections();
 | 
			
		||||
    const section1 = sections['ash'];
 | 
			
		||||
    expect(section1).toBe(100);
 | 
			
		||||
  });
 | 
			
		||||
  it('should handle simple pie', function () {
 | 
			
		||||
    const res = pie.parser.parse(`pie
 | 
			
		||||
"ash" : 60
 | 
			
		||||
"bat" : 40
 | 
			
		||||
`);
 | 
			
		||||
    const sections = pieDb.getSections();
 | 
			
		||||
    const section1 = sections['ash'];
 | 
			
		||||
    expect(section1).toBe(60);
 | 
			
		||||
  });
 | 
			
		||||
  it('should handle simple pie with comments', function () {
 | 
			
		||||
    const res = pie.parser.parse(`pie
 | 
			
		||||
    %% comments
 | 
			
		||||
"ash" : 60
 | 
			
		||||
"bat" : 40
 | 
			
		||||
`);
 | 
			
		||||
    const sections = pieDb.getSections();
 | 
			
		||||
    const section1 = sections['ash'];
 | 
			
		||||
    expect(section1).toBe(60);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should handle simple pie with a directive', function () {
 | 
			
		||||
    const res = pie.parser.parse(`%%{init: {'logLevel':0}}%%
 | 
			
		||||
pie
 | 
			
		||||
"ash" : 60
 | 
			
		||||
"bat" : 40
 | 
			
		||||
`);
 | 
			
		||||
    const sections = pieDb.getSections();
 | 
			
		||||
    const section1 = sections['ash'];
 | 
			
		||||
    expect(section1).toBe(60);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should handle simple pie with a title', function () {
 | 
			
		||||
    const res = pie.parser.parse(`pie title a 60/40 pie
 | 
			
		||||
"ash" : 60
 | 
			
		||||
"bat" : 40
 | 
			
		||||
`);
 | 
			
		||||
    const sections = pieDb.getSections();
 | 
			
		||||
    const title = pieDb.getDiagramTitle();
 | 
			
		||||
    const section1 = sections['ash'];
 | 
			
		||||
    expect(section1).toBe(60);
 | 
			
		||||
    expect(title).toBe('a 60/40 pie');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should handle simple pie without an acc description  (accDescr)', function () {
 | 
			
		||||
    const res = pie.parser.parse(`pie title a neat chart
 | 
			
		||||
"ash" : 60
 | 
			
		||||
"bat" : 40
 | 
			
		||||
`);
 | 
			
		||||
 | 
			
		||||
    const sections = pieDb.getSections();
 | 
			
		||||
    const title = pieDb.getDiagramTitle();
 | 
			
		||||
    const description = pieDb.getAccDescription();
 | 
			
		||||
    const section1 = sections['ash'];
 | 
			
		||||
    expect(section1).toBe(60);
 | 
			
		||||
    expect(title).toBe('a neat chart');
 | 
			
		||||
    expect(description).toBe('');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should handle simple pie with an acc description (accDescr)', function () {
 | 
			
		||||
    const res = pie.parser.parse(`pie title a neat chart
 | 
			
		||||
    accDescr: a neat description
 | 
			
		||||
"ash" : 60
 | 
			
		||||
"bat" : 40
 | 
			
		||||
`);
 | 
			
		||||
 | 
			
		||||
    const sections = pieDb.getSections();
 | 
			
		||||
    const title = pieDb.getDiagramTitle();
 | 
			
		||||
    const description = pieDb.getAccDescription();
 | 
			
		||||
    const section1 = sections['ash'];
 | 
			
		||||
    expect(section1).toBe(60);
 | 
			
		||||
    expect(title).toBe('a neat chart');
 | 
			
		||||
    expect(description).toBe('a neat description');
 | 
			
		||||
  });
 | 
			
		||||
  it('should handle simple pie with a multiline acc description (accDescr)', function () {
 | 
			
		||||
    const res = pie.parser.parse(`pie title a neat chart
 | 
			
		||||
    accDescr {
 | 
			
		||||
      a neat description
 | 
			
		||||
      on multiple lines
 | 
			
		||||
    }
 | 
			
		||||
"ash" : 60
 | 
			
		||||
"bat" : 40
 | 
			
		||||
`);
 | 
			
		||||
 | 
			
		||||
    const sections = pieDb.getSections();
 | 
			
		||||
    const title = pieDb.getDiagramTitle();
 | 
			
		||||
    const description = pieDb.getAccDescription();
 | 
			
		||||
    const section1 = sections['ash'];
 | 
			
		||||
    expect(section1).toBe(60);
 | 
			
		||||
    expect(title).toBe('a neat chart');
 | 
			
		||||
    expect(description).toBe('a neat description\non multiple lines');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should handle simple pie with positive decimal', function () {
 | 
			
		||||
    const res = pie.parser.parse(`pie
 | 
			
		||||
"ash" : 60.67
 | 
			
		||||
"bat" : 40
 | 
			
		||||
`);
 | 
			
		||||
    const sections = pieDb.getSections();
 | 
			
		||||
    const section1 = sections['ash'];
 | 
			
		||||
    expect(section1).toBe(60.67);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should handle simple pie with negative decimal', function () {
 | 
			
		||||
    expect(() => {
 | 
			
		||||
      pie.parser.parse(`pie
 | 
			
		||||
"ash" : 60.67
 | 
			
		||||
"bat" : 40..12
 | 
			
		||||
`);
 | 
			
		||||
    }).toThrowError();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										180
									
								
								packages/mermaid/src/diagrams/pie/pie.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								packages/mermaid/src/diagrams/pie/pie.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,180 @@
 | 
			
		||||
// @ts-ignore: JISON doesn't support types
 | 
			
		||||
import { parser } from './parser/pie.jison';
 | 
			
		||||
import { DEFAULT_PIE_DB, db } from './pieDb.js';
 | 
			
		||||
import { setConfig } from '../../config.js';
 | 
			
		||||
 | 
			
		||||
setConfig({
 | 
			
		||||
  securityLevel: 'strict',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('pie', () => {
 | 
			
		||||
  beforeAll(() => {
 | 
			
		||||
    parser.yy = db;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    parser.yy.clear();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('parse', () => {
 | 
			
		||||
    it('should handle very simple pie', () => {
 | 
			
		||||
      parser.parse(`pie
 | 
			
		||||
      "ash": 100
 | 
			
		||||
      `);
 | 
			
		||||
 | 
			
		||||
      const sections = db.getSections();
 | 
			
		||||
      expect(sections['ash']).toBe(100);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle simple pie', () => {
 | 
			
		||||
      parser.parse(`pie
 | 
			
		||||
      "ash" : 60
 | 
			
		||||
      "bat" : 40
 | 
			
		||||
      `);
 | 
			
		||||
 | 
			
		||||
      const sections = db.getSections();
 | 
			
		||||
      expect(sections['ash']).toBe(60);
 | 
			
		||||
      expect(sections['bat']).toBe(40);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle simple pie with showData', () => {
 | 
			
		||||
      parser.parse(`pie showData
 | 
			
		||||
      "ash" : 60
 | 
			
		||||
      "bat" : 40
 | 
			
		||||
      `);
 | 
			
		||||
 | 
			
		||||
      expect(db.getShowData()).toBeTruthy();
 | 
			
		||||
 | 
			
		||||
      const sections = db.getSections();
 | 
			
		||||
      expect(sections['ash']).toBe(60);
 | 
			
		||||
      expect(sections['bat']).toBe(40);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle simple pie with comments', () => {
 | 
			
		||||
      parser.parse(`pie
 | 
			
		||||
      %% comments
 | 
			
		||||
      "ash" : 60
 | 
			
		||||
      "bat" : 40
 | 
			
		||||
      `);
 | 
			
		||||
 | 
			
		||||
      const sections = db.getSections();
 | 
			
		||||
      expect(sections['ash']).toBe(60);
 | 
			
		||||
      expect(sections['bat']).toBe(40);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle simple pie with a directive', () => {
 | 
			
		||||
      parser.parse(`%%{init: {'logLevel':0}}%%
 | 
			
		||||
      pie
 | 
			
		||||
      "ash" : 60
 | 
			
		||||
      "bat" : 40
 | 
			
		||||
      `);
 | 
			
		||||
      const sections = db.getSections();
 | 
			
		||||
      expect(sections['ash']).toBe(60);
 | 
			
		||||
      expect(sections['bat']).toBe(40);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle simple pie with a title', () => {
 | 
			
		||||
      parser.parse(`pie title a 60/40 pie
 | 
			
		||||
      "ash" : 60
 | 
			
		||||
      "bat" : 40
 | 
			
		||||
      `);
 | 
			
		||||
 | 
			
		||||
      expect(db.getDiagramTitle()).toBe('a 60/40 pie');
 | 
			
		||||
 | 
			
		||||
      const sections = db.getSections();
 | 
			
		||||
      expect(sections['ash']).toBe(60);
 | 
			
		||||
      expect(sections['bat']).toBe(40);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle simple pie with an acc title (accTitle)', () => {
 | 
			
		||||
      parser.parse(`pie title a neat chart
 | 
			
		||||
      accTitle: a neat acc title
 | 
			
		||||
      "ash" : 60
 | 
			
		||||
      "bat" : 40
 | 
			
		||||
      `);
 | 
			
		||||
 | 
			
		||||
      expect(db.getDiagramTitle()).toBe('a neat chart');
 | 
			
		||||
 | 
			
		||||
      expect(db.getAccTitle()).toBe('a neat acc title');
 | 
			
		||||
 | 
			
		||||
      const sections = db.getSections();
 | 
			
		||||
      expect(sections['ash']).toBe(60);
 | 
			
		||||
      expect(sections['bat']).toBe(40);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle simple pie with an acc description (accDescr)', () => {
 | 
			
		||||
      parser.parse(`pie title a neat chart
 | 
			
		||||
      accDescr: a neat description
 | 
			
		||||
      "ash" : 60
 | 
			
		||||
      "bat" : 40
 | 
			
		||||
      `);
 | 
			
		||||
 | 
			
		||||
      expect(db.getDiagramTitle()).toBe('a neat chart');
 | 
			
		||||
 | 
			
		||||
      expect(db.getAccDescription()).toBe('a neat description');
 | 
			
		||||
 | 
			
		||||
      const sections = db.getSections();
 | 
			
		||||
      expect(sections['ash']).toBe(60);
 | 
			
		||||
      expect(sections['bat']).toBe(40);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle simple pie with a multiline acc description (accDescr)', () => {
 | 
			
		||||
      parser.parse(`pie title a neat chart
 | 
			
		||||
      accDescr {
 | 
			
		||||
        a neat description
 | 
			
		||||
        on multiple lines
 | 
			
		||||
      }
 | 
			
		||||
      "ash" : 60
 | 
			
		||||
      "bat" : 40
 | 
			
		||||
    `);
 | 
			
		||||
 | 
			
		||||
      expect(db.getDiagramTitle()).toBe('a neat chart');
 | 
			
		||||
 | 
			
		||||
      expect(db.getAccDescription()).toBe('a neat description\non multiple lines');
 | 
			
		||||
 | 
			
		||||
      const sections = db.getSections();
 | 
			
		||||
      expect(sections['ash']).toBe(60);
 | 
			
		||||
      expect(sections['bat']).toBe(40);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle simple pie with positive decimal', () => {
 | 
			
		||||
      parser.parse(`pie
 | 
			
		||||
      "ash" : 60.67
 | 
			
		||||
      "bat" : 40
 | 
			
		||||
      `);
 | 
			
		||||
 | 
			
		||||
      const sections = db.getSections();
 | 
			
		||||
      expect(sections['ash']).toBe(60.67);
 | 
			
		||||
      expect(sections['bat']).toBe(40);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle simple pie with negative decimal', () => {
 | 
			
		||||
      expect(() => {
 | 
			
		||||
        parser.parse(`pie
 | 
			
		||||
        "ash" : -60.67
 | 
			
		||||
        "bat" : 40.12
 | 
			
		||||
        `);
 | 
			
		||||
      }).toThrowError();
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('config', () => {
 | 
			
		||||
    it.todo('setConfig', () => {
 | 
			
		||||
      // db.setConfig({ useWidth: 850, useMaxWidth: undefined });
 | 
			
		||||
 | 
			
		||||
      const config = db.getConfig();
 | 
			
		||||
      expect(config.useWidth).toBe(850);
 | 
			
		||||
      expect(config.useMaxWidth).toBeTruthy();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('getConfig', () => {
 | 
			
		||||
      expect(db.getConfig()).toStrictEqual(DEFAULT_PIE_DB.config);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it.todo('resetConfig', () => {
 | 
			
		||||
      // db.setConfig({ textPosition: 0 });
 | 
			
		||||
      // db.resetConfig();
 | 
			
		||||
      expect(db.getConfig().textPosition).toStrictEqual(DEFAULT_PIE_DB.config.textPosition);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,69 +0,0 @@
 | 
			
		||||
import { log } from '../../logger.js';
 | 
			
		||||
import mermaidAPI from '../../mermaidAPI.js';
 | 
			
		||||
import * as configApi from '../../config.js';
 | 
			
		||||
import common from '../common/common.js';
 | 
			
		||||
import {
 | 
			
		||||
  setAccTitle,
 | 
			
		||||
  getAccTitle,
 | 
			
		||||
  setDiagramTitle,
 | 
			
		||||
  getDiagramTitle,
 | 
			
		||||
  getAccDescription,
 | 
			
		||||
  setAccDescription,
 | 
			
		||||
  clear as commonClear,
 | 
			
		||||
} from '../../commonDb.js';
 | 
			
		||||
 | 
			
		||||
let sections = {};
 | 
			
		||||
let showData = false;
 | 
			
		||||
 | 
			
		||||
export const parseDirective = function (statement, context, type) {
 | 
			
		||||
  mermaidAPI.parseDirective(this, statement, context, type);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const addSection = function (id, value) {
 | 
			
		||||
  id = common.sanitizeText(id, configApi.getConfig());
 | 
			
		||||
  if (sections[id] === undefined) {
 | 
			
		||||
    sections[id] = value;
 | 
			
		||||
    log.debug('Added new section :', id);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
const getSections = () => sections;
 | 
			
		||||
 | 
			
		||||
const setShowData = function (toggle) {
 | 
			
		||||
  showData = toggle;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getShowData = function () {
 | 
			
		||||
  return showData;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const cleanupValue = function (value) {
 | 
			
		||||
  if (value.substring(0, 1) === ':') {
 | 
			
		||||
    value = value.substring(1).trim();
 | 
			
		||||
    return Number(value.trim());
 | 
			
		||||
  } else {
 | 
			
		||||
    return Number(value.trim());
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const clear = function () {
 | 
			
		||||
  sections = {};
 | 
			
		||||
  showData = false;
 | 
			
		||||
  commonClear();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  parseDirective,
 | 
			
		||||
  getConfig: () => configApi.getConfig().pie,
 | 
			
		||||
  addSection,
 | 
			
		||||
  getSections,
 | 
			
		||||
  cleanupValue,
 | 
			
		||||
  clear,
 | 
			
		||||
  setAccTitle,
 | 
			
		||||
  getAccTitle,
 | 
			
		||||
  setDiagramTitle,
 | 
			
		||||
  getDiagramTitle,
 | 
			
		||||
  setShowData,
 | 
			
		||||
  getShowData,
 | 
			
		||||
  getAccDescription,
 | 
			
		||||
  setAccDescription,
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										84
									
								
								packages/mermaid/src/diagrams/pie/pieDb.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								packages/mermaid/src/diagrams/pie/pieDb.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,84 @@
 | 
			
		||||
import { log } from '../../logger.js';
 | 
			
		||||
import { parseDirective as _parseDirective } from '../../directiveUtils.js';
 | 
			
		||||
import { getConfig as commonGetConfig } from '../../config.js';
 | 
			
		||||
import { sanitizeText } from '../common/common.js';
 | 
			
		||||
import {
 | 
			
		||||
  setAccTitle,
 | 
			
		||||
  getAccTitle,
 | 
			
		||||
  setDiagramTitle,
 | 
			
		||||
  getDiagramTitle,
 | 
			
		||||
  getAccDescription,
 | 
			
		||||
  setAccDescription,
 | 
			
		||||
  clear as commonClear,
 | 
			
		||||
} from '../../commonDb.js';
 | 
			
		||||
import type { ParseDirectiveDefinition } from '../../diagram-api/types.js';
 | 
			
		||||
import type { PieFields, PieDB, Sections } from './pieTypes.js';
 | 
			
		||||
import type { RequiredDeep } from 'type-fest';
 | 
			
		||||
import type { PieDiagramConfig } from '../../config.type.js';
 | 
			
		||||
import DEFAULT_CONFIG from '../../defaultConfig.js';
 | 
			
		||||
 | 
			
		||||
export const DEFAULT_PIE_CONFIG: Required<PieDiagramConfig> = DEFAULT_CONFIG.pie;
 | 
			
		||||
 | 
			
		||||
export const DEFAULT_PIE_DB: RequiredDeep<PieFields> = {
 | 
			
		||||
  sections: {},
 | 
			
		||||
  showData: false,
 | 
			
		||||
  config: DEFAULT_PIE_CONFIG,
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
let sections: Sections = DEFAULT_PIE_DB.sections;
 | 
			
		||||
let showData: boolean = DEFAULT_PIE_DB.showData;
 | 
			
		||||
const config: Required<PieDiagramConfig> = structuredClone(DEFAULT_PIE_CONFIG);
 | 
			
		||||
 | 
			
		||||
const getConfig = (): Required<PieDiagramConfig> => structuredClone(config);
 | 
			
		||||
 | 
			
		||||
const parseDirective: ParseDirectiveDefinition = (statement, context, type) => {
 | 
			
		||||
  _parseDirective(this, statement, context, type);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const clear = (): void => {
 | 
			
		||||
  sections = structuredClone(DEFAULT_PIE_DB.sections);
 | 
			
		||||
  showData = DEFAULT_PIE_DB.showData;
 | 
			
		||||
  commonClear();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const addSection = (label: string, value: number): void => {
 | 
			
		||||
  label = sanitizeText(label, commonGetConfig());
 | 
			
		||||
  if (sections[label] === undefined) {
 | 
			
		||||
    sections[label] = value;
 | 
			
		||||
    log.debug(`added new section: ${label}, with value: ${value}`);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getSections = (): Sections => sections;
 | 
			
		||||
 | 
			
		||||
const cleanupValue = (value: string): number => {
 | 
			
		||||
  if (value.substring(0, 1) === ':') {
 | 
			
		||||
    value = value.substring(1).trim();
 | 
			
		||||
  }
 | 
			
		||||
  return Number(value.trim());
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const setShowData = (toggle: boolean): void => {
 | 
			
		||||
  showData = toggle;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getShowData = (): boolean => showData;
 | 
			
		||||
 | 
			
		||||
export const db: PieDB = {
 | 
			
		||||
  getConfig,
 | 
			
		||||
 | 
			
		||||
  parseDirective,
 | 
			
		||||
  clear,
 | 
			
		||||
  setDiagramTitle,
 | 
			
		||||
  getDiagramTitle,
 | 
			
		||||
  setAccTitle,
 | 
			
		||||
  getAccTitle,
 | 
			
		||||
  setAccDescription,
 | 
			
		||||
  getAccDescription,
 | 
			
		||||
 | 
			
		||||
  addSection,
 | 
			
		||||
  getSections,
 | 
			
		||||
  cleanupValue,
 | 
			
		||||
  setShowData,
 | 
			
		||||
  getShowData,
 | 
			
		||||
};
 | 
			
		||||
@@ -15,10 +15,8 @@ const loader: DiagramLoader = async () => {
 | 
			
		||||
  return { id, diagram };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const plugin: ExternalDiagramDefinition = {
 | 
			
		||||
export const pie: ExternalDiagramDefinition = {
 | 
			
		||||
  id,
 | 
			
		||||
  detector,
 | 
			
		||||
  loader,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default plugin;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
import { DiagramDefinition } from '../../diagram-api/types.js';
 | 
			
		||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
 | 
			
		||||
// @ts-ignore: JISON doesn't support types
 | 
			
		||||
import parser from './parser/pie.jison';
 | 
			
		||||
import db from './pieDb.js';
 | 
			
		||||
import styles from './styles.js';
 | 
			
		||||
import renderer from './pieRenderer.js';
 | 
			
		||||
import { db } from './pieDb.js';
 | 
			
		||||
import styles from './pieStyles.js';
 | 
			
		||||
import { renderer } from './pieRenderer.js';
 | 
			
		||||
 | 
			
		||||
export const diagram: DiagramDefinition = {
 | 
			
		||||
  parser,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,204 +0,0 @@
 | 
			
		||||
/** Created by AshishJ on 11-09-2019. */
 | 
			
		||||
import { select, scaleOrdinal, pie as d3pie, arc } from 'd3';
 | 
			
		||||
import { log } from '../../logger.js';
 | 
			
		||||
import { configureSvgSize } from '../../setupGraphViewbox.js';
 | 
			
		||||
import * as configApi from '../../config.js';
 | 
			
		||||
import { parseFontSize } from '../../utils.js';
 | 
			
		||||
 | 
			
		||||
let conf = configApi.getConfig();
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Draws a Pie Chart with the data given in text.
 | 
			
		||||
 *
 | 
			
		||||
 * @param text
 | 
			
		||||
 * @param id
 | 
			
		||||
 */
 | 
			
		||||
let width;
 | 
			
		||||
const height = 450;
 | 
			
		||||
export const draw = (txt, id, _version, diagObj) => {
 | 
			
		||||
  try {
 | 
			
		||||
    conf = configApi.getConfig();
 | 
			
		||||
    log.debug('Rendering info diagram\n' + txt);
 | 
			
		||||
 | 
			
		||||
    const securityLevel = configApi.getConfig().securityLevel;
 | 
			
		||||
    // Handle root and Document for when rendering in sandbox mode
 | 
			
		||||
    let sandboxElement;
 | 
			
		||||
    if (securityLevel === 'sandbox') {
 | 
			
		||||
      sandboxElement = select('#i' + id);
 | 
			
		||||
    }
 | 
			
		||||
    const root =
 | 
			
		||||
      securityLevel === 'sandbox'
 | 
			
		||||
        ? select(sandboxElement.nodes()[0].contentDocument.body)
 | 
			
		||||
        : select('body');
 | 
			
		||||
    const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document;
 | 
			
		||||
 | 
			
		||||
    // Parse the Pie Chart definition
 | 
			
		||||
    const elem = doc.getElementById(id);
 | 
			
		||||
    width = elem.parentElement.offsetWidth;
 | 
			
		||||
 | 
			
		||||
    if (width === undefined) {
 | 
			
		||||
      width = 1200;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (conf.useWidth !== undefined) {
 | 
			
		||||
      width = conf.useWidth;
 | 
			
		||||
    }
 | 
			
		||||
    if (conf.pie.useWidth !== undefined) {
 | 
			
		||||
      width = conf.pie.useWidth;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const diagram = root.select('#' + id);
 | 
			
		||||
    configureSvgSize(diagram, height, width, conf.pie.useMaxWidth);
 | 
			
		||||
 | 
			
		||||
    // Set viewBox
 | 
			
		||||
    elem.setAttribute('viewBox', '0 0 ' + width + ' ' + height);
 | 
			
		||||
 | 
			
		||||
    // Fetch the default direction, use TD if none was found
 | 
			
		||||
    var margin = 40;
 | 
			
		||||
    var legendRectSize = 18;
 | 
			
		||||
    var legendSpacing = 4;
 | 
			
		||||
 | 
			
		||||
    var radius = Math.min(width, height) / 2 - margin;
 | 
			
		||||
 | 
			
		||||
    var svg = diagram
 | 
			
		||||
      .append('g')
 | 
			
		||||
      .attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
 | 
			
		||||
 | 
			
		||||
    var data = diagObj.db.getSections();
 | 
			
		||||
    var sum = 0;
 | 
			
		||||
    Object.keys(data).forEach(function (key) {
 | 
			
		||||
      sum += data[key];
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const themeVariables = conf.themeVariables;
 | 
			
		||||
    var myGeneratedColors = [
 | 
			
		||||
      themeVariables.pie1,
 | 
			
		||||
      themeVariables.pie2,
 | 
			
		||||
      themeVariables.pie3,
 | 
			
		||||
      themeVariables.pie4,
 | 
			
		||||
      themeVariables.pie5,
 | 
			
		||||
      themeVariables.pie6,
 | 
			
		||||
      themeVariables.pie7,
 | 
			
		||||
      themeVariables.pie8,
 | 
			
		||||
      themeVariables.pie9,
 | 
			
		||||
      themeVariables.pie10,
 | 
			
		||||
      themeVariables.pie11,
 | 
			
		||||
      themeVariables.pie12,
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    const textPosition = conf.pie?.textPosition ?? 0.75;
 | 
			
		||||
    let [outerStrokeWidth] = parseFontSize(themeVariables.pieOuterStrokeWidth);
 | 
			
		||||
    outerStrokeWidth ??= 2;
 | 
			
		||||
 | 
			
		||||
    // Set the color scale
 | 
			
		||||
    var color = scaleOrdinal().range(myGeneratedColors);
 | 
			
		||||
 | 
			
		||||
    // Compute the position of each group on the pie:
 | 
			
		||||
    var pieData = Object.entries(data).map(function (el, idx) {
 | 
			
		||||
      return {
 | 
			
		||||
        order: idx,
 | 
			
		||||
        name: el[0],
 | 
			
		||||
        value: el[1],
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
    var pie = d3pie()
 | 
			
		||||
      .value(function (d) {
 | 
			
		||||
        return d.value;
 | 
			
		||||
      })
 | 
			
		||||
      .sort(function (a, b) {
 | 
			
		||||
        // Sort slices in clockwise direction
 | 
			
		||||
        return a.order - b.order;
 | 
			
		||||
      });
 | 
			
		||||
    var dataReady = pie(pieData);
 | 
			
		||||
 | 
			
		||||
    // Shape helper to build arcs:
 | 
			
		||||
    var arcGenerator = arc().innerRadius(0).outerRadius(radius);
 | 
			
		||||
    var labelArcGenerator = arc()
 | 
			
		||||
      .innerRadius(radius * textPosition)
 | 
			
		||||
      .outerRadius(radius * textPosition);
 | 
			
		||||
 | 
			
		||||
    svg
 | 
			
		||||
      .append('circle')
 | 
			
		||||
      .attr('cx', 0)
 | 
			
		||||
      .attr('cy', 0)
 | 
			
		||||
      .attr('r', radius + outerStrokeWidth / 2)
 | 
			
		||||
      .attr('class', 'pieOuterCircle');
 | 
			
		||||
 | 
			
		||||
    // Build the pie chart: each part of the pie is a path that we build using the arc function.
 | 
			
		||||
    svg
 | 
			
		||||
      .selectAll('mySlices')
 | 
			
		||||
      .data(dataReady)
 | 
			
		||||
      .enter()
 | 
			
		||||
      .append('path')
 | 
			
		||||
      .attr('d', arcGenerator)
 | 
			
		||||
      .attr('fill', function (d) {
 | 
			
		||||
        return color(d.data.name);
 | 
			
		||||
      })
 | 
			
		||||
      .attr('class', 'pieCircle');
 | 
			
		||||
 | 
			
		||||
    // Now add the percentage.
 | 
			
		||||
    // Use the centroid method to get the best coordinates.
 | 
			
		||||
    svg
 | 
			
		||||
      .selectAll('mySlices')
 | 
			
		||||
      .data(dataReady)
 | 
			
		||||
      .enter()
 | 
			
		||||
      .append('text')
 | 
			
		||||
      .text(function (d) {
 | 
			
		||||
        return ((d.data.value / sum) * 100).toFixed(0) + '%';
 | 
			
		||||
      })
 | 
			
		||||
      .attr('transform', function (d) {
 | 
			
		||||
        return 'translate(' + labelArcGenerator.centroid(d) + ')';
 | 
			
		||||
      })
 | 
			
		||||
      .style('text-anchor', 'middle')
 | 
			
		||||
      .attr('class', 'slice');
 | 
			
		||||
 | 
			
		||||
    svg
 | 
			
		||||
      .append('text')
 | 
			
		||||
      .text(diagObj.db.getDiagramTitle())
 | 
			
		||||
      .attr('x', 0)
 | 
			
		||||
      .attr('y', -(height - 50) / 2)
 | 
			
		||||
      .attr('class', 'pieTitleText');
 | 
			
		||||
 | 
			
		||||
    // Add the legends/annotations for each section
 | 
			
		||||
    var legend = svg
 | 
			
		||||
      .selectAll('.legend')
 | 
			
		||||
      .data(color.domain())
 | 
			
		||||
      .enter()
 | 
			
		||||
      .append('g')
 | 
			
		||||
      .attr('class', 'legend')
 | 
			
		||||
      .attr('transform', function (d, i) {
 | 
			
		||||
        const height = legendRectSize + legendSpacing;
 | 
			
		||||
        const offset = (height * color.domain().length) / 2;
 | 
			
		||||
        const horizontal = 12 * legendRectSize;
 | 
			
		||||
        const vertical = i * height - offset;
 | 
			
		||||
        return 'translate(' + horizontal + ',' + vertical + ')';
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    legend
 | 
			
		||||
      .append('rect')
 | 
			
		||||
      .attr('width', legendRectSize)
 | 
			
		||||
      .attr('height', legendRectSize)
 | 
			
		||||
      .style('fill', color)
 | 
			
		||||
      .style('stroke', color);
 | 
			
		||||
 | 
			
		||||
    legend
 | 
			
		||||
      .data(dataReady)
 | 
			
		||||
      .append('text')
 | 
			
		||||
      .attr('x', legendRectSize + legendSpacing)
 | 
			
		||||
      .attr('y', legendRectSize - legendSpacing)
 | 
			
		||||
      .text(function (d) {
 | 
			
		||||
        if (diagObj.db.getShowData() || conf.showData || conf.pie.showData) {
 | 
			
		||||
          return d.data.name + ' [' + d.data.value + ']';
 | 
			
		||||
        } else {
 | 
			
		||||
          return d.data.name;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    log.error('Error while rendering info diagram');
 | 
			
		||||
    log.error(e);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  draw,
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										179
									
								
								packages/mermaid/src/diagrams/pie/pieRenderer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								packages/mermaid/src/diagrams/pie/pieRenderer.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,179 @@
 | 
			
		||||
import d3, { scaleOrdinal, pie as d3pie, arc } from 'd3';
 | 
			
		||||
 | 
			
		||||
import { log } from '../../logger.js';
 | 
			
		||||
import { configureSvgSize } from '../../setupGraphViewbox.js';
 | 
			
		||||
import { getConfig } from '../../config.js';
 | 
			
		||||
import { cleanAndMerge, parseFontSize } from '../../utils.js';
 | 
			
		||||
import type { DrawDefinition, Group, SVG } from '../../diagram-api/types.js';
 | 
			
		||||
import type { D3Sections, PieDB, Sections } from './pieTypes.js';
 | 
			
		||||
import type { MermaidConfig, PieDiagramConfig } from '../../config.type.js';
 | 
			
		||||
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
 | 
			
		||||
 | 
			
		||||
const createPieArcs = (sections: Sections): d3.PieArcDatum<D3Sections>[] => {
 | 
			
		||||
  // Compute the position of each group on the pie:
 | 
			
		||||
  const pieData: D3Sections[] = Object.entries(sections).map(
 | 
			
		||||
    (element: [string, number]): D3Sections => {
 | 
			
		||||
      return {
 | 
			
		||||
        label: element[0],
 | 
			
		||||
        value: element[1],
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
  const pie: d3.Pie<unknown, D3Sections> = d3pie<D3Sections>().value(
 | 
			
		||||
    (d3Section: D3Sections): number => d3Section.value
 | 
			
		||||
  );
 | 
			
		||||
  return pie(pieData);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Draws a Pie Chart with the data given in text.
 | 
			
		||||
 *
 | 
			
		||||
 * @param text - pie chart code
 | 
			
		||||
 * @param id - diagram id
 | 
			
		||||
 * @param _version - MermaidJS version from package.json.
 | 
			
		||||
 * @param diagObj - A standard diagram containing the DB and the text and type etc of the diagram.
 | 
			
		||||
 */
 | 
			
		||||
export const draw: DrawDefinition = (text, id, _version, diagObj) => {
 | 
			
		||||
  log.debug('rendering pie chart\n' + text);
 | 
			
		||||
 | 
			
		||||
  const db = diagObj.db as PieDB;
 | 
			
		||||
  const globalConfig: MermaidConfig = getConfig();
 | 
			
		||||
  const pieConfig: Required<PieDiagramConfig> = cleanAndMerge(db.getConfig(), globalConfig.pie);
 | 
			
		||||
 | 
			
		||||
  const height = 450;
 | 
			
		||||
  // TODO: remove document width
 | 
			
		||||
  const width: number =
 | 
			
		||||
    document.getElementById(id)?.parentElement?.offsetWidth ?? pieConfig.useWidth;
 | 
			
		||||
  const svg: SVG = selectSvgElement(id);
 | 
			
		||||
  // Set viewBox
 | 
			
		||||
  svg.attr('viewBox', `0 0 ${width} ${height}`);
 | 
			
		||||
  configureSvgSize(svg, height, width, pieConfig.useMaxWidth);
 | 
			
		||||
 | 
			
		||||
  const MARGIN = 40;
 | 
			
		||||
  const LEGEND_RECT_SIZE = 18;
 | 
			
		||||
  const LEGEND_SPACING = 4;
 | 
			
		||||
 | 
			
		||||
  const group: Group = svg.append('g');
 | 
			
		||||
  group.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
 | 
			
		||||
 | 
			
		||||
  const { themeVariables } = globalConfig;
 | 
			
		||||
  let [outerStrokeWidth] = parseFontSize(themeVariables.pieOuterStrokeWidth);
 | 
			
		||||
  outerStrokeWidth ??= 2;
 | 
			
		||||
 | 
			
		||||
  const textPosition: number = pieConfig.textPosition;
 | 
			
		||||
  const radius: number = Math.min(width, height) / 2 - MARGIN;
 | 
			
		||||
  // Shape helper to build arcs:
 | 
			
		||||
  const arcGenerator: d3.Arc<unknown, d3.PieArcDatum<D3Sections>> = arc<
 | 
			
		||||
    d3.PieArcDatum<D3Sections>
 | 
			
		||||
  >()
 | 
			
		||||
    .innerRadius(0)
 | 
			
		||||
    .outerRadius(radius);
 | 
			
		||||
  const labelArcGenerator: d3.Arc<unknown, d3.PieArcDatum<D3Sections>> = arc<
 | 
			
		||||
    d3.PieArcDatum<D3Sections>
 | 
			
		||||
  >()
 | 
			
		||||
    .innerRadius(radius * textPosition)
 | 
			
		||||
    .outerRadius(radius * textPosition);
 | 
			
		||||
 | 
			
		||||
  group
 | 
			
		||||
    .append('circle')
 | 
			
		||||
    .attr('cx', 0)
 | 
			
		||||
    .attr('cy', 0)
 | 
			
		||||
    .attr('r', radius + outerStrokeWidth / 2)
 | 
			
		||||
    .attr('class', 'pieOuterCircle');
 | 
			
		||||
 | 
			
		||||
  const sections: Sections = db.getSections();
 | 
			
		||||
  const arcs: d3.PieArcDatum<D3Sections>[] = createPieArcs(sections);
 | 
			
		||||
 | 
			
		||||
  const myGeneratedColors = [
 | 
			
		||||
    themeVariables.pie1,
 | 
			
		||||
    themeVariables.pie2,
 | 
			
		||||
    themeVariables.pie3,
 | 
			
		||||
    themeVariables.pie4,
 | 
			
		||||
    themeVariables.pie5,
 | 
			
		||||
    themeVariables.pie6,
 | 
			
		||||
    themeVariables.pie7,
 | 
			
		||||
    themeVariables.pie8,
 | 
			
		||||
    themeVariables.pie9,
 | 
			
		||||
    themeVariables.pie10,
 | 
			
		||||
    themeVariables.pie11,
 | 
			
		||||
    themeVariables.pie12,
 | 
			
		||||
  ];
 | 
			
		||||
  // Set the color scale
 | 
			
		||||
  const color: d3.ScaleOrdinal<string, 12, never> = scaleOrdinal(myGeneratedColors);
 | 
			
		||||
 | 
			
		||||
  // Build the pie chart: each part of the pie is a path that we build using the arc function.
 | 
			
		||||
  group
 | 
			
		||||
    .selectAll('mySlices')
 | 
			
		||||
    .data(arcs)
 | 
			
		||||
    .enter()
 | 
			
		||||
    .append('path')
 | 
			
		||||
    .attr('d', arcGenerator)
 | 
			
		||||
    .attr('fill', (datum: d3.PieArcDatum<D3Sections>) => {
 | 
			
		||||
      return color(datum.data.label);
 | 
			
		||||
    })
 | 
			
		||||
    .attr('class', 'pieCircle');
 | 
			
		||||
 | 
			
		||||
  let sum = 0;
 | 
			
		||||
  Object.keys(sections).forEach((key: string): void => {
 | 
			
		||||
    sum += sections[key];
 | 
			
		||||
  });
 | 
			
		||||
  // Now add the percentage.
 | 
			
		||||
  // Use the centroid method to get the best coordinates.
 | 
			
		||||
  group
 | 
			
		||||
    .selectAll('mySlices')
 | 
			
		||||
    .data(arcs)
 | 
			
		||||
    .enter()
 | 
			
		||||
    .append('text')
 | 
			
		||||
    .text((datum: d3.PieArcDatum<D3Sections>): string => {
 | 
			
		||||
      return ((datum.data.value / sum) * 100).toFixed(0) + '%';
 | 
			
		||||
    })
 | 
			
		||||
    .attr('transform', (datum: d3.PieArcDatum<D3Sections>): string => {
 | 
			
		||||
      return 'translate(' + labelArcGenerator.centroid(datum) + ')';
 | 
			
		||||
    })
 | 
			
		||||
    .style('text-anchor', 'middle')
 | 
			
		||||
    .attr('class', 'slice');
 | 
			
		||||
 | 
			
		||||
  group
 | 
			
		||||
    .append('text')
 | 
			
		||||
    .text(db.getDiagramTitle())
 | 
			
		||||
    .attr('x', 0)
 | 
			
		||||
    .attr('y', -(height - 50) / 2)
 | 
			
		||||
    .attr('class', 'pieTitleText');
 | 
			
		||||
 | 
			
		||||
  // Add the legends/annotations for each section
 | 
			
		||||
  const legend = group
 | 
			
		||||
    .selectAll('.legend')
 | 
			
		||||
    .data(color.domain())
 | 
			
		||||
    .enter()
 | 
			
		||||
    .append('g')
 | 
			
		||||
    .attr('class', 'legend')
 | 
			
		||||
    .attr('transform', (_datum, index: number): string => {
 | 
			
		||||
      const height = LEGEND_RECT_SIZE + LEGEND_SPACING;
 | 
			
		||||
      const offset = (height * color.domain().length) / 2;
 | 
			
		||||
      const horizontal = 12 * LEGEND_RECT_SIZE;
 | 
			
		||||
      const vertical = index * height - offset;
 | 
			
		||||
      return 'translate(' + horizontal + ',' + vertical + ')';
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
  legend
 | 
			
		||||
    .append('rect')
 | 
			
		||||
    .attr('width', LEGEND_RECT_SIZE)
 | 
			
		||||
    .attr('height', LEGEND_RECT_SIZE)
 | 
			
		||||
    .style('fill', color)
 | 
			
		||||
    .style('stroke', color);
 | 
			
		||||
 | 
			
		||||
  legend
 | 
			
		||||
    .data(arcs)
 | 
			
		||||
    .append('text')
 | 
			
		||||
    .attr('x', LEGEND_RECT_SIZE + LEGEND_SPACING)
 | 
			
		||||
    .attr('y', LEGEND_RECT_SIZE - LEGEND_SPACING)
 | 
			
		||||
    .text((datum: d3.PieArcDatum<D3Sections>): string => {
 | 
			
		||||
      const { label, value } = datum.data;
 | 
			
		||||
      if (db.getShowData()) {
 | 
			
		||||
        return `${label} [${value}]`;
 | 
			
		||||
      }
 | 
			
		||||
      return label;
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const renderer = { draw };
 | 
			
		||||
@@ -1,4 +1,7 @@
 | 
			
		||||
const getStyles = (options) =>
 | 
			
		||||
import type { DiagramStylesProvider } from '../../diagram-api/types.js';
 | 
			
		||||
import type { PieStyleOptions } from './pieTypes.js';
 | 
			
		||||
 | 
			
		||||
const getStyles: DiagramStylesProvider = (options: PieStyleOptions) =>
 | 
			
		||||
  `
 | 
			
		||||
  .pieCircle{
 | 
			
		||||
    stroke: ${options.pieStrokeColor};
 | 
			
		||||
							
								
								
									
										64
									
								
								packages/mermaid/src/diagrams/pie/pieTypes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								packages/mermaid/src/diagrams/pie/pieTypes.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,64 @@
 | 
			
		||||
import type { PieDiagramConfig } from '../../config.type.js';
 | 
			
		||||
import type { DiagramDB, ParseDirectiveDefinition } from '../../diagram-api/types.js';
 | 
			
		||||
 | 
			
		||||
export interface PieFields {
 | 
			
		||||
  sections: Sections;
 | 
			
		||||
  showData: boolean;
 | 
			
		||||
  config: PieDiagramConfig;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface PieStyleOptions {
 | 
			
		||||
  fontFamily: string;
 | 
			
		||||
  pie1: string;
 | 
			
		||||
  pie2: string;
 | 
			
		||||
  pie3: string;
 | 
			
		||||
  pie4: string;
 | 
			
		||||
  pie5: string;
 | 
			
		||||
  pie6: string;
 | 
			
		||||
  pie7: string;
 | 
			
		||||
  pie8: string;
 | 
			
		||||
  pie9: string;
 | 
			
		||||
  pie10: string;
 | 
			
		||||
  pie11: string;
 | 
			
		||||
  pie12: string;
 | 
			
		||||
  pieTitleTextSize: string;
 | 
			
		||||
  pieTitleTextColor: string;
 | 
			
		||||
  pieSectionTextSize: string;
 | 
			
		||||
  pieSectionTextColor: string;
 | 
			
		||||
  pieLegendTextSize: string;
 | 
			
		||||
  pieLegendTextColor: string;
 | 
			
		||||
  pieStrokeColor: string;
 | 
			
		||||
  pieStrokeWidth: string;
 | 
			
		||||
  pieOuterStrokeWidth: string;
 | 
			
		||||
  pieOuterStrokeColor: string;
 | 
			
		||||
  pieOpacity: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type Sections = Record<string, number>;
 | 
			
		||||
 | 
			
		||||
export interface D3Sections {
 | 
			
		||||
  label: string;
 | 
			
		||||
  value: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface PieDB extends DiagramDB {
 | 
			
		||||
  // config
 | 
			
		||||
  getConfig: () => Required<PieDiagramConfig>;
 | 
			
		||||
 | 
			
		||||
  // common db
 | 
			
		||||
  parseDirective: ParseDirectiveDefinition;
 | 
			
		||||
  clear: () => void;
 | 
			
		||||
  setDiagramTitle: (title: string) => void;
 | 
			
		||||
  getDiagramTitle: () => string;
 | 
			
		||||
  setAccTitle: (title: string) => void;
 | 
			
		||||
  getAccTitle: () => string;
 | 
			
		||||
  setAccDescription: (describetion: string) => void;
 | 
			
		||||
  getAccDescription: () => string;
 | 
			
		||||
 | 
			
		||||
  // diagram db
 | 
			
		||||
  addSection: (label: string, value: number) => void;
 | 
			
		||||
  getSections: () => Sections;
 | 
			
		||||
  cleanupValue: (value: string) => number;
 | 
			
		||||
  setShowData: (toggle: boolean) => void;
 | 
			
		||||
  getShowData: () => boolean;
 | 
			
		||||
}
 | 
			
		||||
@@ -3,7 +3,7 @@ import { select, selectAll } from 'd3';
 | 
			
		||||
import svgDraw, { ACTOR_TYPE_WIDTH, drawText, fixLifeLineHeights } from './svgDraw.js';
 | 
			
		||||
import { log } from '../../logger.js';
 | 
			
		||||
import common from '../common/common.js';
 | 
			
		||||
import * as svgDrawCommon from '../common/svgDrawCommon';
 | 
			
		||||
import * as svgDrawCommon from '../common/svgDrawCommon.js';
 | 
			
		||||
import * as configApi from '../../config.js';
 | 
			
		||||
import assignWithDepth from '../../assignWithDepth.js';
 | 
			
		||||
import utils from '../../utils.js';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import common from '../common/common.js';
 | 
			
		||||
import * as svgDrawCommon from '../common/svgDrawCommon';
 | 
			
		||||
import * as svgDrawCommon from '../common/svgDrawCommon.js';
 | 
			
		||||
import { addFunction } from '../../interactionDb.js';
 | 
			
		||||
import { ZERO_WIDTH_SPACE, parseFontSize } from '../../utils.js';
 | 
			
		||||
import { sanitizeUrl } from '@braintree/sanitize-url';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { arc as d3arc } from 'd3';
 | 
			
		||||
import * as svgDrawCommon from '../common/svgDrawCommon';
 | 
			
		||||
import * as svgDrawCommon from '../common/svgDrawCommon.js';
 | 
			
		||||
 | 
			
		||||
export const drawRect = function (elem, rectData) {
 | 
			
		||||
  return svgDrawCommon.drawRect(elem, rectData);
 | 
			
		||||
 
 | 
			
		||||
@@ -101,7 +101,7 @@ function sidebarAll() {
 | 
			
		||||
  return [
 | 
			
		||||
    {
 | 
			
		||||
      text: '📔 Introduction',
 | 
			
		||||
      collapsible: true,
 | 
			
		||||
      collapsed: false,
 | 
			
		||||
      items: [
 | 
			
		||||
        { text: 'About Mermaid', link: '/intro/' },
 | 
			
		||||
        { text: 'Deployment', link: '/intro/n00b-gettingStarted' },
 | 
			
		||||
@@ -123,7 +123,7 @@ function sidebarSyntax() {
 | 
			
		||||
  return [
 | 
			
		||||
    {
 | 
			
		||||
      text: '📊 Diagram Syntax',
 | 
			
		||||
      collapsible: true,
 | 
			
		||||
      collapsed: false,
 | 
			
		||||
      items: [
 | 
			
		||||
        { text: 'Flowchart', link: '/syntax/flowchart' },
 | 
			
		||||
        { text: 'Sequence Diagram', link: '/syntax/sequenceDiagram' },
 | 
			
		||||
@@ -154,7 +154,7 @@ function sidebarConfig() {
 | 
			
		||||
  return [
 | 
			
		||||
    {
 | 
			
		||||
      text: '⚙️ Deployment and Configuration',
 | 
			
		||||
      collapsible: true,
 | 
			
		||||
      collapsed: false,
 | 
			
		||||
      items: [
 | 
			
		||||
        { text: 'Configuration', link: '/config/configuration' },
 | 
			
		||||
        { text: 'Tutorials', link: '/config/Tutorials' },
 | 
			
		||||
@@ -176,7 +176,7 @@ function sidebarEcosystem() {
 | 
			
		||||
  return [
 | 
			
		||||
    {
 | 
			
		||||
      text: '📚 Ecosystem',
 | 
			
		||||
      collapsible: true,
 | 
			
		||||
      collapsed: false,
 | 
			
		||||
      items: [
 | 
			
		||||
        { text: 'Showcases', link: '/ecosystem/showcases' },
 | 
			
		||||
        { text: 'Use-Cases and Integrations', link: '/ecosystem/integrations' },
 | 
			
		||||
@@ -189,7 +189,7 @@ function sidebarCommunity() {
 | 
			
		||||
  return [
 | 
			
		||||
    {
 | 
			
		||||
      text: '🙌 Contributions and Community',
 | 
			
		||||
      collapsible: true,
 | 
			
		||||
      collapsed: false,
 | 
			
		||||
      items: [
 | 
			
		||||
        { text: 'Overview for Beginners', link: '/community/n00b-overview' },
 | 
			
		||||
        ...sidebarCommunityDevelopContribute(),
 | 
			
		||||
@@ -207,7 +207,7 @@ function sidebarCommunityDevelopContribute() {
 | 
			
		||||
    {
 | 
			
		||||
      text: 'Contributing to Mermaid',
 | 
			
		||||
      link: page_path + '#contributing-to-mermaid',
 | 
			
		||||
      collapsible: true,
 | 
			
		||||
      collapsed: false,
 | 
			
		||||
      items: [
 | 
			
		||||
        {
 | 
			
		||||
          text: 'Technical Requirements and Setup',
 | 
			
		||||
@@ -238,7 +238,7 @@ function sidebarNews() {
 | 
			
		||||
  return [
 | 
			
		||||
    {
 | 
			
		||||
      text: '📰 Latest News',
 | 
			
		||||
      collapsible: true,
 | 
			
		||||
      collapsed: false,
 | 
			
		||||
      items: [
 | 
			
		||||
        { text: 'Announcements', link: '/news/announcements' },
 | 
			
		||||
        { text: 'Blog', link: '/news/blog' },
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,9 @@ const getBaseFile = (url: URL): Redirect => {
 | 
			
		||||
  return { path, id };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Used to redirect old documentation pages to corresponding new pages.
 | 
			
		||||
 */
 | 
			
		||||
const idRedirectMap: Record<string, string> = {
 | 
			
		||||
  '8.6.0_docs': '',
 | 
			
		||||
  accessibility: 'config/theming',
 | 
			
		||||
@@ -68,6 +71,9 @@ const idRedirectMap: Record<string, string> = {
 | 
			
		||||
  'user-journey': 'syntax/userJourney',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Used to redirect pages that have been moved in the vitepress site.
 | 
			
		||||
 */
 | 
			
		||||
const urlRedirectMap: Record<string, string> = {
 | 
			
		||||
  '/misc/faq.html': 'configure/faq.html',
 | 
			
		||||
  '/syntax/c4c.html': 'syntax/c4.html',
 | 
			
		||||
 
 | 
			
		||||
@@ -64,7 +64,21 @@ pnpm test
 | 
			
		||||
 | 
			
		||||
The `test` script and others are in the top-level `package.json` file.
 | 
			
		||||
 | 
			
		||||
All tests should run successfully without any errors or failures. (You might see _lint_ or _formatting_ warnings; those are ok during this step.)
 | 
			
		||||
All tests should run successfully without any errors or failures. (You might see _lint_ or _formatting_ "warnings"; those are ok during this step.)
 | 
			
		||||
 | 
			
		||||
#### 4. Make your changes
 | 
			
		||||
 | 
			
		||||
Now you are ready to make your changes!
 | 
			
		||||
Edit whichever files in `src` as required.
 | 
			
		||||
 | 
			
		||||
#### 5. See your changes
 | 
			
		||||
 | 
			
		||||
Open <http://localhost:9000> in your browser, after starting the dev server.
 | 
			
		||||
There is a list of demos that can be used to see and test your changes.
 | 
			
		||||
 | 
			
		||||
If you need a specific diagram, you can duplicate the `example.html` file in `/demos/dev` and add your own mermaid code to your copy.
 | 
			
		||||
That will be served at <http://localhost:9000/dev/your-file-name.html>.
 | 
			
		||||
After making code changes, the dev server will rebuild the mermaid library. You will need to reload the browser page yourself to see the changes. (PRs for auto reload are welcome!)
 | 
			
		||||
 | 
			
		||||
### Docker
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,12 @@ It is a JavaScript based diagramming and charting tool that renders Markdown-ins
 | 
			
		||||
 | 
			
		||||
<img src="/header.png" alt="" />
 | 
			
		||||
 | 
			
		||||
<div class='badges'>
 | 
			
		||||
 | 
			
		||||
[](https://github.com/mermaid-js/mermaid/actions/workflows/build.yml) [](https://www.npmjs.com/package/mermaid) [](https://bundlephobia.com/package/mermaid) [](https://coveralls.io/github/mermaid-js/mermaid?branch=master) [](https://www.jsdelivr.com/package/npm/mermaid) [](https://www.npmjs.com/package/mermaid) [](https://join.slack.com/t/mermaid-talk/shared_invite/enQtNzc4NDIyNzk4OTAyLWVhYjQxOTI2OTg4YmE1ZmJkY2Y4MTU3ODliYmIwOTY3NDJlYjA0YjIyZTdkMDMyZTUwOGI0NjEzYmEwODcwOTE) [](https://twitter.com/mermaidjs_)
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- Mermaid book banner -->
 | 
			
		||||
 | 
			
		||||
[](https://mermaid-js.github.io/mermaid/landing/)
 | 
			
		||||
@@ -166,8 +170,12 @@ The above command generates files into the `dist` folder and publishes them to <
 | 
			
		||||
 | 
			
		||||
## Contributors
 | 
			
		||||
 | 
			
		||||
<div class='badges'>
 | 
			
		||||
 | 
			
		||||
[](https://github.com/mermaid-js/mermaid/issues?q=is%3Aissue+is%3Aopen+label%3A%22Good+first+issue%21%22) [](https://github.com/mermaid-js/mermaid/graphs/contributors) [](https://github.com/mermaid-js/mermaid/graphs/contributors)
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
Mermaid is a growing community and is always accepting new contributors. There's a lot of different ways to help out and we're always looking for extra hands! Look at [this issue](https://github.com/mermaid-js/mermaid/issues/866) if you want to know where to start helping out.
 | 
			
		||||
 | 
			
		||||
Detailed information about how to contribute can be found in the [contribution guide](https://github.com/mermaid-js/mermaid/blob/develop/CONTRIBUTING.md)
 | 
			
		||||
@@ -201,20 +209,14 @@ A quick note from Knut Sveidqvist:
 | 
			
		||||
_Mermaid was created by Knut Sveidqvist for easier documentation._
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
  #contributors + p,
 | 
			
		||||
  #about-mermaid + p + p + blockquote + img + p
 | 
			
		||||
  {
 | 
			
		||||
    display: flex
 | 
			
		||||
 .badges > p {
 | 
			
		||||
    display: flex;
 | 
			
		||||
  }
 | 
			
		||||
  .badges > p > a {
 | 
			
		||||
    margin: 0 0.5rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #contributors + p a,
 | 
			
		||||
  #about-mermaid + p + p + blockquote + img + p a
 | 
			
		||||
  {
 | 
			
		||||
    margin: 0 0.5rem
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .dark #VPContent > div > div > div.content > div > main > div > div > img
 | 
			
		||||
  {
 | 
			
		||||
  .dark #VPContent > div > div > div.content > div > main > div > div > img {
 | 
			
		||||
    filter: invert(1) hue-rotate(217deg)  contrast(0.72);
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -17,21 +17,22 @@
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@vueuse/core": "^10.1.0",
 | 
			
		||||
    "jiti": "^1.18.2",
 | 
			
		||||
    "vue": "^3.2.47"
 | 
			
		||||
    "vue": "^3.3",
 | 
			
		||||
    "mermaid": "workspace:^"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@iconify-json/carbon": "^1.1.16",
 | 
			
		||||
    "@unocss/reset": "^0.54.0",
 | 
			
		||||
    "@unocss/reset": "^0.55.2",
 | 
			
		||||
    "@vite-pwa/vitepress": "^0.2.0",
 | 
			
		||||
    "@vitejs/plugin-vue": "^4.2.1",
 | 
			
		||||
    "fast-glob": "^3.2.12",
 | 
			
		||||
    "https-localhost": "^4.7.1",
 | 
			
		||||
    "pathe": "^1.1.0",
 | 
			
		||||
    "unocss": "^0.54.0",
 | 
			
		||||
    "unocss": "^0.55.2",
 | 
			
		||||
    "unplugin-vue-components": "^0.25.0",
 | 
			
		||||
    "vite": "^4.3.9",
 | 
			
		||||
    "vite-plugin-pwa": "^0.16.0",
 | 
			
		||||
    "vitepress": "1.0.0-beta.7",
 | 
			
		||||
    "vitepress": "1.0.0-rc.4",
 | 
			
		||||
    "workbox-window": "^7.0.0"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -471,6 +471,29 @@ flowchart LR
 | 
			
		||||
  B1 --> B2
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### Limitation
 | 
			
		||||
 | 
			
		||||
If any of a subgraph's nodes are linked to the outside, subgraph direction will be ignored. Instead the subgraph will inherit the direction of the parent graph:
 | 
			
		||||
 | 
			
		||||
```mermaid-example
 | 
			
		||||
flowchart LR
 | 
			
		||||
    subgraph subgraph1
 | 
			
		||||
        direction TB
 | 
			
		||||
        top1[top] --> bottom1[bottom]
 | 
			
		||||
    end
 | 
			
		||||
    subgraph subgraph2
 | 
			
		||||
        direction TB
 | 
			
		||||
        top2[top] --> bottom2[bottom]
 | 
			
		||||
    end
 | 
			
		||||
    %% ^ These subgraphs are identical, except for the links to them:
 | 
			
		||||
 | 
			
		||||
    %% Link *to* subgraph1: subgraph1 direction is mantained
 | 
			
		||||
    outside --> subgraph1
 | 
			
		||||
    %% Link *within* subgraph2:
 | 
			
		||||
    %% subgraph2 inherits the direction of the top-level graph (LR)
 | 
			
		||||
    outside ---> top2
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Markdown Strings
 | 
			
		||||
 | 
			
		||||
The "Markdown Strings" feature enhances flowcharts and mind maps by offering a more versatile string type, which supports text formatting options such as bold and italics, and automatically wraps text within labels.
 | 
			
		||||
 
 | 
			
		||||
@@ -7,11 +7,8 @@ import { MermaidConfig } from './config.type.js';
 | 
			
		||||
import { log } from './logger.js';
 | 
			
		||||
import utils from './utils.js';
 | 
			
		||||
import { mermaidAPI, ParseOptions, RenderResult } from './mermaidAPI.js';
 | 
			
		||||
import {
 | 
			
		||||
  registerLazyLoadedDiagrams,
 | 
			
		||||
  loadRegisteredDiagrams,
 | 
			
		||||
  detectType,
 | 
			
		||||
} from './diagram-api/detectType.js';
 | 
			
		||||
import { registerLazyLoadedDiagrams, detectType } from './diagram-api/detectType.js';
 | 
			
		||||
import { loadRegisteredDiagrams } from './diagram-api/loadDiagram.js';
 | 
			
		||||
import type { ParseErrorFunction } from './Diagram.js';
 | 
			
		||||
import { isDetailedError } from './utils.js';
 | 
			
		||||
import type { DetailedError } from './utils.js';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import { log } from './logger.js';
 | 
			
		||||
import { SVG } from './diagram-api/types.js';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Applies d3 attributes
 | 
			
		||||
@@ -36,7 +35,7 @@ export const calculateSvgSizeAttrs = function (height, width, useMaxWidth) {
 | 
			
		||||
/**
 | 
			
		||||
 * Applies attributes from `calculateSvgSizeAttrs`
 | 
			
		||||
 *
 | 
			
		||||
 * @param {SVG} svgElem The SVG Element to configure
 | 
			
		||||
 * @param {import('./diagram-api/types.js').SVG} svgElem The SVG Element to configure
 | 
			
		||||
 * @param {number} height The height of the SVG
 | 
			
		||||
 * @param {number} width The width of the SVG
 | 
			
		||||
 * @param {boolean} useMaxWidth Whether or not to use max-width and set width to 100%
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ import flowchartElk from './diagrams/flowchart/elk/styles.js';
 | 
			
		||||
import er from './diagrams/er/styles.js';
 | 
			
		||||
import git from './diagrams/git/styles.js';
 | 
			
		||||
import gantt from './diagrams/gantt/styles.js';
 | 
			
		||||
import pie from './diagrams/pie/styles.js';
 | 
			
		||||
import pie from './diagrams/pie/pieStyles.js';
 | 
			
		||||
import requirement from './diagrams/requirement/styles.js';
 | 
			
		||||
import sequence from './diagrams/sequence/styles.js';
 | 
			
		||||
import state from './diagrams/state/styles.js';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { vi } from 'vitest';
 | 
			
		||||
import utils from './utils.js';
 | 
			
		||||
import utils, { cleanAndMerge } from './utils.js';
 | 
			
		||||
import assignWithDepth from './assignWithDepth.js';
 | 
			
		||||
import { detectType } from './diagram-api/detectType.js';
 | 
			
		||||
import { addDiagrams } from './diagram-api/diagram-orchestration.js';
 | 
			
		||||
@@ -10,51 +10,51 @@ addDiagrams();
 | 
			
		||||
 | 
			
		||||
describe('when assignWithDepth: should merge objects within objects', function () {
 | 
			
		||||
  it('should handle simple, depth:1 types (identity)', function () {
 | 
			
		||||
    let config_0 = { foo: 'bar', bar: 0 };
 | 
			
		||||
    let config_1 = { foo: 'bar', bar: 0 };
 | 
			
		||||
    let result = assignWithDepth(config_0, config_1);
 | 
			
		||||
    const config_0 = { foo: 'bar', bar: 0 };
 | 
			
		||||
    const config_1 = { foo: 'bar', bar: 0 };
 | 
			
		||||
    const result = assignWithDepth(config_0, config_1);
 | 
			
		||||
    expect(result).toEqual(config_1);
 | 
			
		||||
  });
 | 
			
		||||
  it('should handle simple, depth:1 types (dst: undefined)', function () {
 | 
			
		||||
    let config_0 = undefined;
 | 
			
		||||
    let config_1 = { foo: 'bar', bar: 0 };
 | 
			
		||||
    let result = assignWithDepth(config_0, config_1);
 | 
			
		||||
    const config_0 = undefined;
 | 
			
		||||
    const config_1 = { foo: 'bar', bar: 0 };
 | 
			
		||||
    const result = assignWithDepth(config_0, config_1);
 | 
			
		||||
    expect(result).toEqual(config_1);
 | 
			
		||||
  });
 | 
			
		||||
  it('should handle simple, depth:1 types (src: undefined)', function () {
 | 
			
		||||
    let config_0 = { foo: 'bar', bar: 0 };
 | 
			
		||||
    let config_1 = undefined;
 | 
			
		||||
    let result = assignWithDepth(config_0, config_1);
 | 
			
		||||
    const config_0 = { foo: 'bar', bar: 0 };
 | 
			
		||||
    const config_1 = undefined;
 | 
			
		||||
    const result = assignWithDepth(config_0, config_1);
 | 
			
		||||
    expect(result).toEqual(config_0);
 | 
			
		||||
  });
 | 
			
		||||
  it('should handle simple, depth:1 types (merge)', function () {
 | 
			
		||||
    let config_0 = { foo: 'bar', bar: 0 };
 | 
			
		||||
    let config_1 = { foo: 'foo' };
 | 
			
		||||
    let result = assignWithDepth(config_0, config_1);
 | 
			
		||||
    const config_0 = { foo: 'bar', bar: 0 };
 | 
			
		||||
    const config_1 = { foo: 'foo' };
 | 
			
		||||
    const result = assignWithDepth(config_0, config_1);
 | 
			
		||||
    expect(result).toEqual({ foo: 'foo', bar: 0 });
 | 
			
		||||
  });
 | 
			
		||||
  it('should handle depth:2 types (dst: orphan)', function () {
 | 
			
		||||
    let config_0 = { foo: 'bar', bar: { foo: 'bar' } };
 | 
			
		||||
    let config_1 = { foo: 'bar' };
 | 
			
		||||
    let result = assignWithDepth(config_0, config_1);
 | 
			
		||||
    const config_0 = { foo: 'bar', bar: { foo: 'bar' } };
 | 
			
		||||
    const config_1 = { foo: 'bar' };
 | 
			
		||||
    const result = assignWithDepth(config_0, config_1);
 | 
			
		||||
    expect(result).toEqual(config_0);
 | 
			
		||||
  });
 | 
			
		||||
  it('should handle depth:2 types (dst: object, src: simple type)', function () {
 | 
			
		||||
    let config_0 = { foo: 'bar', bar: { foo: 'bar' } };
 | 
			
		||||
    let config_1 = { foo: 'foo', bar: 'should NOT clobber' };
 | 
			
		||||
    let result = assignWithDepth(config_0, config_1);
 | 
			
		||||
    const config_0 = { foo: 'bar', bar: { foo: 'bar' } };
 | 
			
		||||
    const config_1 = { foo: 'foo', bar: 'should NOT clobber' };
 | 
			
		||||
    const result = assignWithDepth(config_0, config_1);
 | 
			
		||||
    expect(result).toEqual({ foo: 'foo', bar: { foo: 'bar' } });
 | 
			
		||||
  });
 | 
			
		||||
  it('should handle depth:2 types (src: orphan)', function () {
 | 
			
		||||
    let config_0 = { foo: 'bar' };
 | 
			
		||||
    let config_1 = { foo: 'bar', bar: { foo: 'bar' } };
 | 
			
		||||
    let result = assignWithDepth(config_0, config_1);
 | 
			
		||||
    const config_0 = { foo: 'bar' };
 | 
			
		||||
    const config_1 = { foo: 'bar', bar: { foo: 'bar' } };
 | 
			
		||||
    const result = assignWithDepth(config_0, config_1);
 | 
			
		||||
    expect(result).toEqual(config_1);
 | 
			
		||||
  });
 | 
			
		||||
  it('should handle depth:2 types (merge)', function () {
 | 
			
		||||
    let config_0 = { foo: 'bar', bar: { foo: 'bar' }, boofar: 1 };
 | 
			
		||||
    let config_1 = { foo: 'foo', bar: { bar: 0 }, foobar: 'foobar' };
 | 
			
		||||
    let result = assignWithDepth(config_0, config_1);
 | 
			
		||||
    const config_0 = { foo: 'bar', bar: { foo: 'bar' }, boofar: 1 };
 | 
			
		||||
    const config_1 = { foo: 'foo', bar: { bar: 0 }, foobar: 'foobar' };
 | 
			
		||||
    const result = assignWithDepth(config_0, config_1);
 | 
			
		||||
    expect(result).toEqual({
 | 
			
		||||
      foo: 'foo',
 | 
			
		||||
      bar: { foo: 'bar', bar: 0 },
 | 
			
		||||
@@ -63,17 +63,17 @@ describe('when assignWithDepth: should merge objects within objects', function (
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
  it('should handle depth:3 types (merge with clobber because assignWithDepth::depth == 2)', function () {
 | 
			
		||||
    let config_0 = {
 | 
			
		||||
    const config_0 = {
 | 
			
		||||
      foo: 'bar',
 | 
			
		||||
      bar: { foo: 'bar', bar: { foo: { message: 'this', willbe: 'clobbered' } } },
 | 
			
		||||
      boofar: 1,
 | 
			
		||||
    };
 | 
			
		||||
    let config_1 = {
 | 
			
		||||
    const config_1 = {
 | 
			
		||||
      foo: 'foo',
 | 
			
		||||
      bar: { foo: 'foo', bar: { foo: { message: 'clobbered other foo' } } },
 | 
			
		||||
      foobar: 'foobar',
 | 
			
		||||
    };
 | 
			
		||||
    let result = assignWithDepth(config_0, config_1);
 | 
			
		||||
    const result = assignWithDepth(config_0, config_1);
 | 
			
		||||
    expect(result).toEqual({
 | 
			
		||||
      foo: 'foo',
 | 
			
		||||
      bar: { foo: 'foo', bar: { foo: { message: 'clobbered other foo' } } },
 | 
			
		||||
@@ -82,7 +82,7 @@ describe('when assignWithDepth: should merge objects within objects', function (
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
  it('should handle depth:3 types (merge with clobber because assignWithDepth::depth == 1)', function () {
 | 
			
		||||
    let config_0 = {
 | 
			
		||||
    const config_0 = {
 | 
			
		||||
      foo: 'bar',
 | 
			
		||||
      bar: {
 | 
			
		||||
        foo: 'bar',
 | 
			
		||||
@@ -90,12 +90,12 @@ describe('when assignWithDepth: should merge objects within objects', function (
 | 
			
		||||
      },
 | 
			
		||||
      boofar: 1,
 | 
			
		||||
    };
 | 
			
		||||
    let config_1 = {
 | 
			
		||||
    const config_1 = {
 | 
			
		||||
      foo: 'foo',
 | 
			
		||||
      bar: { foo: 'foo', bar: { foo: { message: 'this' } } },
 | 
			
		||||
      foobar: 'foobar',
 | 
			
		||||
    };
 | 
			
		||||
    let result = assignWithDepth(config_0, config_1, { depth: 1 });
 | 
			
		||||
    const result = assignWithDepth(config_0, config_1, { depth: 1 });
 | 
			
		||||
    expect(result).toEqual({
 | 
			
		||||
      foo: 'foo',
 | 
			
		||||
      bar: { foo: 'foo', bar: { foo: { message: 'this' } } },
 | 
			
		||||
@@ -104,17 +104,17 @@ describe('when assignWithDepth: should merge objects within objects', function (
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
  it('should handle depth:3 types (merge with no clobber because assignWithDepth::depth == 3)', function () {
 | 
			
		||||
    let config_0 = {
 | 
			
		||||
    const config_0 = {
 | 
			
		||||
      foo: 'bar',
 | 
			
		||||
      bar: { foo: 'bar', bar: { foo: { message: '', willbe: 'present' } } },
 | 
			
		||||
      boofar: 1,
 | 
			
		||||
    };
 | 
			
		||||
    let config_1 = {
 | 
			
		||||
    const config_1 = {
 | 
			
		||||
      foo: 'foo',
 | 
			
		||||
      bar: { foo: 'foo', bar: { foo: { message: 'this' } } },
 | 
			
		||||
      foobar: 'foobar',
 | 
			
		||||
    };
 | 
			
		||||
    let result = assignWithDepth(config_0, config_1, { depth: 3 });
 | 
			
		||||
    const result = assignWithDepth(config_0, config_1, { depth: 3 });
 | 
			
		||||
    expect(result).toEqual({
 | 
			
		||||
      foo: 'foo',
 | 
			
		||||
      bar: { foo: 'foo', bar: { foo: { message: 'this', willbe: 'present' } } },
 | 
			
		||||
@@ -125,8 +125,8 @@ describe('when assignWithDepth: should merge objects within objects', function (
 | 
			
		||||
});
 | 
			
		||||
describe('when memoizing', function () {
 | 
			
		||||
  it('should return the same value', function () {
 | 
			
		||||
    const fib = memoize(
 | 
			
		||||
      function (n, x, canary) {
 | 
			
		||||
    const fib: any = memoize(
 | 
			
		||||
      function (n: number, x: string, canary: { flag: boolean }) {
 | 
			
		||||
        canary.flag = true;
 | 
			
		||||
        if (n < 2) {
 | 
			
		||||
          return 1;
 | 
			
		||||
@@ -260,7 +260,7 @@ describe('when formatting urls', function () {
 | 
			
		||||
  it('should handle links', function () {
 | 
			
		||||
    const url = 'https://mermaid-js.github.io/mermaid/#/';
 | 
			
		||||
 | 
			
		||||
    let config = { securityLevel: 'loose' };
 | 
			
		||||
    const config = { securityLevel: 'loose' };
 | 
			
		||||
    let result = utils.formatUrl(url, config);
 | 
			
		||||
    expect(result).toEqual(url);
 | 
			
		||||
 | 
			
		||||
@@ -271,7 +271,7 @@ describe('when formatting urls', function () {
 | 
			
		||||
  it('should handle anchors', function () {
 | 
			
		||||
    const url = '#interaction';
 | 
			
		||||
 | 
			
		||||
    let config = { securityLevel: 'loose' };
 | 
			
		||||
    const config = { securityLevel: 'loose' };
 | 
			
		||||
    let result = utils.formatUrl(url, config);
 | 
			
		||||
    expect(result).toEqual(url);
 | 
			
		||||
 | 
			
		||||
@@ -282,7 +282,7 @@ describe('when formatting urls', function () {
 | 
			
		||||
  it('should handle mailto', function () {
 | 
			
		||||
    const url = 'mailto:user@user.user';
 | 
			
		||||
 | 
			
		||||
    let config = { securityLevel: 'loose' };
 | 
			
		||||
    const config = { securityLevel: 'loose' };
 | 
			
		||||
    let result = utils.formatUrl(url, config);
 | 
			
		||||
    expect(result).toEqual(url);
 | 
			
		||||
 | 
			
		||||
@@ -293,7 +293,7 @@ describe('when formatting urls', function () {
 | 
			
		||||
  it('should handle other protocols', function () {
 | 
			
		||||
    const url = 'notes://do-your-thing/id';
 | 
			
		||||
 | 
			
		||||
    let config = { securityLevel: 'loose' };
 | 
			
		||||
    const config = { securityLevel: 'loose' };
 | 
			
		||||
    let result = utils.formatUrl(url, config);
 | 
			
		||||
    expect(result).toEqual(url);
 | 
			
		||||
 | 
			
		||||
@@ -304,7 +304,7 @@ describe('when formatting urls', function () {
 | 
			
		||||
  it('should handle scripts', function () {
 | 
			
		||||
    const url = 'javascript:alert("test")';
 | 
			
		||||
 | 
			
		||||
    let config = { securityLevel: 'loose' };
 | 
			
		||||
    const config = { securityLevel: 'loose' };
 | 
			
		||||
    let result = utils.formatUrl(url, config);
 | 
			
		||||
    expect(result).toEqual(url);
 | 
			
		||||
 | 
			
		||||
@@ -425,6 +425,42 @@ describe('when parsing font sizes', function () {
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('handles unparseable input', function () {
 | 
			
		||||
    // @ts-expect-error Explicitly testing unparsable input
 | 
			
		||||
    expect(utils.parseFontSize({ fontSize: 14 })).toEqual([undefined, undefined]);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('cleanAndMerge', () => {
 | 
			
		||||
  test('should merge objects', () => {
 | 
			
		||||
    expect(cleanAndMerge({ a: 1, b: 2 }, { b: 3 })).toEqual({ a: 1, b: 3 });
 | 
			
		||||
    expect(cleanAndMerge({ a: 1 }, { a: 2 })).toEqual({ a: 2 });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test('should remove undefined values', () => {
 | 
			
		||||
    expect(cleanAndMerge({ a: 1, b: 2 }, { b: undefined })).toEqual({ a: 1, b: 2 });
 | 
			
		||||
    expect(cleanAndMerge({ a: 1, b: 2 }, { a: 2, b: undefined })).toEqual({ a: 2, b: 2 });
 | 
			
		||||
    expect(cleanAndMerge({ a: 1, b: { c: 2 } }, { a: 2, b: undefined })).toEqual({
 | 
			
		||||
      a: 2,
 | 
			
		||||
      b: { c: 2 },
 | 
			
		||||
    });
 | 
			
		||||
    // @ts-expect-error Explicitly testing different type
 | 
			
		||||
    expect(cleanAndMerge({ a: 1, b: { c: 2 } }, { a: 2, b: { c: undefined } })).toEqual({
 | 
			
		||||
      a: 2,
 | 
			
		||||
      b: { c: 2 },
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test('should create deep copies of object', () => {
 | 
			
		||||
    const input: { a: number; b?: number } = { a: 1 };
 | 
			
		||||
    const output = cleanAndMerge(input, { b: 2 });
 | 
			
		||||
    expect(output).toEqual({ a: 1, b: 2 });
 | 
			
		||||
    output.b = 3;
 | 
			
		||||
    expect(input).toEqual({ a: 1 });
 | 
			
		||||
 | 
			
		||||
    const inputDeep = { a: { b: 1 } };
 | 
			
		||||
    const outputDeep = cleanAndMerge(inputDeep, { a: { b: 2 } });
 | 
			
		||||
    expect(outputDeep).toEqual({ a: { b: 2 } });
 | 
			
		||||
    outputDeep.a.b = 3;
 | 
			
		||||
    expect(inputDeep).toEqual({ a: { b: 1 } });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -31,6 +31,8 @@ import { detectType } from './diagram-api/detectType.js';
 | 
			
		||||
import assignWithDepth from './assignWithDepth.js';
 | 
			
		||||
import { MermaidConfig } from './config.type.js';
 | 
			
		||||
import memoize from 'lodash-es/memoize.js';
 | 
			
		||||
import merge from 'lodash-es/merge.js';
 | 
			
		||||
import { directiveRegex } from './diagram-api/regexes.js';
 | 
			
		||||
 | 
			
		||||
export const ZERO_WIDTH_SPACE = '\u200b';
 | 
			
		||||
 | 
			
		||||
@@ -57,7 +59,7 @@ const d3CurveTypes = {
 | 
			
		||||
  curveStepAfter: curveStepAfter,
 | 
			
		||||
  curveStepBefore: curveStepBefore,
 | 
			
		||||
};
 | 
			
		||||
const directive = /%{2}{\s*(?:(\w+)\s*:|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi;
 | 
			
		||||
 | 
			
		||||
const directiveWithoutOpen =
 | 
			
		||||
  /\s*(?:(\w+)(?=:):|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi;
 | 
			
		||||
 | 
			
		||||
@@ -162,10 +164,10 @@ export const detectDirective = function (
 | 
			
		||||
    );
 | 
			
		||||
    let match;
 | 
			
		||||
    const result = [];
 | 
			
		||||
    while ((match = directive.exec(text)) !== null) {
 | 
			
		||||
    while ((match = directiveRegex.exec(text)) !== null) {
 | 
			
		||||
      // This is necessary to avoid infinite loops with zero-width matches
 | 
			
		||||
      if (match.index === directive.lastIndex) {
 | 
			
		||||
        directive.lastIndex++;
 | 
			
		||||
      if (match.index === directiveRegex.lastIndex) {
 | 
			
		||||
        directiveRegex.lastIndex++;
 | 
			
		||||
      }
 | 
			
		||||
      if (
 | 
			
		||||
        (match && !type) ||
 | 
			
		||||
@@ -802,7 +804,7 @@ export const calculateTextDimensions: (
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export const initIdGenerator = class iterator {
 | 
			
		||||
  constructor(deterministic, seed) {
 | 
			
		||||
  constructor(deterministic, seed?: any) {
 | 
			
		||||
    this.deterministic = deterministic;
 | 
			
		||||
    // TODO: Seed is only used for length?
 | 
			
		||||
    this.seed = seed;
 | 
			
		||||
@@ -994,12 +996,17 @@ export const parseFontSize = (fontSize: string | number | undefined): [number?,
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function cleanAndMerge<T>(defaultData: T, data?: Partial<T>): T {
 | 
			
		||||
  return merge({}, defaultData, data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  assignWithDepth,
 | 
			
		||||
  wrapLabel,
 | 
			
		||||
  calculateTextHeight,
 | 
			
		||||
  calculateTextWidth,
 | 
			
		||||
  calculateTextDimensions,
 | 
			
		||||
  cleanAndMerge,
 | 
			
		||||
  detectInit,
 | 
			
		||||
  detectDirective,
 | 
			
		||||
  isSubstringInArray,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1362
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1362
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -12,7 +12,7 @@
 | 
			
		||||
  "author": "",
 | 
			
		||||
  "license": "ISC",
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "webpack": "^5.74.0",
 | 
			
		||||
    "webpack": "^5.88.2",
 | 
			
		||||
    "webpack-cli": "^4.10.0",
 | 
			
		||||
    "webpack-dev-server": "^4.11.1"
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user