Compare commits

...

137 Commits

Author SHA1 Message Date
Sidharth Vinod
3944e9ac00 chore: Remove circular dependency 2023-08-22 13:36:32 +05:30
Sidharth Vinod
7e251ee8bc Merge remote-tracking branch 'origin/develop' into feature/frontmatterConfig
* origin/develop:
  chore: Remove duplicate CI action
  chore: Add circular dependency check in CI
  refactor: Remove circular dependencies
2023-08-22 13:30:36 +05:30
Knut Sveidqvist
6e0f41180f Merge pull request #4761 from mermaid-js/sidv/RemoveCircularDeps
Remove Circular Dependencies
2023-08-22 07:37:32 +00:00
Sidharth Vinod
52f05552a1 chore: Remove duplicate CI action 2023-08-22 11:42:12 +05:30
Sidharth Vinod
bb01b3972d chore: Add circular dependency check in CI 2023-08-22 11:38:55 +05:30
Sidharth Vinod
1a50a326cb refactor: Remove circular dependencies 2023-08-22 11:38:25 +05:30
Sidharth Vinod
fd45dbfc14 Merge branch 'develop' into feature/frontmatterConfig 2023-08-21 14:18:12 +00:00
sidharthv96
5c9eafabae Update docs 2023-08-21 09:36:24 +00:00
Sidharth Vinod
2b9dc0ea80 docs: Add frontmatter config demos 2023-08-21 15:02:29 +05:30
Sidharth Vinod
8ac7dc81e0 docs: Add frontmatter config docs 2023-08-21 15:02:05 +05:30
Sidharth Vinod
2967b3c1bb fix: XSS vulnerability 2023-08-21 14:25:25 +05:30
Sidharth Vinod
534fd85339 chore: Minor typo fixes 2023-08-21 12:53:29 +05:30
Sidharth Vinod
7298008374 Merge branch 'feature/frontmatterConfig' of https://github.com/mermaid-js/mermaid into feature/frontmatterConfig
* 'feature/frontmatterConfig' of https://github.com/mermaid-js/mermaid:
  Update docs
2023-08-21 12:30:23 +05:30
Sidharth Vinod
844a175039 chore: Add test with both frontmatter and directive 2023-08-21 12:30:04 +05:30
sidharthv96
fd35a54735 Update docs 2023-08-21 06:43:15 +00:00
Sidharth Vinod
a6e6c3fb18 feat: Add support for config in frontmatter 2023-08-21 12:09:38 +05:30
Sidharth Vinod
31c0a0cbab deps: Update unocss and webpack to address vulnerability. 2023-08-21 10:26:41 +05:30
Sidharth Vinod
767baa4ec6 chore: Fix type in assignWithDepth 2023-08-21 10:18:23 +05:30
Sidharth Vinod
f422a66dde refactor: Move sanitizeDirective into addDirective 2023-08-21 10:17:55 +05:30
Sidharth Vinod
fae976e994 refactor: Rename and cleanup directiveSanitizer 2023-08-21 10:13:48 +05:30
Sidharth Vinod
f4c62436ea Merge pull request #4753 from mermaid-js/renovate/patch-all-patch
chore(deps): update all patch dependencies (patch)
2023-08-21 03:53:05 +00:00
renovate[bot]
21d0998db8 chore(deps): update all patch dependencies 2023-08-21 03:01:21 +00:00
Sidharth Vinod
0899f7918a Merge pull request #4752 from mermaid-js/chore/add-more-release-notes-categories
ci(release-drafter): add more release notes categories
2023-08-21 02:55:59 +00:00
Alois Klink
7a547abd89 ci(release-drafter): add more release notes categories
Add the following new categories to the release notes:
  - **BREAKING CHANGES**
  - Performance
  - Documentation

I've also made the `feature`, `fix`, and `chore` label point to
their appropriate section.
2023-08-20 23:42:18 +01:00
Sidharth Vinod
72fa3488b5 Merge pull request #4501 from Yokozuna59/standardized-pie-definitions
standardized pie definitions
2023-08-20 14:56:42 +00:00
Sidharth Vinod
cfffba2817 chore: Add comments on redirectMaps 2023-08-20 00:59:58 +05:30
Reda Al Sulais
e7ee3eb9ea Merge branch 'develop' into standardized-pie-definitions 2023-08-19 01:07:55 +03:00
Nikolay Rozhkov
b85c011cd1 Merge pull request #4742 from nirname/docs/2910_small-fixes
Fixes to Docs sidebar, main page and badges
2023-08-17 17:59:12 +00:00
Nikolay Rozhkov
aec97d68cc Fixed wrong collapsible option at the left menu, updated vue
Fixed badges

Rely on classes instead of DOM structure

Prettier

Update docs
2023-08-17 16:37:22 +03:00
Sidharth Vinod
7b7e281ec7 Merge pull request #4740 from jason-curtis/issue-2509-subgraph-direction-limitation
Documentation for #2509
2023-08-17 04:57:56 +00:00
Sidharth Vinod
35cc34d422 chore: Move SVG import to comment. 2023-08-17 10:28:50 +05:30
Jason Curtis
edb6ceae43 build docs 2023-08-17 03:48:00 +00:00
Jason Curtis
ef8b75a6da Remove whitespace on empty line 2023-08-17 03:32:42 +00:00
Reda Al Sulais
02daf5417b Merge branch 'standardized-pie-definitions' of https://github.com/Yokozuna59/mermaid into standardized-pie-definitions 2023-08-17 02:22:12 +03:00
Reda Al Sulais
77d8e15dc4 remove chart from pie.spec.ts description 2023-08-17 02:21:15 +03:00
Yokozuna59
62142089f1 Update docs 2023-08-16 22:47:51 +00:00
Reda Al Sulais
defe40692a change defaultConfig type to RequiredDeep and use it in pieDb 2023-08-17 01:42:41 +03:00
Jason Curtis
337ff3c32b Documentation for #2509 2023-08-16 21:12:32 +00:00
Reda Al Sulais
744cc792f4 Merge branch 'develop' into standardized-pie-definitions 2023-08-15 23:27:03 +03:00
Reda Al Sulais
99978da55b Merge pull request #4724 from Yokozuna59/chore/convert-svgDrawCommon-to-ts
convert `diagrams/common/svgDrawCommon.js` to ts
2023-08-15 16:41:11 +00:00
Reda Al Sulais
50eb3cf1c9 Merge branch 'develop' into chore/convert-svgDrawCommon-to-ts 2023-08-15 19:37:02 +03:00
Reda Al Sulais
c1b9c54fc9 use DiagramStylesProvider in pieStyles.ts 2023-08-15 17:56:18 +03:00
Reda Al Sulais
0d179c501e Merge branch 'develop' into standardized-pie-definitions 2023-08-15 14:01:09 +03:00
Sidharth Vinod
085e8f78b3 Merge pull request #4731 from mermaid-js/renovate/patch-all-patch
Update all patch dependencies (patch)
2023-08-14 11:05:49 +00:00
Sidharth Vinod
f9d4a62609 Merge pull request #4732 from mermaid-js/renovate/all-minor
Update all minor dependencies (minor)
2023-08-14 11:04:27 +00:00
renovate[bot]
2a8374312f Update all minor dependencies 2023-08-14 03:07:45 +00:00
renovate[bot]
b60410161d Update all patch dependencies 2023-08-14 02:02:28 +00:00
Reda Al Sulais
d219f92a19 Merge branch 'develop' into chore/convert-svgDrawCommon-to-ts 2023-08-13 17:45:37 +03:00
Reda Al Sulais
3f3a7340e3 remove setConfig and resetConfig in pie 2023-08-12 21:18:07 +03:00
Reda Al Sulais
cb5f70c139 add structuredClone in pie getConfig 2023-08-12 21:09:14 +03:00
Reda Al Sulais
99c1758490 make more RectData required and remove optional assignment 2023-08-12 20:25:10 +03:00
Sidharth Vinod
43217e1395 Merge branch 'master' into develop
* master:
  Version update and adjusted error diagram
  Fix for broker error diagram related #4178
  Adding new flowchart tests related to issue #2139
  Fix for interim issue with classes in state diagrams
  #2139 Applying user defined classes properly when calculating shape width
  chore: Ignore localhost
  Update packages/mermaid/src/docs/community/development.md
  Update docs/community/development.md
  docs: Add development example page.
2023-08-12 17:04:25 +05:30
Sidharth Vinod
aa34b99203 Merge remote-tracking branch 'origin/release/10.3.1'
* origin/release/10.3.1: (85 commits)
  Version update and adjusted error diagram
  Fix for broker error diagram related #4178
  Adding new flowchart tests related to issue #2139
  Fix for interim issue with classes in state diagrams
  use default export in `error` diagram
  create `ParserDefinition` type
  standardized `error` diagram'
  #2139 Applying user defined classes properly when calculating shape width
  style: format packages/mermaid/src/config.type.ts
  build(types): use prettier conf on config.types.ts
  Syntax Update CONTRIBUTING.md
  docs: Correct detectType filename
  chore: Minor cleanups
  chore: remove comment
  chore: Remove comments, cleanup
  fix!(deps): fix zenuml style leakage. update @zenuml/core to ^3.0.6 to fix the style leakage.
  Update selectSvgElement.ts
  create `Group` type
  Add specialChars in textNoTagsToken, alphaNumToken
  Return Unicode Text to idStringToken definition
  ...
2023-08-12 17:04:06 +05:30
Reda Al Sulais
541ee1eade Merge remote-tracking branch 'upstream/develop' into chore/convert-svgDrawCommon-to-ts 2023-08-11 23:07:45 +03:00
Reda Al Sulais
f01ad644e3 Merge remote-tracking branch 'upstream/develop' into standardized-pie-definitions
Signed-off-by: Reda Al Sulais <u.yokozuna@gmail.com>
2023-08-11 22:44:05 +03:00
Reda Al Sulais
9538233573 use lineBreakRegex in svgDrawCommon 2023-08-11 21:16:53 +03:00
Reda Al Sulais
5a2ea7c297 fix svgDrawCommon import by adding .js 2023-08-11 21:13:17 +03:00
Reda Al Sulais
22b172d873 add types to svgDrawCommon.ts 2023-08-11 20:56:00 +03:00
Reda Al Sulais
92098e23eb convert svgDrawCommon to TS 2023-08-11 20:54:39 +03:00
Sidharth Vinod
591cb794eb Merge pull request #4717 from Yokozuna59/chore/convert-assignWithDepth-to-ts
convert `assignWithDepth` to TS
2023-08-11 17:32:40 +00:00
Reda Al Sulais
f9d978859e Merge branch 'develop' into standardized-pie-definitions 2023-08-11 19:56:22 +03:00
Reda Al Sulais
6170538c47 Merge branch 'develop' into chore/convert-assignWithDepth-to-ts 2023-08-11 19:54:57 +03:00
Reda Al Sulais
c37c494a1e Update assignWithDepth.ts 2023-08-11 19:54:37 +03:00
Reda Al Sulais
8a8e062342 cleanAndMerge pieConfig 2023-08-11 03:55:04 +03:00
Reda Al Sulais
1721282182 Merge branch 'develop' into standardized-pie-definitions 2023-08-11 01:46:33 +03:00
Reda Al Sulais
d8dd68cad2 redeclare config parameter add default value for each variable 2023-08-11 01:42:26 +03:00
Reda Al Sulais
120bdabee1 Merge branch 'develop' into standardized-pie-definitions 2023-08-11 01:04:06 +03:00
Reda Al Sulais
a5a3ffc768 Merge pull request #2 from mermaid-js/sidv/cleanMerge
Sidv/clean merge
2023-08-11 00:49:38 +03:00
Sidharth Vinod
dfeb25127b remove cleanClone 2023-08-11 02:50:24 +05:30
Sidharth Vinod
396bda8d95 feat: Add cleanAndMerge and tests 2023-08-11 02:47:46 +05:30
Sidharth Vinod
cc70d37166 chore: Rename utils.spec.ts 2023-08-11 02:38:57 +05:30
Reda Al Sulais
3f5da06bb0 move db assignment from beforeEach to beforeAll 2023-08-10 22:43:39 +03:00
Reda Al Sulais
be106befff create structuredCleanClone helper function 2023-08-10 23:10:06 +03:00
Reda Al Sulais
d5f11fc80a convert assignWithDepth to TS 2023-08-10 22:09:14 +03:00
Reda Al Sulais
c4113541e1 Merge remote-tracking branch 'upstream/develop' into standardized-pie-definitions 2023-08-10 21:44:29 +03:00
Reda Al Sulais
fb49f25eef add more types to pieRenderer 2023-08-10 21:36:39 +03:00
Reda Al Sulais
a9681d1b1c add resetConfig to clear in pieDb 2023-08-10 21:19:56 +03:00
Reda Al Sulais
545d361d3f rename reset to resetConfig 2023-08-10 21:01:32 +03:00
Nikolay Rozhkov
3e598f4e8e Merge pull request #4714 from mermaid-js/sidv/exampleDev
docs: Add development example page.
2023-08-09 17:08:29 +03:00
Sidharth Vinod
eb397fdb04 Merge branch 'sidv/exampleDev' of https://github.com/mermaid-js/mermaid into sidv/exampleDev
* 'sidv/exampleDev' of https://github.com/mermaid-js/mermaid:
  Update packages/mermaid/src/docs/community/development.md
  Update docs/community/development.md
2023-08-09 19:00:01 +05:30
Sidharth Vinod
6c1afb7a8d chore: Ignore localhost 2023-08-09 18:59:55 +05:30
Sidharth Vinod
fee6f459a8 Update packages/mermaid/src/docs/community/development.md 2023-08-09 17:44:49 +05:30
Sidharth Vinod
e128a11f3b Update docs/community/development.md 2023-08-09 17:44:24 +05:30
Reda Al Sulais
38dc17f426 use structedClone in pieDb 2023-08-09 14:12:08 +03:00
Reda Al Sulais
453c67e5ea remove PieDiagramConfig and import generated one 2023-08-09 13:47:08 +03:00
Reda Al Sulais
a69a97fdd9 remove unnecessary lines in pie files 2023-08-09 13:41:01 +03:00
Reda Al Sulais
820cc48c11 Merge remote-tracking branch 'upstream/develop' into standardized-pie-definitions 2023-08-09 13:31:34 +03:00
Sidharth Vinod
06e4a6398c docs: Add development example page. 2023-08-09 14:59:50 +05:30
Reda Al Sulais
ca1cdb1d94 remove unused HTML import in pieRenderer 2023-08-08 19:00:27 +03:00
Reda Al Sulais
5485517b27 remove SVG import logs in setupGraphViewbox 2023-08-08 18:55:46 +03:00
Reda Al Sulais
96380600d9 Merge remote-tracking branch 'upstream/develop' into standardized-pie-definitions 2023-08-08 15:02:23 +03:00
Reda Al Sulais
9563b22132 refactor pieRenderer 2023-08-06 21:23:42 +03:00
Reda Al Sulais
e6a18eea91 Merge remote-tracking branch upstream/develop into standardized-pie-definitions 2023-08-06 18:30:51 +03:00
Reda Al Sulais
71205f5bd6 Merge branch 'mermaid-js:develop' into standardized-pie-definitions 2023-08-05 21:33:03 +03:00
Reda Al Sulais
23b6d53f80 Merge branch 'standardized-pie-definitions' of https://github.com/Yokozuna59/mermaid into standardized-pie-definitions 2023-08-04 01:33:29 +03:00
Reda Al Sulais
f4671e4e3a Merge branch 'mermaid-js:develop' into standardized-pie-definitions 2023-08-04 01:32:12 +03:00
Reda Al Sulais
c954e0eb1d rename styles.ts into pieStyles.ts 2023-08-03 21:00:22 +03:00
Reda Al Sulais
de37efefd7 add type for PieStyleOptions 2023-08-03 20:58:39 +03:00
Reda Al Sulais
bdfd8974d4 change default useWidth in pie to 984 2023-08-03 20:56:18 +03:00
Reda Al Sulais
aecf451ed1 update pnpm-lock.yaml 2023-08-02 15:55:02 +03:00
Reda Al Sulais
7d69ad2d5b Merge remote-tracking branch upstream/develop into standardized-pie-definitions 2023-08-02 15:54:23 +03:00
Reda Al Sulais
74fa9956a3 run pnpm lint:fix 2023-08-02 15:46:18 +03:00
Reda Al Sulais
e33340331a add type-fest to mermaid dev deps 2023-08-02 15:44:44 +03:00
Yokozuna59
760548335c Merge branch 'develop' into standardized-pie-definitions 2023-06-27 01:45:35 +03:00
Yokozuna59
41c5152015 Merge branch 'mermaid-js:develop' into standardized-pie-definitions 2023-06-21 15:23:21 +03:00
Yokozuna59
796a761a7d remove unnecessary types in pieRenderer 2023-06-21 00:32:14 +03:00
Yokozuna59
09c4a26509 rename PieDb interface and more types with null sefety 2023-06-21 00:22:56 +03:00
Yokozuna59
ce9d0e2e6a use global useMaxWidth until making setConfig updates pie setConfig 2023-06-19 22:07:15 +03:00
Yokozuna59
ae8860eec3 add undefined to getConfig return type until handle other diagrams 2023-06-19 21:56:35 +03:00
Yokozuna59
67d287f85e use local config instead of glolal one in pieRenderer 2023-06-19 21:51:38 +03:00
Yokozuna59
9c2b95fc3c add config functions to DiagramDB interface 2023-06-19 20:13:15 +03:00
Yokozuna59
3a22d4a501 shorten vitest pie.spec.ts by removing one time use variables 2023-06-19 17:57:39 +03:00
Yokozuna59
cecf759b0b add showData unit test case 2023-06-19 17:52:28 +03:00
Yokozuna59
35c6b671de add config unit test cases for pie chart 2023-06-19 17:46:19 +03:00
Yokozuna59
c894c1f5b5 add Required and RequiredDeep to pieDb, add config functions 2023-06-19 17:45:38 +03:00
Yokozuna59
a92571d588 add type-fest to mermaid dev deps 2023-06-19 17:40:01 +03:00
Yokozuna59
34a47706fd update pnpm-lock.yaml 2023-06-19 13:41:36 +03:00
Yokozuna59
906d909d87 Merge branch develop into standardized-pie-definitions 2023-06-19 13:39:41 +03:00
Yokozuna59
1d0aa763de document pie useWidth and update other pie config attributes 2023-06-18 17:53:28 +03:00
Yokozuna59
f6dc089ddf add D3Sections type with variable renaming 2023-06-18 17:39:15 +03:00
Yokozuna59
95e01b4935 move PieDiagramConfig into pieTypes 2023-06-18 01:45:21 +03:00
Yokozuna59
9145a9e69e Merge branch 'mermaid-js:develop' into standardized-pie-definitions 2023-06-17 15:21:39 +03:00
Yokozuna59
6941814729 update pnpm-lock.yaml 2023-06-17 15:01:20 +03:00
Yokozuna59
32d3001e2a Merge branch 'develop' into standardized-pie-definitions 2023-06-17 14:54:44 +03:00
Yokozuna59
452e543e77 Merge branch 'develop' into standardized-pie-definitions 2023-06-17 14:43:51 +03:00
Yokozuna59
23a5832fc9 add null safety in pieRenderer 2023-06-17 04:05:07 +03:00
Yokozuna59
8794fa0b38 clean up pieRenderer.ts 2023-06-17 03:28:08 +03:00
Yokozuna59
f2338f5b66 remove vanilla-extract from dev dependencies 2023-06-17 00:42:29 +03:00
Yokozuna59
5aba2fed8b remove cy.get of info diagram 2023-06-17 00:01:54 +03:00
Yokozuna59
bd6795032f return pieRender back to original and update it partially 2023-06-16 23:58:56 +03:00
Yokozuna59
c17b723295 convert pie.spec.js to ts, remove cy.get and useless comment, add showData unit test case 2023-06-16 23:45:13 +03:00
Yokozuna59
231a9630df do deep cloning on section field in pie clear 2023-06-16 23:30:41 +03:00
Yokozuna59
bdb967e0a8 fix renderer import in pieDiagram 2023-06-16 23:09:10 +03:00
Yokozuna59
ea3fbbd58d initial converting pie files 2023-06-16 23:05:06 +03:00
Yokozuna59
afea3e8f37 clean up demos/pie.html 2023-06-16 23:03:42 +03:00
Yokozuna59
4e7dbf76cc add SVG type in configureSvgSize function 2023-06-16 23:03:07 +03:00
Yokozuna59
a3901f691a add @types/d3-scale and @types/d3-shape` to mermaid dev deps 2023-06-16 22:47:40 +03:00
78 changed files with 2181 additions and 1719 deletions

View File

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

View File

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

@@ -43,3 +43,6 @@ stats/
**/contributor-names.json
.pnpm-store
.nyc_output
demos/dev/**
!/demos/dev/example.html

View File

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

View File

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

@@ -0,0 +1,8 @@
/**
* Mocked pie (picChart) diagram renderer
*/
import { vi } from 'vitest';
const draw = vi.fn().mockImplementation(() => '');
export const renderer = { draw };

View File

@@ -44,6 +44,7 @@
"faber",
"flatmap",
"foswiki",
"frontmatter",
"ftplugin",
"gantt",
"gitea",
@@ -106,6 +107,7 @@
"rects",
"reda",
"redmine",
"regexes",
"rehype",
"roledescription",
"rozhkov",

View File

@@ -14,7 +14,6 @@ describe('Configuration and directives - nodes should be light blue', () => {
`,
{}
);
cy.get('svg');
});
it('Settings from initialize - nodes should be green', () => {
imgSnapshotTest(
@@ -28,7 +27,6 @@ graph TD
end `,
{ theme: 'forest' }
);
cy.get('svg');
});
it('Settings from initialize overriding themeVariable - nodes should be red', () => {
imgSnapshotTest(
@@ -46,7 +44,6 @@ graph TD
`,
{ theme: 'base', themeVariables: { primaryColor: '#ff0000' }, logLevel: 0 }
);
cy.get('svg');
});
it('Settings from directive - nodes should be grey', () => {
imgSnapshotTest(
@@ -62,7 +59,24 @@ graph TD
`,
{}
);
cy.get('svg');
});
it('Settings from frontmatter - nodes should be grey', () => {
imgSnapshotTest(
`
---
config:
theme: neutral
---
graph TD
A(Start) --> B[/Another/]
A[/Another/] --> C[End]
subgraph section
B
C
end
`,
{}
);
});
it('Settings from directive overriding theme variable - nodes should be red', () => {
@@ -79,7 +93,6 @@ graph TD
`,
{}
);
cy.get('svg');
});
it('Settings from initialize and directive - nodes should be grey', () => {
imgSnapshotTest(
@@ -95,7 +108,6 @@ graph TD
`,
{ theme: 'forest' }
);
cy.get('svg');
});
it('Theme from initialize, directive overriding theme variable - nodes should be red', () => {
imgSnapshotTest(
@@ -111,8 +123,71 @@ graph TD
`,
{ theme: 'base' }
);
cy.get('svg');
});
it('Theme from initialize, frontmatter overriding theme variable - nodes should be red', () => {
imgSnapshotTest(
`
---
config:
theme: base
themeVariables:
primaryColor: '#ff0000'
---
graph TD
A(Start) --> B[/Another/]
A[/Another/] --> C[End]
subgraph section
B
C
end
`,
{ theme: 'forest' }
);
});
it('Theme from initialize, frontmatter overriding theme variable, directive overriding primaryColor - nodes should be red', () => {
imgSnapshotTest(
`
---
config:
theme: base
themeVariables:
primaryColor: '#00ff00'
---
%%{init: {'theme': 'base', 'themeVariables':{ 'primaryColor': '#ff0000'}}}%%
graph TD
A(Start) --> B[/Another/]
A[/Another/] --> C[End]
subgraph section
B
C
end
`,
{ theme: 'forest' }
);
});
it('should render if values are not quoted properly', () => {
// #ff0000 is not quoted properly, and will evaluate to null.
// This test ensures that the rendering still works.
imgSnapshotTest(
`---
config:
theme: base
themeVariables:
primaryColor: #ff0000
---
graph TD
A(Start) --> B[/Another/]
A[/Another/] --> C[End]
subgraph section
B
C
end
`,
{ theme: 'forest' }
);
});
it('Theme variable from initialize, theme from directive - nodes should be red', () => {
imgSnapshotTest(
`
@@ -127,13 +202,11 @@ graph TD
`,
{ themeVariables: { primaryColor: '#ff0000' } }
);
cy.get('svg');
});
describe('when rendering several diagrams', () => {
it('diagrams should not taint later diagrams', () => {
const url = 'http://localhost:9000/theme-directives.html';
cy.visit(url);
cy.get('svg');
cy.matchImageSnapshot('conf-and-directives.spec-when-rendering-several-diagrams-diagram-1');
});
});

View File

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

View File

@@ -21,6 +21,8 @@
<pre class="mermaid">
---
title: This is a title
config:
theme: forest
---
erDiagram
%% title This is a title

View File

@@ -123,6 +123,13 @@
<h3>flowchart</h3>
<pre class="mermaid">
---
title: This is another complicated flow
config:
theme: base
flowchart:
curve: cardinal
---
flowchart LR
sid-B3655226-6C29-4D00-B685-3D5C734DC7E1["

View File

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

View File

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

View File

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

View File

@@ -10,10 +10,41 @@ When mermaid starts, configuration is extracted to determine a configuration to
- The default configuration
- Overrides at the site level are set by the initialize call, and will be applied to all diagrams in the site/app. The term for this is the **siteConfig**.
- Directives - diagram authors can update select configuration parameters directly in the diagram code via directives. These are applied to the render config.
- Frontmatter (v\<MERMAID_RELEASE_VERSION>+) - diagram authors can update select configuration parameters in the frontmatter of the diagram. These are applied to the render config.
- Directives (Deprecated by Frontmatter) - diagram authors can update select configuration parameters directly in the diagram code via directives. These are applied to the render config.
**The render config** is configuration that is used when rendering by applying these configurations.
## Frontmatter config
The entire mermaid configuration (except the secure configs) can be overridden by the diagram author in the frontmatter of the diagram. The frontmatter is a YAML block at the top of the diagram.
```mermaid-example
---
title: Hello Title
config:
theme: base
themeVariables:
primaryColor: "#00ff00"
---
flowchart
Hello --> World
```
```mermaid
---
title: Hello Title
config:
theme: base
themeVariables:
primaryColor: "#00ff00"
---
flowchart
Hello --> World
```
## Theme configuration
## Starting mermaid

View File

@@ -6,6 +6,9 @@
# Directives
> **Warning**
> Directives are deprecated from v\<MERMAID_RELEASE_VERSION>. Please use the `config` key in frontmatter to pass configuration. See [Configuration](./configuration.md) for more details.
## Directives
Directives give a diagram author the capability to alter the appearance of a diagram before rendering by changing the applied configuration.

View File

@@ -16,4 +16,4 @@
#### Defined in
[mermaidAPI.ts:77](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L77)
[mermaidAPI.ts:78](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L78)

View File

@@ -39,7 +39,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present.
#### Defined in
[mermaidAPI.ts:97](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L97)
[mermaidAPI.ts:98](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L98)
---
@@ -51,4 +51,4 @@ The svg code for the rendered graph.
#### Defined in
[mermaidAPI.ts:87](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L87)
[mermaidAPI.ts:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L88)

View File

@@ -14,7 +14,7 @@
#### Defined in
[config.ts:7](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L7)
[config.ts:8](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L8)
## Functions
@@ -26,9 +26,9 @@ Pushes in a directive to the configuration
#### Parameters
| Name | Type | Description |
| :---------- | :---- | :----------------------- |
| `directive` | `any` | The directive to push in |
| Name | Type | Description |
| :---------- | :-------------- | :----------------------- |
| `directive` | `MermaidConfig` | The directive to push in |
#### Returns
@@ -36,7 +36,7 @@ Pushes in a directive to the configuration
#### Defined in
[config.ts:191](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L191)
[config.ts:188](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L188)
---
@@ -60,7 +60,7 @@ The currentConfig
#### Defined in
[config.ts:137](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L137)
[config.ts:131](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L131)
---
@@ -118,7 +118,7 @@ The siteConfig
#### Defined in
[config.ts:223](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L223)
[config.ts:218](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L218)
---
@@ -147,7 +147,7 @@ options in-place
#### Defined in
[config.ts:152](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L152)
[config.ts:146](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L146)
---
@@ -242,10 +242,10 @@ The new siteConfig
#### Parameters
| Name | Type |
| :------------ | :-------------- |
| `siteCfg` | `MermaidConfig` |
| `_directives` | `any`\[] |
| Name | Type |
| :------------ | :----------------- |
| `siteCfg` | `MermaidConfig` |
| `_directives` | `MermaidConfig`\[] |
#### Returns
@@ -253,7 +253,7 @@ The new siteConfig
#### Defined in
[config.ts:14](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L14)
[config.ts:15](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L15)
---

View File

@@ -10,17 +10,17 @@
### configKeys
`Const` **configKeys**: `string`\[]
`Const` **configKeys**: `Set`<`string`>
#### 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)

View File

@@ -25,7 +25,7 @@ Renames and re-exports [mermaidAPI](mermaidAPI.md#mermaidapi)
#### Defined in
[mermaidAPI.ts:81](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L81)
[mermaidAPI.ts:82](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L82)
## Variables
@@ -96,7 +96,7 @@ mermaid.initialize(config);
#### Defined in
[mermaidAPI.ts:668](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L668)
[mermaidAPI.ts:673](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L673)
## Functions
@@ -127,7 +127,7 @@ Return the last node appended
#### Defined in
[mermaidAPI.ts:309](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L309)
[mermaidAPI.ts:310](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L310)
---
@@ -153,7 +153,7 @@ the cleaned up svgCode
#### Defined in
[mermaidAPI.ts:255](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L255)
[mermaidAPI.ts:256](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L256)
---
@@ -179,7 +179,7 @@ the string with all the user styles
#### Defined in
[mermaidAPI.ts:184](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L184)
[mermaidAPI.ts:185](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L185)
---
@@ -202,7 +202,7 @@ the string with all the user styles
#### Defined in
[mermaidAPI.ts:232](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L232)
[mermaidAPI.ts:233](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L233)
---
@@ -229,7 +229,7 @@ with an enclosing block that has each of the cssClasses followed by !important;
#### Defined in
[mermaidAPI.ts:168](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L168)
[mermaidAPI.ts:169](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L169)
---
@@ -249,7 +249,7 @@ with an enclosing block that has each of the cssClasses followed by !important;
#### Defined in
[mermaidAPI.ts:154](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L154)
[mermaidAPI.ts:155](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L155)
---
@@ -269,7 +269,7 @@ with an enclosing block that has each of the cssClasses followed by !important;
#### Defined in
[mermaidAPI.ts:125](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L125)
[mermaidAPI.ts:126](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L126)
---
@@ -295,7 +295,7 @@ Put the svgCode into an iFrame. Return the iFrame code
#### Defined in
[mermaidAPI.ts:286](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L286)
[mermaidAPI.ts:287](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L287)
---
@@ -320,4 +320,4 @@ Remove any existing elements from the given document
#### Defined in
[mermaidAPI.ts:359](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L359)
[mermaidAPI.ts:360](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L360)

View File

@@ -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'>
[![Build CI Status](https://github.com/mermaid-js/mermaid/actions/workflows/build.yml/badge.svg)](https://github.com/mermaid-js/mermaid/actions/workflows/build.yml) [![NPM](https://img.shields.io/npm/v/mermaid)](https://www.npmjs.com/package/mermaid) [![npm minified gzipped bundle size](https://img.shields.io/bundlephobia/minzip/mermaid)](https://bundlephobia.com/package/mermaid) [![Coverage Status](https://coveralls.io/repos/github/mermaid-js/mermaid/badge.svg?branch=master)](https://coveralls.io/github/mermaid-js/mermaid?branch=master) [![CDN Status](https://img.shields.io/jsdelivr/npm/hm/mermaid)](https://www.jsdelivr.com/package/npm/mermaid) [![NPM](https://img.shields.io/npm/dm/mermaid)](https://www.npmjs.com/package/mermaid) [![Join our Slack!](https://img.shields.io/static/v1?message=join%20chat&color=9cf&logo=slack&label=slack)](https://join.slack.com/t/mermaid-talk/shared_invite/enQtNzc4NDIyNzk4OTAyLWVhYjQxOTI2OTg4YmE1ZmJkY2Y4MTU3ODliYmIwOTY3NDJlYjA0YjIyZTdkMDMyZTUwOGI0NjEzYmEwODcwOTE) [![Twitter Follow](https://img.shields.io/twitter/follow/mermaidjs_?style=social)](https://twitter.com/mermaidjs_)
</div>
<!-- Mermaid book banner -->
[![Explore Mermaid.js in depth, with real-world examples, tips & tricks from the creator... The first official book on Mermaid is available for purchase. Check it out!](img/book-banner-post-release.jpg)](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'>
[![Good first issue](https://img.shields.io/github/labels/mermaid-js/mermaid/Good%20first%20issue%21)](https://github.com/mermaid-js/mermaid/issues?q=is%3Aissue+is%3Aopen+label%3A%22Good+first+issue%21%22) [![Contributors](https://img.shields.io/github/contributors/mermaid-js/mermaid)](https://github.com/mermaid-js/mermaid/graphs/contributors) [![Commits](https://img.shields.io/github/commit-activity/m/mermaid-js/mermaid)](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>

View File

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

View File

@@ -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
View 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"
}

View File

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

View File

@@ -48,7 +48,7 @@ export class Diagram {
// extractFrontMatter().
this.parser.parse = (text: string) =>
originalParse(cleanupComments(extractFrontMatter(text, this.db)));
originalParse(cleanupComments(extractFrontMatter(text, this.db, configApi.addDirective)));
this.parser.parser.yy = this.db;
this.init = diagram.init;

View File

@@ -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 Object.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;

View File

@@ -1,11 +1,13 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import * as configApi from './config.js';
import type { MermaidConfig } from './config.type.js';
describe('when working with site config', function () {
describe('when working with site config', () => {
beforeEach(() => {
// Resets the site config to default config
configApi.setSiteConfig({});
});
it('should set site config and config properly', function () {
it('should set site config and config properly', () => {
const config_0 = { fontFamily: 'foo-font', fontSize: 150 };
configApi.setSiteConfig(config_0);
const config_1 = configApi.getSiteConfig();
@@ -14,19 +16,26 @@ describe('when working with site config', function () {
expect(config_1.fontSize).toEqual(config_0.fontSize);
expect(config_1).toEqual(config_2);
});
it('should respect secure keys when applying directives', function () {
const config_0 = {
it('should respect secure keys when applying directives', () => {
const config_0: MermaidConfig = {
fontFamily: 'foo-font',
securityLevel: 'strict', // can't be changed
fontSize: 12345, // can't be changed
secure: [...configApi.defaultConfig.secure!, 'fontSize'],
};
configApi.setSiteConfig(config_0);
const directive = { fontFamily: 'baf', fontSize: 54321 /* fontSize shouldn't be changed */ };
const cfg = configApi.updateCurrentConfig(config_0, [directive]);
const directive: MermaidConfig = {
fontFamily: 'baf',
// fontSize and securityLevel shouldn't be changed
fontSize: 54321,
securityLevel: 'loose',
};
const cfg: MermaidConfig = configApi.updateCurrentConfig(config_0, [directive]);
expect(cfg.fontFamily).toEqual(directive.fontFamily);
expect(cfg.fontSize).toBe(config_0.fontSize);
expect(cfg.securityLevel).toBe(config_0.securityLevel);
});
it('should allow setting partial options', function () {
it('should allow setting partial options', () => {
const defaultConfig = configApi.getConfig();
configApi.setConfig({
@@ -42,7 +51,7 @@ describe('when working with site config', function () {
updatedConfig.quadrantChart!.chartWidth
);
});
it('should set reset config properly', function () {
it('should set reset config properly', () => {
const config_0 = { fontFamily: 'foo-font', fontSize: 150 };
configApi.setSiteConfig(config_0);
const config_1 = { fontFamily: 'baf' };
@@ -55,7 +64,7 @@ describe('when working with site config', function () {
const config_4 = configApi.getSiteConfig();
expect(config_4.fontFamily).toEqual(config_0.fontFamily);
});
it('should set global reset config properly', function () {
it('should set global reset config properly', () => {
const config_0 = { fontFamily: 'foo-font', fontSize: 150 };
configApi.setSiteConfig(config_0);
const config_1 = configApi.getSiteConfig();

View File

@@ -3,15 +3,16 @@ import { log } from './logger.js';
import theme from './themes/index.js';
import config from './defaultConfig.js';
import type { MermaidConfig } from './config.type.js';
import { sanitizeDirective } from './utils.js';
export const defaultConfig: MermaidConfig = Object.freeze(config);
let siteConfig: MermaidConfig = assignWithDepth({}, defaultConfig);
let configFromInitialize: MermaidConfig;
let directives: any[] = [];
let directives: MermaidConfig[] = [];
let currentConfig: MermaidConfig = assignWithDepth({}, defaultConfig);
export const updateCurrentConfig = (siteCfg: MermaidConfig, _directives: any[]) => {
export const updateCurrentConfig = (siteCfg: MermaidConfig, _directives: MermaidConfig[]) => {
// start with config being the siteConfig
let cfg: MermaidConfig = assignWithDepth({}, siteCfg);
// let sCfg = assignWithDepth(defaultConfig, siteConfigDelta);
@@ -20,7 +21,6 @@ export const updateCurrentConfig = (siteCfg: MermaidConfig, _directives: any[])
let sumOfDirectives: MermaidConfig = {};
for (const d of _directives) {
sanitize(d);
// Apply the data from the directive where the the overrides the themeVariables
sumOfDirectives = assignWithDepth(sumOfDirectives, d);
}
@@ -111,12 +111,6 @@ export const getSiteConfig = (): MermaidConfig => {
* @returns The currentConfig merged with the sanitized conf
*/
export const setConfig = (conf: MermaidConfig): MermaidConfig => {
// sanitize(conf);
// Object.keys(conf).forEach(key => {
// const manipulator = manipulators[key];
// conf[key] = manipulator ? manipulator(conf[key]) : conf[key];
// });
checkConfig(conf);
assignWithDepth(currentConfig, conf);
@@ -150,9 +144,12 @@ export const getConfig = (): MermaidConfig => {
* @param options - The potential setConfig parameter
*/
export const sanitize = (options: any) => {
if (!options) {
return;
}
// Checking that options are not in the list of excluded options
['secure', ...(siteConfig.secure ?? [])].forEach((key) => {
if (options[key] !== undefined) {
if (Object.hasOwn(options, key)) {
// DO NOT attempt to print options[key] within `${}` as a malicious script
// can exploit the logger's attempt to stringify the value and execute arbitrary code
log.debug(`Denied attempt to modify a secure key ${key}`, options[key]);
@@ -162,7 +159,7 @@ export const sanitize = (options: any) => {
// Check that there no attempts of prototype pollution
Object.keys(options).forEach((key) => {
if (key.indexOf('__') === 0) {
if (key.startsWith('__')) {
delete options[key];
}
});
@@ -188,16 +185,14 @@ export const sanitize = (options: any) => {
*
* @param directive - The directive to push in
*/
export const addDirective = (directive: any) => {
if (directive.fontFamily) {
if (!directive.themeVariables) {
directive.themeVariables = { fontFamily: directive.fontFamily };
} else {
if (!directive.themeVariables.fontFamily) {
directive.themeVariables = { fontFamily: directive.fontFamily };
}
}
export const addDirective = (directive: MermaidConfig) => {
sanitizeDirective(directive);
// If the directive has a fontFamily, but no themeVariables, add the fontFamily to the themeVariables
if (directive.fontFamily && (!directive.themeVariables || !directive.themeVariables.fontFamily)) {
directive.themeVariables = { fontFamily: directive.fontFamily };
}
directives.push(directive);
updateCurrentConfig(siteConfig, directives);
};

View File

@@ -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,
@@ -263,5 +265,5 @@ const keyify = (obj: any, prefix = ''): string[] =>
return [...res, prefix + el];
}, []);
export const configKeys: string[] = keyify(config, '');
export const configKeys: Set<string> = new Set(keyify(config, ''));
export default config;

View File

@@ -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`);

View File

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

View File

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

View File

@@ -2,8 +2,13 @@ import { vi } from 'vitest';
import { extractFrontMatter } from './frontmatter.js';
const dbMock = () => ({ setDiagramTitle: vi.fn() });
const setConfigMock = vi.fn();
describe('extractFrontmatter', () => {
beforeEach(() => {
setConfigMock.mockClear();
});
it('returns text unchanged if no frontmatter', () => {
expect(extractFrontMatter('diagram', dbMock())).toEqual('diagram');
});
@@ -75,4 +80,21 @@ describe('extractFrontmatter', () => {
'tag suffix cannot contain exclamation marks'
);
});
it('handles frontmatter with config', () => {
const text = `---
title: hello
config:
graph:
string: hello
number: 14
boolean: false
array: [1, 2, 3]
---
diagram`;
expect(extractFrontMatter(text, {}, setConfigMock)).toEqual('diagram');
expect(setConfigMock).toHaveBeenCalledWith({
graph: { string: 'hello', number: 14, boolean: false, array: [1, 2, 3] },
});
});
});

View File

@@ -1,46 +1,53 @@
import { DiagramDB } from './types.js';
import type { MermaidConfig } from '../config.type.js';
import { frontMatterRegex } from './regexes.js';
import type { DiagramDB } from './types.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 = {
interface FrontMatterMetadata {
title?: string;
// Allows custom display modes. Currently used for compact mode in gantt charts.
displayMode?: string;
};
config?: MermaidConfig;
}
/**
* Extract and parse frontmatter from text, if present, and sets appropriate
* properties in the provided db.
* @param text - The text that may have a YAML frontmatter.
* @param db - Diagram database, could be of any diagram.
* @param setDiagramConfig - Optional function to set diagram config.
* @returns text with frontmatter stripped out
*/
export function extractFrontMatter(text: string, db: DiagramDB): string {
export function extractFrontMatter(
text: string,
db: DiagramDB,
setDiagramConfig?: (config: MermaidConfig) => void
): string {
const matches = text.match(frontMatterRegex);
if (matches) {
const parsed: FrontMatterMetadata = yaml.load(matches[1], {
// To keep things simple, only allow strings, arrays, and plain objects.
// https://www.yaml.org/spec/1.2/spec.html#id2802346
schema: yaml.FAILSAFE_SCHEMA,
}) as FrontMatterMetadata;
if (parsed?.title) {
db.setDiagramTitle?.(parsed.title);
}
if (parsed?.displayMode) {
db.setDisplayMode?.(parsed.displayMode);
}
return text.slice(matches[0].length);
} else {
if (!matches) {
return text;
}
const parsed: FrontMatterMetadata = yaml.load(matches[1], {
// To support config, we need JSON schema.
// https://www.yaml.org/spec/1.2/spec.html#id2803231
schema: yaml.JSON_SCHEMA,
}) as FrontMatterMetadata;
if (parsed?.title) {
// toString() is necessary because YAML could parse the title as a number/boolean
db.setDiagramTitle?.(parsed.title.toString());
}
if (parsed?.displayMode) {
// toString() is necessary because YAML could parse the title as a number/boolean
db.setDisplayMode?.(parsed.displayMode.toString());
}
if (parsed?.config) {
setDiagramConfig?.(parsed.config);
}
return text.slice(matches[0].length);
}

View 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`);
}
};

View 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;

View File

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

View File

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

View File

@@ -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&colon;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(

View File

@@ -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;
/**

View 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>;

View File

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

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

View File

@@ -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 name amounts
2 Foo 33
3 Rishab 12
4 Alexis 41
5 Tom 16
6 Courtney 59
7 Christina 38
8 Jack 21
9 Mickey 25
10 Paul 30

View File

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

View 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);
});
});
});

View File

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

View 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,
};

View File

@@ -15,10 +15,8 @@ const loader: DiagramLoader = async () => {
return { id, diagram };
};
const plugin: ExternalDiagramDefinition = {
export const pie: ExternalDiagramDefinition = {
id,
detector,
loader,
};
export default plugin;

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,5 @@
import * as configApi from './config.js';
import { log } from './logger.js';
import { directiveSanitizer } from './utils.js';
let currentDirective: { type?: string; args?: any } | undefined = {};
@@ -60,9 +58,6 @@ const handleDirective = function (p: any, directive: any, type: string): void {
delete directive.args[prop];
}
});
log.info('sanitize in handleDirective', directive.args);
directiveSanitizer(directive.args);
log.info('sanitize in handleDirective (done)', directive.args);
configApi.addDirective(directive.args);
break;
}

View File

@@ -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' },

View File

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

View File

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

View File

@@ -4,10 +4,28 @@ When mermaid starts, configuration is extracted to determine a configuration to
- The default configuration
- Overrides at the site level are set by the initialize call, and will be applied to all diagrams in the site/app. The term for this is the **siteConfig**.
- Directives - diagram authors can update select configuration parameters directly in the diagram code via directives. These are applied to the render config.
- Frontmatter (v<MERMAID_RELEASE_VERSION>+) - diagram authors can update select configuration parameters in the frontmatter of the diagram. These are applied to the render config.
- Directives (Deprecated by Frontmatter) - diagram authors can update select configuration parameters directly in the diagram code via directives. These are applied to the render config.
**The render config** is configuration that is used when rendering by applying these configurations.
## Frontmatter config
The entire mermaid configuration (except the secure configs) can be overridden by the diagram author in the frontmatter of the diagram. The frontmatter is a YAML block at the top of the diagram.
```mermaid-example
---
title: Hello Title
config:
theme: base
themeVariables:
primaryColor: "#00ff00"
---
flowchart
Hello --> World
```
## Theme configuration
## Starting mermaid

View File

@@ -1,5 +1,9 @@
# Directives
```warning
Directives are deprecated from v<MERMAID_RELEASE_VERSION>. Please use the `config` key in frontmatter to pass configuration. See [Configuration](./configuration.md) for more details.
```
## Directives
Directives give a diagram author the capability to alter the appearance of a diagram before rendering by changing the applied configuration.

View File

@@ -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'>
[![Build CI Status](https://github.com/mermaid-js/mermaid/actions/workflows/build.yml/badge.svg)](https://github.com/mermaid-js/mermaid/actions/workflows/build.yml) [![NPM](https://img.shields.io/npm/v/mermaid)](https://www.npmjs.com/package/mermaid) [![npm minified gzipped bundle size](https://img.shields.io/bundlephobia/minzip/mermaid)](https://bundlephobia.com/package/mermaid) [![Coverage Status](https://coveralls.io/repos/github/mermaid-js/mermaid/badge.svg?branch=master)](https://coveralls.io/github/mermaid-js/mermaid?branch=master) [![CDN Status](https://img.shields.io/jsdelivr/npm/hm/mermaid)](https://www.jsdelivr.com/package/npm/mermaid) [![NPM](https://img.shields.io/npm/dm/mermaid)](https://www.npmjs.com/package/mermaid) [![Join our Slack!](https://img.shields.io/static/v1?message=join%20chat&color=9cf&logo=slack&label=slack)](https://join.slack.com/t/mermaid-talk/shared_invite/enQtNzc4NDIyNzk4OTAyLWVhYjQxOTI2OTg4YmE1ZmJkY2Y4MTU3ODliYmIwOTY3NDJlYjA0YjIyZTdkMDMyZTUwOGI0NjEzYmEwODcwOTE) [![Twitter Follow](https://img.shields.io/twitter/follow/mermaidjs_?style=social)](https://twitter.com/mermaidjs_)
</div>
<!-- Mermaid book banner -->
[![Explore Mermaid.js in depth, with real-world examples, tips & tricks from the creator... The first official book on Mermaid is available for purchase. Check it out!](img/book-banner-post-release.jpg)](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'>
[![Good first issue](https://img.shields.io/github/labels/mermaid-js/mermaid/Good%20first%20issue%21)](https://github.com/mermaid-js/mermaid/issues?q=is%3Aissue+is%3Aopen+label%3A%22Good+first+issue%21%22) [![Contributors](https://img.shields.io/github/contributors/mermaid-js/mermaid)](https://github.com/mermaid-js/mermaid/graphs/contributors) [![Commits](https://img.shields.io/github/commit-activity/m/mermaid-js/mermaid)](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>

View File

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

View File

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

View File

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

View File

@@ -23,13 +23,14 @@ import { attachFunctions } from './interactionDb.js';
import { log, setLogLevel } from './logger.js';
import getStyles from './styles.js';
import theme from './themes/index.js';
import utils, { directiveSanitizer } from './utils.js';
import utils from './utils.js';
import DOMPurify from 'dompurify';
import { MermaidConfig } from './config.type.js';
import { evaluate } from './diagrams/common/common.js';
import isEmpty from 'lodash-es/isEmpty.js';
import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility.js';
import { parseDirective } from './directiveUtils.js';
import { extractFrontMatter } from './diagram-api/frontmatter.js';
// diagram names that support classDef statements
const CLASSDEF_DIAGRAMS = [
@@ -385,10 +386,14 @@ const render = async function (
configApi.reset();
// Add Directives. Must do this before getting the config and before creating the diagram.
// We need to add the directives before creating the diagram.
// So extractFrontMatter is called twice. Once here and once in the diagram parser.
// This can be fixed in a future refactor.
extractFrontMatter(text, {}, configApi.addDirective);
// Add Directives.
const graphInit = utils.detectInit(text);
if (graphInit) {
directiveSanitizer(graphInit);
configApi.addDirective(graphInit);
}

View File

@@ -13,10 +13,10 @@
# Non-JSON values, like functions or `undefined`, still need to be manually
# set in `src/defaultConfig.ts`)
# - `src/docs.mts`
# Used to genereate Markdown documentation for this JSON Schema by using
# Used to generate Markdown documentation for this JSON Schema by using
# the `@adobe/jsonschema2md` NPM package.
# Useful things to know when editting this file
# Useful things to know when editing this file
# - Use the `|` character for multi-line strings
# - Use `meta:enum` to document enum values (from jsonschema2md)
# - Use `tsType` to override the TypeScript type (from json-schema-to-typescript)
@@ -1851,7 +1851,7 @@ $defs: # JSON Schema definition (maybe we should move these to a seperate file)
The color of the links in the sankey diagram.
anyOf:
- $ref: '#/$defs/SankeyLinkColor'
- description: An arbtirary [CSS color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value)
- description: An arbitrary [CSS color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value)
type: string
default: gradient
nodeAlignment:

View File

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

View File

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

View File

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

View File

@@ -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;
@@ -95,32 +97,36 @@ const directiveWithoutOpen =
* @param config - Optional mermaid configuration object.
* @returns The json object representing the init passed to mermaid.initialize()
*/
export const detectInit = function (text: string, config?: MermaidConfig): MermaidConfig {
export const detectInit = function (
text: string,
config?: MermaidConfig
): MermaidConfig | undefined {
const inits = detectDirective(text, /(?:init\b)|(?:initialize\b)/);
let results = {};
if (Array.isArray(inits)) {
const args = inits.map((init) => init.args);
directiveSanitizer(args);
sanitizeDirective(args);
results = assignWithDepth(results, [...args]);
} else {
results = inits.args;
}
if (results) {
let type = detectType(text, config);
['config'].forEach((prop) => {
if (results[prop] !== undefined) {
if (type === 'flowchart-v2') {
type = 'flowchart';
}
results[type] = results[prop];
delete results[prop];
}
});
if (!results) {
return;
}
// Todo: refactor this, these results are never used
let type = detectType(text, config);
['config'].forEach((prop) => {
if (results[prop] !== undefined) {
if (type === 'flowchart-v2') {
type = 'flowchart';
}
results[type] = results[prop];
delete results[prop];
}
});
return results;
};
@@ -162,10 +168,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 +808,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;
@@ -841,67 +847,63 @@ export const entityDecode = function (html: string): string {
*
* @param args - Directive's JSON
*/
export const directiveSanitizer = (args: any) => {
log.debug('directiveSanitizer called with', args);
if (typeof args === 'object') {
// check for array
if (args.length) {
args.forEach((arg) => directiveSanitizer(arg));
} else {
// This is an object
Object.keys(args).forEach((key) => {
log.debug('Checking key', key);
if (key.startsWith('__')) {
log.debug('sanitize deleting __ option', key);
delete args[key];
}
export const sanitizeDirective = (args: unknown): void => {
log.debug('sanitizeDirective called with', args);
if (key.includes('proto')) {
log.debug('sanitize deleting proto option', key);
delete args[key];
}
// Return if not an object
if (typeof args !== 'object' || args == null) {
return;
}
if (key.includes('constr')) {
log.debug('sanitize deleting constr option', key);
delete args[key];
}
// Sanitize each element if an array
if (Array.isArray(args)) {
args.forEach((arg) => sanitizeDirective(arg));
return;
}
if (key.includes('themeCSS')) {
log.debug('sanitizing themeCss option');
args[key] = sanitizeCss(args[key]);
}
if (key.includes('fontFamily')) {
log.debug('sanitizing fontFamily option');
args[key] = sanitizeCss(args[key]);
}
if (key.includes('altFontFamily')) {
log.debug('sanitizing altFontFamily option');
args[key] = sanitizeCss(args[key]);
}
if (!configKeys.includes(key)) {
log.debug('sanitize deleting option', key);
delete args[key];
} else {
if (typeof args[key] === 'object') {
log.debug('sanitize deleting object', key);
directiveSanitizer(args[key]);
}
}
});
// Sanitize each key if an object
for (const key of Object.keys(args)) {
log.debug('Checking key', key);
if (
key.startsWith('__') ||
key.includes('proto') ||
key.includes('constr') ||
!configKeys.has(key) ||
args[key] == null
) {
log.debug('sanitize deleting key: ', key);
delete args[key];
continue;
}
// Recurse if an object
if (typeof args[key] === 'object') {
log.debug('sanitizing object', key);
sanitizeDirective(args[key]);
continue;
}
const cssMatchers = ['themeCSS', 'fontFamily', 'altFontFamily'];
for (const cssKey of cssMatchers) {
if (key.includes(cssKey)) {
log.debug('sanitizing css option', key);
args[key] = sanitizeCss(args[key]);
}
}
}
if (args.themeVariables) {
const kArr = Object.keys(args.themeVariables);
for (const k of kArr) {
for (const k of Object.keys(args.themeVariables)) {
const val = args.themeVariables[k];
if (val && val.match && !val.match(/^[\d "#%(),.;A-Za-z]+$/)) {
if (val?.match && !val.match(/^[\d "#%(),.;A-Za-z]+$/)) {
args.themeVariables[k] = '';
}
}
}
log.debug('After sanitization', args);
};
export const sanitizeCss = (str) => {
export const sanitizeCss = (str: string): string => {
let startCnt = 0;
let endCnt = 0;
@@ -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,
@@ -1013,8 +1020,8 @@ export default {
random,
runFunc,
entityDecode,
initIdGenerator: initIdGenerator,
directiveSanitizer,
initIdGenerator,
sanitizeDirective,
sanitizeCss,
insertTitle,
parseFontSize,

1362
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -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"
},