mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-12-30 08:06:21 +01:00
Compare commits
116 Commits
sidv/useUn
...
timeline
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82f7e1b754 | ||
|
|
580903051f | ||
|
|
ce037a84ca | ||
|
|
7f254e37e9 | ||
|
|
d562a81019 | ||
|
|
6a045db83c | ||
|
|
563896400b | ||
|
|
73ee9e9a92 | ||
|
|
923ddc8309 | ||
|
|
64096b22dc | ||
|
|
84d563584f | ||
|
|
ac67794fb2 | ||
|
|
98b2148352 | ||
|
|
b3e509b7d4 | ||
|
|
8d6d90021a | ||
|
|
71e5a53172 | ||
|
|
a75cacd4a4 | ||
|
|
c2ec63d4fd | ||
|
|
7ecaaaf46f | ||
|
|
4900647bf0 | ||
|
|
df10d64989 | ||
|
|
b9bed14cda | ||
|
|
cd007cabb5 | ||
|
|
cb1a20264d | ||
|
|
7179f1bcba | ||
|
|
b35e4a8c52 | ||
|
|
a89cb9f0d6 | ||
|
|
8b4426aebf | ||
|
|
003997372e | ||
|
|
61f33567ae | ||
|
|
9fb6f1998f | ||
|
|
19e3624e89 | ||
|
|
be332cfdef | ||
|
|
c51f6df82c | ||
|
|
dc0a46f742 | ||
|
|
c76728b423 | ||
|
|
0aa7da261f | ||
|
|
5e6aac4377 | ||
|
|
52bd5181f9 | ||
|
|
49ce5222c9 | ||
|
|
df1e9c4117 | ||
|
|
5c14df0aeb | ||
|
|
8af5c4c341 | ||
|
|
f7756ccc00 | ||
|
|
3066a4b43a | ||
|
|
26e9b1790b | ||
|
|
c256a6b887 | ||
|
|
bd1449a0d3 | ||
|
|
c9833dcd79 | ||
|
|
6792bb94b7 | ||
|
|
b36e5d0d3b | ||
|
|
ead4037963 | ||
|
|
a28f6bf80c | ||
|
|
fb6ba231d0 | ||
|
|
e4491136c3 | ||
|
|
e7451e7a4e | ||
|
|
816f2f512e | ||
|
|
76c3716b2d | ||
|
|
2f1a521db6 | ||
|
|
8f4caa4537 | ||
|
|
b26cdb3e46 | ||
|
|
de8928b2d9 | ||
|
|
e5b33087f3 | ||
|
|
bc56a7d4f1 | ||
|
|
80903e427c | ||
|
|
7b67f5783e | ||
|
|
023f2354cd | ||
|
|
7ef71cc04d | ||
|
|
3cd15cdcf2 | ||
|
|
e865368649 | ||
|
|
be818ad57f | ||
|
|
ca22e85e55 | ||
|
|
652a42fe1a | ||
|
|
3aeef7b846 | ||
|
|
250f1f9687 | ||
|
|
5b9839cbd0 | ||
|
|
afe3f593e1 | ||
|
|
e573be4afd | ||
|
|
549483d19b | ||
|
|
8a485c3c88 | ||
|
|
96dedc7b5e | ||
|
|
9629c8d8d6 | ||
|
|
7e8c1b0393 | ||
|
|
e4bdfee85a | ||
|
|
c0f9b3c00b | ||
|
|
fba3f8481e | ||
|
|
ae7c9475ef | ||
|
|
d320e788c7 | ||
|
|
ded83b2ce7 | ||
|
|
1c8c69f8db | ||
|
|
4d3f151cf0 | ||
|
|
8f8ae64ced | ||
|
|
b93ce24c3d | ||
|
|
05d1fc44e8 | ||
|
|
ee056e1ac3 | ||
|
|
0476bdc68f | ||
|
|
242a508d50 | ||
|
|
7b5bebd9c4 | ||
|
|
68609fd8d0 | ||
|
|
117f0ab6da | ||
|
|
64a935515c | ||
|
|
e6befbaa3f | ||
|
|
e659601e03 | ||
|
|
521a30dcd7 | ||
|
|
61ef4692b0 | ||
|
|
6ab7eb55cb | ||
|
|
fefcc43dd4 | ||
|
|
2141057ab4 | ||
|
|
ae7fd777a7 | ||
|
|
d666981599 | ||
|
|
f0b8657423 | ||
|
|
ae25a08fe3 | ||
|
|
2b7aa3f99d | ||
|
|
e7b4f7f6ca | ||
|
|
14ff8a8570 | ||
|
|
0d5246fbc7 |
@@ -3,4 +3,5 @@ dist/**
|
||||
docs/Setup.md
|
||||
cypress.config.js
|
||||
cypress/plugins/index.js
|
||||
coverage
|
||||
coverage
|
||||
*.json
|
||||
150
.eslintrc.cjs
Normal file
150
.eslintrc.cjs
Normal file
@@ -0,0 +1,150 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
'jest/globals': true,
|
||||
node: true,
|
||||
},
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
experimentalObjectRestSpread: true,
|
||||
jsx: true,
|
||||
},
|
||||
tsconfigRootDir: __dirname,
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
allowAutomaticSingleRunInference: true,
|
||||
project: ['./tsconfig.eslint.json', './packages/*/tsconfig.json'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:json/recommended',
|
||||
'plugin:markdown/recommended',
|
||||
'plugin:@cspell/recommended',
|
||||
'prettier',
|
||||
],
|
||||
plugins: [
|
||||
'@typescript-eslint',
|
||||
'no-only-tests',
|
||||
'html',
|
||||
'jest',
|
||||
'jsdoc',
|
||||
'json',
|
||||
'@cspell',
|
||||
'lodash',
|
||||
'unicorn',
|
||||
],
|
||||
rules: {
|
||||
curly: 'error',
|
||||
'no-console': 'error',
|
||||
'no-prototype-builtins': 'off',
|
||||
'no-unused-vars': 'off',
|
||||
'cypress/no-async-tests': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'@typescript-eslint/no-misused-promises': 'error',
|
||||
'@typescript-eslint/ban-ts-comment': [
|
||||
'error',
|
||||
{
|
||||
'ts-expect-error': 'allow-with-description',
|
||||
'ts-ignore': 'allow-with-description',
|
||||
'ts-nocheck': 'allow-with-description',
|
||||
'ts-check': 'allow-with-description',
|
||||
minimumDescriptionLength: 10,
|
||||
},
|
||||
],
|
||||
'json/*': ['error', 'allowComments'],
|
||||
'@cspell/spellchecker': [
|
||||
'error',
|
||||
{
|
||||
checkIdentifiers: false,
|
||||
checkStrings: false,
|
||||
checkStringTemplates: false,
|
||||
},
|
||||
],
|
||||
'no-empty': [
|
||||
'error',
|
||||
{
|
||||
allowEmptyCatch: true,
|
||||
},
|
||||
],
|
||||
'no-only-tests/no-only-tests': 'error',
|
||||
'lodash/import-scope': ['error', 'method'],
|
||||
'unicorn/better-regex': 'error',
|
||||
'unicorn/no-abusive-eslint-disable': 'error',
|
||||
'unicorn/no-array-push-push': 'error',
|
||||
'unicorn/no-for-loop': 'error',
|
||||
'unicorn/no-instanceof-array': 'error',
|
||||
'unicorn/no-typeof-undefined': 'error',
|
||||
'unicorn/no-unnecessary-await': 'error',
|
||||
'unicorn/no-unsafe-regex': 'warn',
|
||||
'unicorn/no-useless-promise-resolve-reject': 'error',
|
||||
'unicorn/prefer-array-find': 'error',
|
||||
'unicorn/prefer-array-flat-map': 'error',
|
||||
'unicorn/prefer-array-index-of': 'error',
|
||||
'unicorn/prefer-array-some': 'error',
|
||||
'unicorn/prefer-default-parameters': 'error',
|
||||
'unicorn/prefer-includes': 'error',
|
||||
'unicorn/prefer-negative-index': 'error',
|
||||
'unicorn/prefer-object-from-entries': 'error',
|
||||
'unicorn/prefer-string-starts-ends-with': 'error',
|
||||
'unicorn/prefer-string-trim-start-end': 'error',
|
||||
'unicorn/string-content': 'error',
|
||||
'unicorn/prefer-spread': 'error',
|
||||
'unicorn/no-lonely-if': 'error',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['cypress/**', 'demos/**'],
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.{js,jsx,mjs,cjs}'],
|
||||
extends: ['plugin:jsdoc/recommended'],
|
||||
rules: {
|
||||
'jsdoc/check-indentation': 'off',
|
||||
'jsdoc/check-alignment': 'off',
|
||||
'jsdoc/check-line-alignment': 'off',
|
||||
'jsdoc/multiline-blocks': 'off',
|
||||
'jsdoc/newline-after-description': 'off',
|
||||
'jsdoc/tag-lines': 'off',
|
||||
'jsdoc/require-param-description': 'off',
|
||||
'jsdoc/require-param-type': 'off',
|
||||
'jsdoc/require-returns': 'off',
|
||||
'jsdoc/require-returns-description': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.{ts,tsx}'],
|
||||
plugins: ['tsdoc'],
|
||||
rules: {
|
||||
'tsdoc/syntax': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.spec.{ts,js}', 'cypress/**', 'demos/**', '**/docs/**'],
|
||||
rules: {
|
||||
'jsdoc/require-jsdoc': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.html', '*.md', '**/*.md/*'],
|
||||
rules: {
|
||||
'no-var': 'error',
|
||||
'no-undef': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'off',
|
||||
'@typescript-eslint/no-misused-promises': 'off',
|
||||
},
|
||||
parserOptions: {
|
||||
project: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
137
.eslintrc.json
137
.eslintrc.json
@@ -1,137 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"jest/globals": true,
|
||||
"node": true
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": true
|
||||
},
|
||||
"sourceType": "module"
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:json/recommended",
|
||||
"plugin:markdown/recommended",
|
||||
"plugin:@cspell/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"no-only-tests",
|
||||
"html",
|
||||
"jest",
|
||||
"jsdoc",
|
||||
"json",
|
||||
"@cspell",
|
||||
"lodash",
|
||||
"unicorn"
|
||||
],
|
||||
"rules": {
|
||||
"curly": "error",
|
||||
"no-console": "error",
|
||||
"no-prototype-builtins": "off",
|
||||
"no-unused-vars": "off",
|
||||
"cypress/no-async-tests": "off",
|
||||
"@typescript-eslint/ban-ts-comment": [
|
||||
"error",
|
||||
{
|
||||
"ts-expect-error": "allow-with-description",
|
||||
"ts-ignore": "allow-with-description",
|
||||
"ts-nocheck": "allow-with-description",
|
||||
"ts-check": "allow-with-description",
|
||||
"minimumDescriptionLength": 10
|
||||
}
|
||||
],
|
||||
"json/*": ["error", "allowComments"],
|
||||
"@cspell/spellchecker": [
|
||||
"error",
|
||||
{
|
||||
"checkIdentifiers": false,
|
||||
"checkStrings": false,
|
||||
"checkStringTemplates": false
|
||||
}
|
||||
],
|
||||
"no-empty": [
|
||||
"error",
|
||||
{
|
||||
"allowEmptyCatch": true
|
||||
}
|
||||
],
|
||||
"no-only-tests/no-only-tests": "error",
|
||||
"lodash/import-scope": ["error", "method"],
|
||||
"unicorn/better-regex": "error",
|
||||
"unicorn/no-abusive-eslint-disable": "error",
|
||||
"unicorn/no-array-push-push": "error",
|
||||
"unicorn/no-for-loop": "error",
|
||||
"unicorn/no-instanceof-array": "error",
|
||||
"unicorn/no-typeof-undefined": "error",
|
||||
"unicorn/no-unnecessary-await": "error",
|
||||
"unicorn/no-unsafe-regex": "warn",
|
||||
"unicorn/no-useless-promise-resolve-reject": "error",
|
||||
"unicorn/prefer-array-find": "error",
|
||||
"unicorn/prefer-array-flat-map": "error",
|
||||
"unicorn/prefer-array-index-of": "error",
|
||||
"unicorn/prefer-array-some": "error",
|
||||
"unicorn/prefer-default-parameters": "error",
|
||||
"unicorn/prefer-includes": "error",
|
||||
"unicorn/prefer-negative-index": "error",
|
||||
"unicorn/prefer-object-from-entries": "error",
|
||||
"unicorn/prefer-string-starts-ends-with": "error",
|
||||
"unicorn/prefer-string-trim-start-end": "error",
|
||||
"unicorn/string-content": "error",
|
||||
"unicorn/prefer-spread": "error",
|
||||
"unicorn/no-lonely-if": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["cypress/**", "demos/**"],
|
||||
"rules": {
|
||||
"no-console": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.{js,jsx,mjs,cjs}"],
|
||||
"extends": ["plugin:jsdoc/recommended"],
|
||||
"rules": {
|
||||
"jsdoc/check-indentation": "off",
|
||||
"jsdoc/check-alignment": "off",
|
||||
"jsdoc/check-line-alignment": "off",
|
||||
"jsdoc/multiline-blocks": "off",
|
||||
"jsdoc/newline-after-description": "off",
|
||||
"jsdoc/tag-lines": "off",
|
||||
"jsdoc/require-param-description": "off",
|
||||
"jsdoc/require-param-type": "off",
|
||||
"jsdoc/require-returns": "off",
|
||||
"jsdoc/require-returns-description": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.{ts,tsx}"],
|
||||
"plugins": ["tsdoc"],
|
||||
"rules": {
|
||||
"tsdoc/syntax": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.spec.{ts,js}", "cypress/**", "demos/**", "**/docs/**"],
|
||||
"rules": {
|
||||
"jsdoc/require-jsdoc": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.html", "*.md", "**/*.md/*"],
|
||||
"rules": {
|
||||
"no-var": "error",
|
||||
"no-undef": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
@@ -1,6 +1,8 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [knsv]
|
||||
github:
|
||||
- knsv
|
||||
- sidharthv96
|
||||
#patreon: # Replace with a single Patreon username
|
||||
#open_collective: # Replace with a single Open Collective username
|
||||
#ko_fi: # Replace with a single Ko-fi username
|
||||
|
||||
8
.github/workflows/e2e.yml
vendored
8
.github/workflows/e2e.yml
vendored
@@ -32,6 +32,7 @@ jobs:
|
||||
# and run all Cypress tests
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v4
|
||||
id: cypress
|
||||
# If CYPRESS_RECORD_KEY is set, run in parallel on all containers
|
||||
# Otherwise (e.g. if running from fork), we run on a single container only
|
||||
if: ${{ ( env.CYPRESS_RECORD_KEY != '' ) || ( matrix.containers == 1 ) }}
|
||||
@@ -44,3 +45,10 @@ jobs:
|
||||
parallel: ${{ secrets.CYPRESS_RECORD_KEY != '' }}
|
||||
env:
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
if: ${{ failure() && steps.cypress.conclusion == 'failure' }}
|
||||
with:
|
||||
name: error-snapshots
|
||||
path: cypress/snapshots/**/__diff_output__/*
|
||||
|
||||
15
.github/workflows/lint.yml
vendored
15
.github/workflows/lint.yml
vendored
@@ -37,7 +37,20 @@ jobs:
|
||||
CYPRESS_CACHE_FOLDER: .cache/Cypress
|
||||
|
||||
- name: Run Linting
|
||||
run: pnpm run lint
|
||||
shell: bash
|
||||
run: |
|
||||
if ! pnpm run lint; then
|
||||
# print a nice error message on lint failure
|
||||
ERROR_MESSAGE='Running `pnpm run lint` failed.'
|
||||
ERROR_MESSAGE+=' Running `pnpm run lint:fix` may fix this issue. '
|
||||
ERROR_MESSAGE+=" If this error doesn't occur on your local machine,"
|
||||
ERROR_MESSAGE+=' make sure your packages are up-to-date by running `pnpm install`.'
|
||||
ERROR_MESSAGE+=' You may also need to delete your prettier cache by running'
|
||||
ERROR_MESSAGE+=' `rm ./node_modules/.cache/prettier/.prettier-cache`.'
|
||||
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
|
||||
id: verifyDocs
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
export default {
|
||||
'!(docs/**/*)*.{ts,js,json,html,md,mts}': ['eslint --fix', 'prettier --write'],
|
||||
'!(docs/**/*)*.{ts,js,json,html,md,mts}': [
|
||||
'eslint --cache --cache-strategy content --fix',
|
||||
// don't cache prettier yet, since we use `prettier-plugin-jsdoc`,
|
||||
// and prettier doesn't invalidate cache on plugin updates"
|
||||
// https://prettier.io/docs/en/cli.html#--cache
|
||||
'prettier --write',
|
||||
],
|
||||
'cSpell.json': ['ts-node-esm scripts/fixCSpell.ts'],
|
||||
'**/*.jison': ['pnpm -w run lint:jison'],
|
||||
};
|
||||
|
||||
@@ -41,16 +41,6 @@ const packageOptions = {
|
||||
packageName: 'mermaid-mindmap',
|
||||
file: 'detector.ts',
|
||||
},
|
||||
'mermaid-flowchart-v3': {
|
||||
name: 'mermaid-flowchart-v3',
|
||||
packageName: 'mermaid-flowchart-v3',
|
||||
file: 'detector.ts',
|
||||
},
|
||||
// 'mermaid-example-diagram-detector': {
|
||||
// name: 'mermaid-example-diagram-detector',
|
||||
// packageName: 'mermaid-example-diagram',
|
||||
// file: 'detector.ts',
|
||||
// },
|
||||
};
|
||||
|
||||
interface BuildOptions {
|
||||
@@ -124,12 +114,7 @@ export const getBuildConfig = ({ minify, core, watch, entryName }: BuildOptions)
|
||||
|
||||
if (watch && config.build) {
|
||||
config.build.watch = {
|
||||
include: [
|
||||
'packages/mermaid-flowchart-v3/src/**',
|
||||
'packages/mermaid-mindmap/src/**',
|
||||
'packages/mermaid/src/**',
|
||||
// 'packages/mermaid-example-diagram/src/**',
|
||||
],
|
||||
include: ['packages/mermaid-mindmap/src/**', 'packages/mermaid/src/**'],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -154,9 +139,7 @@ const main = async () => {
|
||||
if (watch) {
|
||||
build(getBuildConfig({ minify: false, watch, core: false, entryName: 'mermaid' }));
|
||||
if (!mermaidOnly) {
|
||||
build(getBuildConfig({ minify: false, watch, entryName: 'mermaid-flowchart-v3' }));
|
||||
build(getBuildConfig({ minify: false, watch, entryName: 'mermaid-mindmap' }));
|
||||
// build(getBuildConfig({ minify: false, watch, entryName: 'mermaid-example-diagram' }));
|
||||
}
|
||||
} else if (visualize) {
|
||||
await build(getBuildConfig({ minify: false, core: true, entryName: 'mermaid' }));
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import express, { NextFunction, Request, Response } from 'express';
|
||||
import { createServer as createViteServer } from 'vite';
|
||||
// import { getBuildConfig } from './build';
|
||||
|
||||
const cors = (req: Request, res: Response, next: NextFunction) => {
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
@@ -22,7 +21,7 @@ async function createServer() {
|
||||
|
||||
app.use(cors);
|
||||
app.use(express.static('./packages/mermaid/dist'));
|
||||
app.use(express.static('./packages/mermaid-example-diagram/dist'));
|
||||
// app.use(express.static('./packages/mermaid-example-diagram/dist'));
|
||||
app.use(express.static('./packages/mermaid-mindmap/dist'));
|
||||
app.use(vite.middlewares);
|
||||
app.use(express.static('demos'));
|
||||
@@ -33,5 +32,4 @@ async function createServer() {
|
||||
});
|
||||
}
|
||||
|
||||
// build(getBuildConfig({ minify: false, watch: true }));
|
||||
createServer();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# mermaid
|
||||
|
||||
[](https://github.com/mermaid-js/mermaid/actions/workflows/build.yml) [](https://www.npmjs.com/package/mermaid) [](https://bundlephobia.com/package/mermaid) [](https://coveralls.io/github/mermaid-js/mermaid?branch=master) [](https://www.jsdelivr.com/package/npm/mermaid) [](https://www.npmjs.com/package/mermaid) [](https://join.slack.com/t/mermaid-talk/shared_invite/enQtNzc4NDIyNzk4OTAyLWVhYjQxOTI2OTg4YmE1ZmJkY2Y4MTU3ODliYmIwOTY3NDJlYjA0YjIyZTdkMDMyZTUwOGI0NjEzYmEwODcwOTE) [](https://twitter.com/mermaidjs_)
|
||||
[](https://github.com/mermaid-js/mermaid/actions/workflows/build.yml) [](https://www.npmjs.com/package/mermaid) [](https://bundlephobia.com/package/mermaid) [](https://coveralls.io/github/mermaid-js/mermaid?branch=master) [](https://www.jsdelivr.com/package/npm/mermaid) [](https://www.npmjs.com/package/mermaid) [](https://join.slack.com/t/mermaid-talk/shared_invite/enQtNzc4NDIyNzk4OTAyLWVhYjQxOTI2OTg4YmE1ZmJkY2Y4MTU3ODliYmIwOTY3NDJlYjA0YjIyZTdkMDMyZTUwOGI0NjEzYmEwODcwOTE) [](https://twitter.com/mermaidjs_)
|
||||
|
||||
English | [简体中文](./README.zh-CN.md)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# mermaid
|
||||
|
||||
[](https://github.com/mermaid-js/mermaid/actions/workflows/build.yml) [](https://www.npmjs.com/package/mermaid) [](https://bundlephobia.com/package/mermaid) [](https://coveralls.io/github/mermaid-js/mermaid?branch=master) [](https://www.jsdelivr.com/package/npm/mermaid) [](https://www.npmjs.com/package/mermaid) [](https://join.slack.com/t/mermaid-talk/shared_invite/enQtNzc4NDIyNzk4OTAyLWVhYjQxOTI2OTg4YmE1ZmJkY2Y4MTU3ODliYmIwOTY3NDJlYjA0YjIyZTdkMDMyZTUwOGI0NjEzYmEwODcwOTE) [](https://twitter.com/mermaidjs_)
|
||||
[](https://github.com/mermaid-js/mermaid/actions/workflows/build.yml) [](https://www.npmjs.com/package/mermaid) [](https://bundlephobia.com/package/mermaid) [](https://coveralls.io/github/mermaid-js/mermaid?branch=master) [](https://www.jsdelivr.com/package/npm/mermaid) [](https://www.npmjs.com/package/mermaid) [](https://join.slack.com/t/mermaid-talk/shared_invite/enQtNzc4NDIyNzk4OTAyLWVhYjQxOTI2OTg4YmE1ZmJkY2Y4MTU3ODliYmIwOTY3NDJlYjA0YjIyZTdkMDMyZTUwOGI0NjEzYmEwODcwOTE) [](https://twitter.com/mermaidjs_)
|
||||
|
||||
[English](./README.md) | 简体中文
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"adamiecki",
|
||||
"alois",
|
||||
"antiscript",
|
||||
"appli",
|
||||
"applitools",
|
||||
"asciidoctor",
|
||||
"ashish",
|
||||
@@ -13,6 +14,7 @@
|
||||
"bbox",
|
||||
"bilkent",
|
||||
"bisheng",
|
||||
"blrs",
|
||||
"braintree",
|
||||
"brkt",
|
||||
"brolin",
|
||||
@@ -54,8 +56,10 @@
|
||||
"knut",
|
||||
"laganeckas",
|
||||
"lintstagedrc",
|
||||
"logmsg",
|
||||
"lucida",
|
||||
"matthieu",
|
||||
"mdast",
|
||||
"mdbook",
|
||||
"mermerd",
|
||||
"mindaugas",
|
||||
@@ -81,6 +85,7 @@
|
||||
"setupgraphviewbox",
|
||||
"shiki",
|
||||
"sidharth",
|
||||
"sidharthv",
|
||||
"sphinxcontrib",
|
||||
"statediagram",
|
||||
"stylis",
|
||||
|
||||
@@ -2,7 +2,7 @@ const utf8ToB64 = (str) => {
|
||||
return window.btoa(unescape(encodeURIComponent(str)));
|
||||
};
|
||||
|
||||
const batchId = 'mermid-batch' + new Date().getTime();
|
||||
const batchId = 'mermaid-batch' + new Date().getTime();
|
||||
|
||||
export const mermaidUrl = (graphStr, options, api) => {
|
||||
const obj = {
|
||||
@@ -46,8 +46,22 @@ export const imgSnapshotTest = (graphStr, _options, api = false, validation) =>
|
||||
if (!options.fontSize) {
|
||||
options.fontSize = '16px';
|
||||
}
|
||||
const url = mermaidUrl(graphStr, options, api);
|
||||
openURLAndVerifyRendering(url, options, validation);
|
||||
};
|
||||
|
||||
export const urlSnapshotTest = (url, _options, api = false, validation) => {
|
||||
const options = Object.assign(_options);
|
||||
openURLAndVerifyRendering(url, options, validation);
|
||||
};
|
||||
|
||||
export const renderGraph = (graphStr, options, api) => {
|
||||
const url = mermaidUrl(graphStr, options, api);
|
||||
openURLAndVerifyRendering(url, options);
|
||||
};
|
||||
|
||||
const openURLAndVerifyRendering = (url, options, validation = undefined) => {
|
||||
const useAppli = Cypress.env('useAppli');
|
||||
cy.log('Hello ' + useAppli ? 'Appli' : 'image-snapshot');
|
||||
const name = (options.name || cy.state('runnable').fullTitle()).replace(/\s+/g, '-');
|
||||
|
||||
if (useAppli) {
|
||||
@@ -60,82 +74,20 @@ export const imgSnapshotTest = (graphStr, _options, api = false, validation) =>
|
||||
});
|
||||
}
|
||||
|
||||
const url = mermaidUrl(graphStr, options, api);
|
||||
|
||||
cy.visit(url);
|
||||
cy.window().should('have.property', 'rendered', true);
|
||||
cy.get('svg').should('be.visible');
|
||||
|
||||
if (validation) {
|
||||
cy.get('svg').should(validation);
|
||||
}
|
||||
cy.get('svg');
|
||||
// Default name to test title
|
||||
|
||||
if (useAppli) {
|
||||
cy.log('Check eyes' + Cypress.spec.name);
|
||||
cy.eyesCheckWindow('Click!');
|
||||
cy.log('Closing eyes: ' + Cypress.spec.name);
|
||||
cy.log('Closing eyes' + Cypress.spec.name);
|
||||
cy.eyesClose();
|
||||
} else {
|
||||
cy.matchImageSnapshot(name);
|
||||
}
|
||||
};
|
||||
|
||||
export const urlSnapshotTest = (url, _options, api = false, validation) => {
|
||||
cy.log(_options);
|
||||
const options = Object.assign(_options);
|
||||
if (!options.fontFamily) {
|
||||
options.fontFamily = 'courier';
|
||||
}
|
||||
if (!options.sequence) {
|
||||
options.sequence = {};
|
||||
}
|
||||
if (!options.sequence || (options.sequence && !options.sequence.actorFontFamily)) {
|
||||
options.sequence.actorFontFamily = 'courier';
|
||||
}
|
||||
if (options.sequence && !options.sequence.noteFontFamily) {
|
||||
options.sequence.noteFontFamily = 'courier';
|
||||
}
|
||||
options.sequence.actorFontFamily = 'courier';
|
||||
options.sequence.noteFontFamily = 'courier';
|
||||
options.sequence.messageFontFamily = 'courier';
|
||||
if (options.sequence && !options.sequence.actorFontFamily) {
|
||||
options.sequence.actorFontFamily = 'courier';
|
||||
}
|
||||
if (!options.fontSize) {
|
||||
options.fontSize = '16px';
|
||||
}
|
||||
const useAppli = Cypress.env('useAppli');
|
||||
cy.log('Hello ' + useAppli ? 'Appli' : 'image-snapshot');
|
||||
const name = (options.name || cy.state('runnable').fullTitle()).replace(/\s+/g, '-');
|
||||
|
||||
if (useAppli) {
|
||||
cy.log('Opening eyes 2' + Cypress.spec.name);
|
||||
cy.eyesOpen({
|
||||
appName: 'Mermaid',
|
||||
testName: name,
|
||||
batchName: Cypress.spec.name,
|
||||
batchId: batchId + Cypress.spec.name,
|
||||
});
|
||||
}
|
||||
|
||||
cy.visit(url);
|
||||
if (validation) {
|
||||
cy.get('svg').should(validation);
|
||||
}
|
||||
cy.get('body');
|
||||
// Default name to test title
|
||||
|
||||
if (useAppli) {
|
||||
cy.log('Check eyes 2' + Cypress.spec.name);
|
||||
cy.eyesCheckWindow('Click!');
|
||||
cy.log('Closing eyes 2' + Cypress.spec.name);
|
||||
cy.eyesClose();
|
||||
} else {
|
||||
cy.matchImageSnapshot(name);
|
||||
}
|
||||
};
|
||||
|
||||
export const renderGraph = (graphStr, options, api) => {
|
||||
const url = mermaidUrl(graphStr, options, api);
|
||||
|
||||
cy.visit(url);
|
||||
};
|
||||
|
||||
687
cypress/integration/rendering/flowchart-elk.spec.js
Normal file
687
cypress/integration/rendering/flowchart-elk.spec.js
Normal file
@@ -0,0 +1,687 @@
|
||||
import { imgSnapshotTest, renderGraph } from '../../helpers/util';
|
||||
|
||||
describe('Flowchart ELK', () => {
|
||||
it('1-elk: should render a simple flowchart', () => {
|
||||
imgSnapshotTest(
|
||||
`flowchart-elk TD
|
||||
A[Christmas] -->|Get money| B(Go shopping)
|
||||
B --> C{Let me think}
|
||||
C -->|One| D[Laptop]
|
||||
C -->|Two| E[iPhone]
|
||||
C -->|Three| F[fa:fa-car Car]
|
||||
`,
|
||||
{}
|
||||
);
|
||||
imgSnapshotTest(
|
||||
`flowchart TD
|
||||
A[Christmas] -->|Get money| B(Go shopping)
|
||||
B --> C{Let me think}
|
||||
C -->|One| D[Laptop]
|
||||
C -->|Two| E[iPhone]
|
||||
C -->|Three| F[fa:fa-car Car]
|
||||
`,
|
||||
{ flowchart: { defaultRenderer: 'elk' } }
|
||||
);
|
||||
});
|
||||
|
||||
it('2-elk: should render a simple flowchart with diagramPadding set to 0', () => {
|
||||
imgSnapshotTest(
|
||||
`flowchart-elk TD
|
||||
A[Christmas] -->|Get money| B(Go shopping)
|
||||
B --> C{Let me think}
|
||||
%% this is a comment
|
||||
C -->|One| D[Laptop]
|
||||
C -->|Two| E[iPhone]
|
||||
C -->|Three| F[fa:fa-car Car]
|
||||
`,
|
||||
{ flowchart: { diagramPadding: 0 } }
|
||||
);
|
||||
});
|
||||
|
||||
it('3-elk: a link with correct arrowhead to a subgraph', () => {
|
||||
imgSnapshotTest(
|
||||
`flowchart-elk TD
|
||||
P1
|
||||
P1 -->P1.5
|
||||
subgraph P1.5
|
||||
P2
|
||||
P2.5(( A ))
|
||||
P3
|
||||
end
|
||||
P2 --> P4
|
||||
P3 --> P6
|
||||
P1.5 --> P5
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('4-elk: Length of edges', () => {
|
||||
imgSnapshotTest(
|
||||
`flowchart-elk TD
|
||||
L1 --- L2
|
||||
L2 --- C
|
||||
M1 ---> C
|
||||
R1 .-> R2
|
||||
R2 <.-> C
|
||||
C -->|Label 1| E1
|
||||
C <-- Label 2 ---> E2
|
||||
C ----> E3
|
||||
C <-...-> E4
|
||||
C ======> E5
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('5-elk: should render escaped without html labels', () => {
|
||||
imgSnapshotTest(
|
||||
`flowchart-elk TD
|
||||
a["<strong>Haiya</strong>"]---->b
|
||||
`,
|
||||
{ htmlLabels: false, flowchart: { htmlLabels: false } }
|
||||
);
|
||||
});
|
||||
it('6-elk: should render non-escaped with html labels', () => {
|
||||
imgSnapshotTest(
|
||||
`flowchart-elk TD
|
||||
a["<strong>Haiya</strong>"]===>b
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
it('7-elk: should render a flowchart when useMaxWidth is true (default)', () => {
|
||||
renderGraph(
|
||||
`flowchart-elk TD
|
||||
A[Christmas] -->|Get money| B(Go shopping)
|
||||
B --> C{Let me think}
|
||||
C -->|One| D[Laptop]
|
||||
C -->|Two| E[iPhone]
|
||||
C -->|Three| F[fa:fa-car Car]
|
||||
`,
|
||||
{ flowchart: { useMaxWidth: true } }
|
||||
);
|
||||
cy.get('svg').should((svg) => {
|
||||
expect(svg).to.have.attr('width', '100%');
|
||||
// expect(svg).to.have.attr('height');
|
||||
// use within because the absolute value can be slightly different depending on the environment ±5%
|
||||
// const height = parseFloat(svg.attr('height'));
|
||||
// expect(height).to.be.within(446 * 0.95, 446 * 1.05);
|
||||
const style = svg.attr('style');
|
||||
expect(style).to.match(/^max-width: [\d.]+px;$/);
|
||||
const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join(''));
|
||||
expect(maxWidthValue).to.be.within(230 * 0.95, 230 * 1.05);
|
||||
});
|
||||
});
|
||||
it('8-elk: should render a flowchart when useMaxWidth is false', () => {
|
||||
renderGraph(
|
||||
`flowchart-elk TD
|
||||
A[Christmas] -->|Get money| B(Go shopping)
|
||||
B --> C{Let me think}
|
||||
C -->|One| D[Laptop]
|
||||
C -->|Two| E[iPhone]
|
||||
C -->|Three| F[fa:fa-car Car]
|
||||
`,
|
||||
{ flowchart: { useMaxWidth: false } }
|
||||
);
|
||||
cy.get('svg').should((svg) => {
|
||||
// const height = parseFloat(svg.attr('height'));
|
||||
const width = parseFloat(svg.attr('width'));
|
||||
// use within because the absolute value can be slightly different depending on the environment ±5%
|
||||
// expect(height).to.be.within(446 * 0.95, 446 * 1.05);
|
||||
expect(width).to.be.within(230 * 0.95, 230 * 1.05);
|
||||
expect(svg).to.not.have.attr('style');
|
||||
});
|
||||
});
|
||||
|
||||
it('V2 elk - 16: Render Stadium shape', () => {
|
||||
imgSnapshotTest(
|
||||
` flowchart-elk TD
|
||||
A([stadium shape test])
|
||||
A -->|Get money| B([Go shopping])
|
||||
B --> C([Let me think...<br />Do I want something for work,<br />something to spend every free second with,<br />or something to get around?])
|
||||
C -->|One| D([Laptop])
|
||||
C -->|Two| E([iPhone])
|
||||
C -->|Three| F([Car<br/>wroom wroom])
|
||||
click A "index.html#link-clicked" "link test"
|
||||
click B testClick "click test"
|
||||
classDef someclass fill:#f96;
|
||||
class A someclass;
|
||||
class C someclass;
|
||||
`,
|
||||
{ flowchart: { htmlLabels: false }, fontFamily: 'courier' }
|
||||
);
|
||||
});
|
||||
|
||||
it('50-elk: handle nested subgraphs in reverse order', () => {
|
||||
imgSnapshotTest(
|
||||
`flowchart-elk LR
|
||||
a -->b
|
||||
subgraph A
|
||||
B
|
||||
end
|
||||
subgraph B
|
||||
b
|
||||
end
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
|
||||
it('51-elk: handle nested subgraphs in reverse order', () => {
|
||||
imgSnapshotTest(
|
||||
`flowchart-elk LR
|
||||
a -->b
|
||||
subgraph A
|
||||
B
|
||||
end
|
||||
subgraph B
|
||||
b
|
||||
end
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
|
||||
it('52-elk: handle nested subgraphs in several levels', () => {
|
||||
imgSnapshotTest(
|
||||
`flowchart-elk TB
|
||||
b-->B
|
||||
a-->c
|
||||
subgraph O
|
||||
A
|
||||
end
|
||||
subgraph B
|
||||
c
|
||||
end
|
||||
subgraph A
|
||||
a
|
||||
b
|
||||
B
|
||||
end
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
|
||||
it('53-elk: handle nested subgraphs with edges in and out', () => {
|
||||
imgSnapshotTest(
|
||||
`flowchart-elk TB
|
||||
internet
|
||||
nat
|
||||
routeur
|
||||
lb1
|
||||
lb2
|
||||
compute1
|
||||
compute2
|
||||
subgraph project
|
||||
routeur
|
||||
nat
|
||||
subgraph subnet1
|
||||
compute1
|
||||
lb1
|
||||
end
|
||||
subgraph subnet2
|
||||
compute2
|
||||
lb2
|
||||
end
|
||||
end
|
||||
internet --> routeur
|
||||
routeur --> subnet1 & subnet2
|
||||
subnet1 & subnet2 --> nat --> internet
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
|
||||
it('54-elk: handle nested subgraphs with outgoing links', () => {
|
||||
imgSnapshotTest(
|
||||
`flowchart-elk TD
|
||||
subgraph main
|
||||
subgraph subcontainer
|
||||
subcontainer-child
|
||||
end
|
||||
subcontainer-child--> subcontainer-sibling
|
||||
end
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
|
||||
it('55-elk: handle nested subgraphs with outgoing links 2', () => {
|
||||
imgSnapshotTest(
|
||||
`flowchart-elk TD
|
||||
|
||||
subgraph one[One]
|
||||
subgraph sub_one[Sub One]
|
||||
_sub_one
|
||||
end
|
||||
subgraph sub_two[Sub Two]
|
||||
_sub_two
|
||||
end
|
||||
_one
|
||||
end
|
||||
|
||||
%% here, either the first or the second one
|
||||
sub_one --> sub_two
|
||||
_one --> b
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
|
||||
it('56-elk: handle nested subgraphs with outgoing links 3', () => {
|
||||
imgSnapshotTest(
|
||||
`flowchart-elk TB
|
||||
subgraph container_Beta
|
||||
process_C-->Process_D
|
||||
end
|
||||
subgraph container_Alpha
|
||||
process_A-->process_B
|
||||
process_A-->|messages|process_C
|
||||
end
|
||||
process_B-->|via_AWSBatch|container_Beta
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
it('57-elk: handle nested subgraphs with outgoing links 4', () => {
|
||||
imgSnapshotTest(
|
||||
`flowchart-elk LR
|
||||
subgraph A
|
||||
a -->b
|
||||
end
|
||||
subgraph B
|
||||
b
|
||||
end
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
|
||||
it('57-elk: handle nested subgraphs with outgoing links 2', () => {
|
||||
imgSnapshotTest(
|
||||
`flowchart-elk TB
|
||||
c1-->a2
|
||||
subgraph one
|
||||
a1-->a2
|
||||
end
|
||||
subgraph two
|
||||
b1-->b2
|
||||
end
|
||||
subgraph three
|
||||
c1-->c2
|
||||
end
|
||||
one --> two
|
||||
three --> two
|
||||
two --> c2
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
it('57.x: handle nested subgraphs with outgoing links 5', () => {
|
||||
imgSnapshotTest(
|
||||
`%% this does not produce the desired result
|
||||
flowchart-elk TB
|
||||
subgraph container_Beta
|
||||
process_C-->Process_D
|
||||
end
|
||||
subgraph container_Alpha
|
||||
process_A-->process_B
|
||||
process_B-->|via_AWSBatch|container_Beta
|
||||
process_A-->|messages|process_C
|
||||
end
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
it('58-elk: handle styling with style expressions', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
flowchart-elk LR
|
||||
id1(Start)-->id2(Stop)
|
||||
style id1 fill:#f9f,stroke:#333,stroke-width:4px
|
||||
style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
it('59-elk: handle styling of subgraphs and links', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
flowchart-elk TD
|
||||
A[Christmas] ==> D
|
||||
A[Christmas] -->|Get money| B(Go shopping)
|
||||
A[Christmas] ==> C
|
||||
subgraph T ["Test"]
|
||||
A
|
||||
B
|
||||
C
|
||||
end
|
||||
|
||||
classDef Test fill:#F84E68,stroke:#333,color:white;
|
||||
class A,T Test
|
||||
classDef TestSub fill:green;
|
||||
class T TestSub
|
||||
linkStyle 0,1 color:orange, stroke: orange;
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
it('60-elk: handle styling for all node shapes - v2', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
flowchart-elk LR
|
||||
A[red text] -->|default style| B(blue text)
|
||||
C([red text]) -->|default style| D[[blue text]]
|
||||
E[(red text)] -->|default style| F((blue text))
|
||||
G>red text] -->|default style| H{blue text}
|
||||
I{{red text}} -->|default style| J[/blue text/]
|
||||
K[\\ red text\\] -->|default style| L[/blue text\\]
|
||||
M[\\ red text/] -->|default style| N[blue text];
|
||||
O(((red text))) -->|default style| P(((blue text)));
|
||||
linkStyle default color:Sienna;
|
||||
style A stroke:#ff0000,fill:#ffcccc,color:#ff0000;
|
||||
style B stroke:#0000ff,fill:#ccccff,color:#0000ff;
|
||||
style C stroke:#ff0000,fill:#ffcccc,color:#ff0000;
|
||||
style D stroke:#0000ff,fill:#ccccff,color:#0000ff;
|
||||
style E stroke:#ff0000,fill:#ffcccc,color:#ff0000;
|
||||
style F stroke:#0000ff,fill:#ccccff,color:#0000ff;
|
||||
style G stroke:#ff0000,fill:#ffcccc,color:#ff0000;
|
||||
style H stroke:#0000ff,fill:#ccccff,color:#0000ff;
|
||||
style I stroke:#ff0000,fill:#ffcccc,color:#ff0000;
|
||||
style J stroke:#0000ff,fill:#ccccff,color:#0000ff;
|
||||
style K stroke:#ff0000,fill:#ffcccc,color:#ff0000;
|
||||
style L stroke:#0000ff,fill:#ccccff,color:#0000ff;
|
||||
style M stroke:#ff0000,fill:#ffcccc,color:#ff0000;
|
||||
style N stroke:#0000ff,fill:#ccccff,color:#0000ff;
|
||||
style O stroke:#ff0000,fill:#ffcccc,color:#ff0000;
|
||||
style P stroke:#0000ff,fill:#ccccff,color:#0000ff;
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose', logLevel: 2 }
|
||||
);
|
||||
});
|
||||
it('61-elk: fontawesome icons in edge labels', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
flowchart-elk TD
|
||||
C -->|fa:fa-car Car| F[fa:fa-car Car]
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
it('62-elk: should render styled subgraphs', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
flowchart-elk TB
|
||||
A
|
||||
B
|
||||
subgraph foo[Foo SubGraph]
|
||||
C
|
||||
D
|
||||
end
|
||||
subgraph bar[Bar SubGraph]
|
||||
E
|
||||
F
|
||||
end
|
||||
G
|
||||
|
||||
A-->B
|
||||
B-->C
|
||||
C-->D
|
||||
B-->D
|
||||
D-->E
|
||||
E-->A
|
||||
E-->F
|
||||
F-->D
|
||||
F-->G
|
||||
B-->G
|
||||
G-->D
|
||||
|
||||
style foo fill:#F99,stroke-width:2px,stroke:#F0F,color:darkred
|
||||
style bar fill:#999,stroke-width:10px,stroke:#0F0,color:blue
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
it('63-elk: title on subgraphs should be themable', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
%%{init:{"theme":"base", "themeVariables": {"primaryColor":"#411d4e", "titleColor":"white", "darkMode":true}}}%%
|
||||
flowchart-elk LR
|
||||
subgraph A
|
||||
a --> b
|
||||
end
|
||||
subgraph B
|
||||
i -->f
|
||||
end
|
||||
A --> B
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
it('65-elk: text-color from classes', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
flowchart-elk LR
|
||||
classDef dark fill:#000,stroke:#000,stroke-width:4px,color:#fff
|
||||
Lorem --> Ipsum --> Dolor
|
||||
class Lorem,Dolor dark
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
it('66-elk: More nested subgraph cases (TB)', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
flowchart-elk TB
|
||||
subgraph two
|
||||
b1
|
||||
end
|
||||
subgraph three
|
||||
c2
|
||||
end
|
||||
|
||||
three --> two
|
||||
two --> c2
|
||||
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
it('67-elk: More nested subgraph cases (RL)', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
flowchart-elk RL
|
||||
subgraph two
|
||||
b1
|
||||
end
|
||||
subgraph three
|
||||
c2
|
||||
end
|
||||
|
||||
three --> two
|
||||
two --> c2
|
||||
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
it('68-elk: More nested subgraph cases (BT)', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
flowchart-elk BT
|
||||
subgraph two
|
||||
b1
|
||||
end
|
||||
subgraph three
|
||||
c2
|
||||
end
|
||||
|
||||
three --> two
|
||||
two --> c2
|
||||
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
it('69-elk: More nested subgraph cases (LR)', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
flowchart-elk LR
|
||||
subgraph two
|
||||
b1
|
||||
end
|
||||
subgraph three
|
||||
c2
|
||||
end
|
||||
|
||||
three --> two
|
||||
two --> c2
|
||||
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
it('70-elk: Handle nested subgraph cases (TB) link out and link between subgraphs', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
flowchart-elk TB
|
||||
subgraph S1
|
||||
sub1 -->sub2
|
||||
end
|
||||
subgraph S2
|
||||
sub4
|
||||
end
|
||||
S1 --> S2
|
||||
sub1 --> sub4
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
it('71-elk: Handle nested subgraph cases (RL) link out and link between subgraphs', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
flowchart-elk RL
|
||||
subgraph S1
|
||||
sub1 -->sub2
|
||||
end
|
||||
subgraph S2
|
||||
sub4
|
||||
end
|
||||
S1 --> S2
|
||||
sub1 --> sub4
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
it('72-elk: Handle nested subgraph cases (BT) link out and link between subgraphs', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
flowchart-elk BT
|
||||
subgraph S1
|
||||
sub1 -->sub2
|
||||
end
|
||||
subgraph S2
|
||||
sub4
|
||||
end
|
||||
S1 --> S2
|
||||
sub1 --> sub4
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
it('74-elk: Handle nested subgraph cases (RL) link out and link between subgraphs', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
flowchart-elk RL
|
||||
subgraph S1
|
||||
sub1 -->sub2
|
||||
end
|
||||
subgraph S2
|
||||
sub4
|
||||
end
|
||||
S1 --> S2
|
||||
sub1 --> sub4
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
it('74-elk: Handle labels for multiple edges from and to the same couple of nodes', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
flowchart-elk RL
|
||||
subgraph one
|
||||
a1 -- l1 --> a2
|
||||
a1 -- l2 --> a2
|
||||
end
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
|
||||
it('76-elk: handle unicode encoded character with HTML labels true', () => {
|
||||
imgSnapshotTest(
|
||||
`flowchart-elk TB
|
||||
a{{"Lorem 'ipsum' dolor 'sit' amet, 'consectetur' adipiscing 'elit'."}}
|
||||
--> b{{"Lorem #quot;ipsum#quot; dolor #quot;sit#quot; amet,#quot;consectetur#quot; adipiscing #quot;elit#quot;."}}
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
|
||||
it('2050-elk: handling of different rendering direction in subgraphs', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
flowchart-elk LR
|
||||
|
||||
subgraph TOP
|
||||
direction TB
|
||||
subgraph B1
|
||||
direction RL
|
||||
i1 -->f1
|
||||
end
|
||||
subgraph B2
|
||||
direction BT
|
||||
i2 -->f2
|
||||
end
|
||||
end
|
||||
A --> TOP --> B
|
||||
B1 --> B2
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
|
||||
it('2388-elk: handling default in the node name', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
flowchart-elk LR
|
||||
default-index.js --> dot.template.js
|
||||
index.js --> module-utl.js
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
it('2824-elk: Clipping of edges', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
flowchart-elk TD
|
||||
A --> B
|
||||
A --> C
|
||||
B --> C
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
);
|
||||
});
|
||||
it('1433-elk: should render a titled flowchart with titleTopMargin set to 0', () => {
|
||||
imgSnapshotTest(
|
||||
`---
|
||||
title: Simple flowchart
|
||||
---
|
||||
flowchart-elk TD
|
||||
A --> B
|
||||
`,
|
||||
{ titleTopMargin: 0 }
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { imgSnapshotTest, renderGraph } from '../../helpers/util.js';
|
||||
import { imgSnapshotTest } from '../../helpers/util.js';
|
||||
|
||||
/**
|
||||
* Check whether the SVG Element has a Mindmap root
|
||||
@@ -158,7 +158,6 @@ mindmap
|
||||
undefined,
|
||||
shouldHaveRoot
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
it('rounded rect shape', () => {
|
||||
imgSnapshotTest(
|
||||
@@ -172,7 +171,6 @@ mindmap
|
||||
undefined,
|
||||
shouldHaveRoot
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
it('circle shape', () => {
|
||||
imgSnapshotTest(
|
||||
@@ -186,7 +184,6 @@ mindmap
|
||||
undefined,
|
||||
shouldHaveRoot
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
it('default shape', () => {
|
||||
imgSnapshotTest(
|
||||
@@ -198,7 +195,6 @@ mindmap
|
||||
undefined,
|
||||
shouldHaveRoot
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
it('adding children', () => {
|
||||
imgSnapshotTest(
|
||||
@@ -212,7 +208,6 @@ mindmap
|
||||
undefined,
|
||||
shouldHaveRoot
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
it('adding grand children', () => {
|
||||
imgSnapshotTest(
|
||||
@@ -227,7 +222,6 @@ mindmap
|
||||
undefined,
|
||||
shouldHaveRoot
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
/* The end */
|
||||
});
|
||||
|
||||
@@ -3,6 +3,42 @@
|
||||
import { imgSnapshotTest, renderGraph } from '../../helpers/util';
|
||||
|
||||
context('Sequence diagram', () => {
|
||||
it('should render a sequence diagram with boxes', () => {
|
||||
renderGraph(
|
||||
`
|
||||
sequenceDiagram
|
||||
box LightGrey Alice and Bob
|
||||
participant Alice
|
||||
participant Bob
|
||||
end
|
||||
participant John as John<br/>Second Line
|
||||
Alice ->> Bob: Hello Bob, how are you?
|
||||
Bob-->>John: How about you John?
|
||||
Bob--x Alice: I am good thanks!
|
||||
Bob-x John: I am good thanks!
|
||||
Note right of John: Bob thinks a long<br/>long time, so long<br/>that the text does<br/>not fit on a row.
|
||||
Bob-->Alice: Checking with John...
|
||||
alt either this
|
||||
Alice->>John: Yes
|
||||
else or this
|
||||
Alice->>John: No
|
||||
else or this will happen
|
||||
Alice->John: Maybe
|
||||
end
|
||||
par this happens in parallel
|
||||
Alice -->> Bob: Parallel message 1
|
||||
and
|
||||
Alice -->> John: Parallel message 2
|
||||
end
|
||||
`,
|
||||
{ sequence: { useMaxWidth: false } }
|
||||
);
|
||||
cy.get('svg').should((svg) => {
|
||||
const width = parseFloat(svg.attr('width'));
|
||||
expect(width).to.be.within(830 * 0.95, 830 * 1.05);
|
||||
expect(svg).to.not.have.attr('style');
|
||||
});
|
||||
});
|
||||
it('should render a simple sequence diagram', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
|
||||
164
cypress/integration/rendering/timeline.spec.ts
Normal file
164
cypress/integration/rendering/timeline.spec.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { imgSnapshotTest } from '../../helpers/util.js';
|
||||
|
||||
describe('Timeline diagram', () => {
|
||||
it('1: should render a simple timeline with no specific sections', () => {
|
||||
imgSnapshotTest(
|
||||
`timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('2: should render a timeline diagram with sections', () => {
|
||||
imgSnapshotTest(
|
||||
`timeline
|
||||
title Timeline of Industrial Revolution
|
||||
section 17th-20th century
|
||||
Industry 1.0 : Machinery, Water power, Steam <br>power
|
||||
Industry 2.0 : Electricity, Internal combustion engine, Mass production
|
||||
Industry 3.0 : Electronics, Computers, Automation
|
||||
section 21st century
|
||||
Industry 4.0 : Internet, Robotics, Internet of Things
|
||||
Industry 5.0 : Artificial intelligence, Big data,3D printing
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('3: should render a complex timeline with sections, and long events text with <br>', () => {
|
||||
imgSnapshotTest(
|
||||
`timeline
|
||||
title England's History Timeline
|
||||
section Stone Age
|
||||
7600 BC : Britain's oldest known house was built in Orkney, Scotland
|
||||
6000 BC : Sea levels rise and Britain becomes an island.<br> The people who live here are hunter-gatherers.
|
||||
section Broze Age
|
||||
2300 BC : People arrive from Europe and settle in Britain. <br>They bring farming and metalworking.
|
||||
: New styles of pottery and ways of burying the dead appear.
|
||||
2200 BC : The last major building works are completed at Stonehenge.<br> People now bury their dead in stone circles.
|
||||
: The first metal objects are made in Britain.Some other nice things happen. it is a good time to be alive.
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('4: should render a simple timeline with directives and disableMultiColor:true ', () => {
|
||||
imgSnapshotTest(
|
||||
`%%{init: { 'logLevel': 'debug', 'theme': 'base', 'timeline': {'disableMulticolor': true}}}%%
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('5: should render a simple timeline with directive overriden colors', () => {
|
||||
imgSnapshotTest(
|
||||
` %%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
|
||||
'cScale0': '#ff0000',
|
||||
'cScale1': '#00ff00',
|
||||
'cScale2': '#0000ff'
|
||||
} } }%%
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008 : Instagram
|
||||
2010 : Pinterest
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('6: should render a simple timeline in base theme', () => {
|
||||
imgSnapshotTest(
|
||||
`%%{init: { 'logLevel': 'debug', 'theme': 'base' } }%%
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008 : Instagram
|
||||
2010 : Pinterest
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('7: should render a simple timeline in default theme', () => {
|
||||
imgSnapshotTest(
|
||||
`%%{init: { 'logLevel': 'debug', 'theme': 'default' } }%%
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008 : Instagram
|
||||
2010 : Pinterest
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('8: should render a simple timeline in dark theme', () => {
|
||||
imgSnapshotTest(
|
||||
`%%{init: { 'logLevel': 'debug', 'theme': 'dark' } }%%
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008 : Instagram
|
||||
2010 : Pinterest
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('9: should render a simple timeline in neutral theme', () => {
|
||||
imgSnapshotTest(
|
||||
`%%{init: { 'logLevel': 'debug', 'theme': 'neutral' } }%%
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008 : Instagram
|
||||
2010 : Pinterest
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('10: should render a simple timeline in forest theme', () => {
|
||||
imgSnapshotTest(
|
||||
`%%{init: { 'logLevel': 'debug', 'theme': 'forest' } }%%
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008 : Instagram
|
||||
2010 : Pinterest
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
231
cypress/platform/ashish2.html
Normal file
231
cypress/platform/ashish2.html
Normal file
@@ -0,0 +1,231 @@
|
||||
<html>
|
||||
<head>
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat&display=swap" rel="stylesheet" />
|
||||
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
|
||||
/>
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Noto+Sans+SC&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
body {
|
||||
/* background: rgb(221, 208, 208); */
|
||||
/* background:#333; */
|
||||
font-family: 'Arial';
|
||||
/* font-size: 18px !important; */
|
||||
}
|
||||
h1 {
|
||||
color: grey;
|
||||
}
|
||||
.mermaid2 {
|
||||
display: none;
|
||||
}
|
||||
.mermaid svg {
|
||||
/* font-size: 18px !important; */
|
||||
background-color: #eee;
|
||||
background-image: radial-gradient(#fff 1%, transparent 11%),
|
||||
radial-gradient(#fff 1%, transparent 11%);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0, 10px 10px;
|
||||
background-repeat: repeat;
|
||||
}
|
||||
.malware {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 150px;
|
||||
background: red;
|
||||
color: black;
|
||||
display: flex;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: monospace;
|
||||
font-size: 72px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>Security check</div>
|
||||
<pre id="diagram" class="mermaid2">
|
||||
timeline
|
||||
title My day
|
||||
section section with no tasks
|
||||
section Go to work at the dog office
|
||||
1930 : first step : second step is a long step
|
||||
: third step
|
||||
1940 : fourth step : fifth step
|
||||
section Go home
|
||||
1950 : India got independent and already won war against Pakistan
|
||||
1960 : India fights poverty, looses war to China and gets nuclear weapons from USA and USSR
|
||||
1970 : Green Revolution comes to india
|
||||
section Another section with no tasks
|
||||
I am a big big big tasks
|
||||
I am not so big tasks
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid">
|
||||
timeline
|
||||
title MermaidChart 2023 Timeline
|
||||
section 2023 Q1 <br> Release Personal Tier
|
||||
Buttet 1 : sub-point 1a : sub-point 1b
|
||||
: sub-point 1c
|
||||
Bullet 2 : sub-point 2a : sub-point 2b
|
||||
section 2023 Q2 <br> Release XYZ Tier
|
||||
Buttet 3 : sub-point <br> 3a : sub-point 3b
|
||||
: sub-point 3c
|
||||
Bullet 4 : sub-point 4a : sub-point 4b
|
||||
|
||||
</pre>
|
||||
|
||||
<pre id="diagram" class="mermaid">
|
||||
timeline
|
||||
title England's History Timeline
|
||||
section Stone Age
|
||||
7600 BC : Britain's oldest known house was built in Orkney, Scotland
|
||||
6000 BC : Sea levels rise and Britain becomes an island. The people who live here are hunter-gatherers.
|
||||
section Broze Age
|
||||
2300 BC : People arrive from Europe and settle in Britain. They bring farming and metalworking.
|
||||
: New styles of pottery and ways of burying the dead appear.
|
||||
2200 BC : The last major building works are completed at Stonehenge. People now bury their dead in stone circles.
|
||||
: The first metal objects are made in Britain.Some other nice things happen. it is a good time to be alive.
|
||||
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid2">
|
||||
%%{'init': { 'logLevel': 'debug', 'theme': 'default', 'timeline': {'disableMulticolor':false} } }%%
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google : Pixar
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008s : Instagram
|
||||
2010 : Pinterest
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid2">
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'themeVariables': {
|
||||
'cScale0': '#ff0000',
|
||||
'cScale1': '#00ff00',
|
||||
'cScale2': '#ff0000'
|
||||
} } }%%
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google : Pixar
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008s : Instagram
|
||||
2010 : Pinterest
|
||||
</pre>
|
||||
|
||||
<pre id="diagram" class="mermaid2">
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
|
||||
'cScale0': '#ff0000',
|
||||
'cScale1': '#00ff00',
|
||||
'cScale2': '#0000ff'
|
||||
} } }%%
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008 : Instagram
|
||||
2010 : Pinterest
|
||||
|
||||
</pre>
|
||||
|
||||
<pre id="diagram" class="mermaid2">
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008s : Instagram
|
||||
2010 : Pinterest
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid2">
|
||||
mindmap
|
||||
root
|
||||
child1((Circle))
|
||||
grandchild 1
|
||||
grandchild 2
|
||||
child2(Round rectangle)
|
||||
grandchild 3
|
||||
grandchild 4
|
||||
child3[Square]
|
||||
grandchild 5
|
||||
::icon(mdi mdi-fire)
|
||||
gc6((grand<br/>child 6))
|
||||
::icon(mdi mdi-fire)
|
||||
gc7((grand<br/>grand<br/>child 8))
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid2">
|
||||
flowchart-elk TB
|
||||
a --> b
|
||||
a --> c
|
||||
b --> d
|
||||
c --> d
|
||||
</pre>
|
||||
|
||||
<!-- <div id="cy"></div> -->
|
||||
<!-- <script src="http://localhost:9000/packages/mermaid-mindmap/dist/mermaid-mindmap-detector.js"></script> -->
|
||||
<!-- <script src="./mermaid-example-diagram-detector.js"></script> -->
|
||||
<!-- <script src="//cdn.jsdelivr.net/npm/mermaid@9.1.7/dist/mermaid.min.js"></script> -->
|
||||
<script type="module">
|
||||
//import mindmap from '../../packages/mermaid-mindmap/src/detector';
|
||||
// import example from '../../packages/mermaid-example-diagram/src/detector';
|
||||
// import timeline from '../../packages/mermaid-timeline/src/detector';
|
||||
import mermaid from '../../packages/mermaid/src/mermaid';
|
||||
// await mermaid.registerExternalDiagrams([]);
|
||||
mermaid.parseError = function (err, hash) {
|
||||
// console.error('Mermaid error: ', err);
|
||||
};
|
||||
mermaid.initialize({
|
||||
theme: 'base',
|
||||
startOnLoad: true,
|
||||
logLevel: 0,
|
||||
flowchart: {
|
||||
useMaxWidth: false,
|
||||
htmlLabels: true,
|
||||
},
|
||||
gantt: {
|
||||
useMaxWidth: false,
|
||||
},
|
||||
timeline: {
|
||||
disableMulticolor: false,
|
||||
htmlLabels: false,
|
||||
},
|
||||
useMaxWidth: true,
|
||||
lazyLoadedDiagrams: [
|
||||
// './mermaid-mindmap-detector.esm.mjs',
|
||||
// './mermaid-example-diagram-detector.esm.mjs',
|
||||
//'./mermaid-timeline-detector.esm.mjs',
|
||||
],
|
||||
});
|
||||
function callback() {
|
||||
alert('It worked');
|
||||
}
|
||||
mermaid.parseError = function (err, hash) {
|
||||
console.error('In parse error:');
|
||||
console.error(err);
|
||||
};
|
||||
// mermaid.test1('first_slow', 1200).then((r) => console.info(r));
|
||||
// mermaid.test1('second_fast', 200).then((r) => console.info(r));
|
||||
// mermaid.test1('third_fast', 200).then((r) => console.info(r));
|
||||
// mermaid.test1('forth_slow', 1200).then((r) => console.info(r));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -46,13 +46,9 @@
|
||||
<pre class="mermaid" style="width: 100%; height: 20%">
|
||||
%%{init: {'theme': 'base', 'fontFamily': 'courier', 'themeVariables': { 'primaryColor': '#fff000'}}}%%
|
||||
classDiagram-v2
|
||||
class BankAccount{
|
||||
+String owner
|
||||
+BigDecimal balance
|
||||
+deposit(amount) bool
|
||||
+withdrawl(amount) int
|
||||
}
|
||||
cssClass "BankAccount" customCss
|
||||
classA <|-- classB : implements
|
||||
classC *-- classD : composition
|
||||
classE o-- classF : aggregation
|
||||
</pre>
|
||||
<pre class="mermaid2" style="width: 100%; height: 20%">
|
||||
%%{init: {'theme': 'base', 'fontFamily': 'courier', 'themeVariables': { 'primaryColor': '#fff000'}}}%%
|
||||
|
||||
@@ -44,6 +44,9 @@ mindmap
|
||||
await mermaid.registerExternalDiagrams([mindmap]);
|
||||
await mermaid.initialize({ logLevel: 0 });
|
||||
await mermaid.initThrowsErrorsAsync();
|
||||
if (window.Cypress) {
|
||||
window.rendered = true;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
const diagram = document.getElementById('diagram');
|
||||
const svg = mermaid.render('diagram-svg', graph);
|
||||
diagram.innerHTML = svg;
|
||||
if (window.Cypress) {
|
||||
window.rendered = true;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
const diagram = document.getElementById('diagram');
|
||||
const svg = mermaid.render('diagram-svg', graph);
|
||||
diagram.innerHTML = svg;
|
||||
if (window.Cypress) {
|
||||
window.rendered = true;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -94,6 +94,9 @@
|
||||
// document.querySelector('#diagram').innerHTML = diagram;
|
||||
mermaid.render('diagram', diagram, (res) => {
|
||||
document.querySelector('#res').innerHTML = res;
|
||||
if (window.Cypress) {
|
||||
window.rendered = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<pre id="diagram" class="mermaid">
|
||||
<pre id="diagram" class="mermaid2">
|
||||
%%{init: {"flowchart": {"defaultRenderer": "elk"}} }%%
|
||||
graph TB
|
||||
a --> b
|
||||
@@ -62,7 +62,14 @@ graph TB
|
||||
b --> d
|
||||
c --> d
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid">
|
||||
<pre id="diagram" class="mermaid2">
|
||||
flowchart-elk TB
|
||||
a --> b
|
||||
a --> c
|
||||
b --> d
|
||||
c --> d
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid2">
|
||||
%%{init: {"flowchart": {"defaultRenderer": "elk"}} }%%
|
||||
flowchart TB
|
||||
%% I could not figure out how to use double quotes in labels in Mermaid
|
||||
@@ -118,7 +125,7 @@ flowchart TB
|
||||
</pre
|
||||
>
|
||||
<br />
|
||||
<pre id="diagram" class="mermaid">
|
||||
<pre id="diagram" class="mermaid2">
|
||||
flowchart TB
|
||||
%% I could not figure out how to use double quotes in labels in Mermaid
|
||||
subgraph ibm[IBM Espresso CPU]
|
||||
@@ -220,14 +227,24 @@ sequenceDiagram
|
||||
Customer->>+Merchant: Receives goods or services
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram" class="mermaid2">
|
||||
gantt
|
||||
title Style today marker (vertical line should be 5px wide and half-transparent blue)
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat %d
|
||||
todayMarker stroke-width:5px,stroke:#00f,opacity:0.5
|
||||
section Section1
|
||||
Today: 1, -1h
|
||||
<pre id="diagram" class="mermaid">
|
||||
mindmap
|
||||
root((mindmap))
|
||||
Origins
|
||||
Long history
|
||||
::icon(fa fa-book)
|
||||
Popularisation
|
||||
British popular psychology author Tony Buzan
|
||||
Research
|
||||
On effectiveness<br/>and features
|
||||
On Automatic creation
|
||||
Uses
|
||||
Creative techniques
|
||||
Strategic planning
|
||||
Argument mapping
|
||||
Tools
|
||||
Pen and paper
|
||||
Mermaid
|
||||
</pre>
|
||||
|
||||
<!-- <div id="cy"></div> -->
|
||||
@@ -238,15 +255,14 @@ sequenceDiagram
|
||||
|
||||
<script type="module">
|
||||
import mindmap from '../../packages/mermaid-mindmap/src/detector';
|
||||
import flowV3 from '../../packages/mermaid-flowchart-v3/src/detector';
|
||||
// import example from '../../packages/mermaid-example-diagram/src/detector';
|
||||
import mermaid from '../../packages/mermaid/src/mermaid';
|
||||
await mermaid.registerExternalDiagrams([mindmap, flowV3]);
|
||||
await mermaid.registerExternalDiagrams([mindmap]);
|
||||
mermaid.parseError = function (err, hash) {
|
||||
// console.error('Mermaid error: ', err);
|
||||
};
|
||||
mermaid.initialize({
|
||||
// theme: 'forest',
|
||||
theme: 'dark',
|
||||
startOnLoad: true,
|
||||
logLevel: 0,
|
||||
flowchart: {
|
||||
|
||||
@@ -1,14 +1,283 @@
|
||||
<html>
|
||||
<head>
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat&display=swap" rel="stylesheet" />
|
||||
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
|
||||
/>
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Noto+Sans+SC&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
body {
|
||||
/* background: rgb(221, 208, 208); */
|
||||
/* background:#333; */
|
||||
font-family: 'Arial';
|
||||
/* font-size: 18px !important; */
|
||||
}
|
||||
h1 {
|
||||
color: grey;
|
||||
}
|
||||
.mermaid2 {
|
||||
display: none;
|
||||
}
|
||||
.mermaid svg {
|
||||
/* font-size: 18px !important; */
|
||||
background-color: #eee;
|
||||
background-image: radial-gradient(#fff 1%, transparent 11%),
|
||||
radial-gradient(#fff 1%, transparent 11%);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0, 10px 10px;
|
||||
background-repeat: repeat;
|
||||
}
|
||||
.malware {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 150px;
|
||||
background: red;
|
||||
color: black;
|
||||
display: flex;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: monospace;
|
||||
font-size: 72px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<pre class="mermaid">
|
||||
none
|
||||
hello world
|
||||
<pre id="diagram" class="mermaid">
|
||||
%%{init: {"flowchart": {"defaultRenderer": "elk"}} }%%
|
||||
graph TB
|
||||
a --> b
|
||||
a --> c
|
||||
b --> d
|
||||
c --> d
|
||||
</pre>
|
||||
<script src="./mermaid.js"></script>
|
||||
<script>
|
||||
<pre id="diagram" class="mermaid">
|
||||
flowchart-elk LR
|
||||
subgraph A
|
||||
a -->b
|
||||
end
|
||||
subgraph B
|
||||
b
|
||||
end
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid">
|
||||
%%{init: {"flowchart": {"defaultRenderer": "elk"}} }%%
|
||||
flowchart TB
|
||||
%% I could not figure out how to use double quotes in labels in Mermaid
|
||||
subgraph ibm[IBM Espresso CPU]
|
||||
core0[IBM PowerPC Broadway Core 0]
|
||||
core1[IBM PowerPC Broadway Core 1]
|
||||
core2[IBM PowerPC Broadway Core 2]
|
||||
|
||||
rom[16 KB ROM]
|
||||
|
||||
core0 --- core2
|
||||
|
||||
rom --> core2
|
||||
end
|
||||
|
||||
subgraph amd[AMD Latte GPU]
|
||||
mem[Memory & I/O Bridge]
|
||||
dram[DRAM Controller]
|
||||
edram[32 MB EDRAM MEM1]
|
||||
rom[512 B SEEPROM]
|
||||
|
||||
sata[SATA IF]
|
||||
exi[EXI]
|
||||
|
||||
subgraph gx[GX]
|
||||
sram[3 MB 1T-SRAM]
|
||||
end
|
||||
|
||||
radeon[AMD Radeon R7xx GX2]
|
||||
|
||||
mem --- gx
|
||||
mem --- radeon
|
||||
|
||||
rom --- mem
|
||||
|
||||
mem --- sata
|
||||
mem --- exi
|
||||
|
||||
dram --- sata
|
||||
dram --- exi
|
||||
end
|
||||
|
||||
ddr3[2 GB DDR3 RAM MEM2]
|
||||
|
||||
mem --- ddr3
|
||||
dram --- ddr3
|
||||
edram --- ddr3
|
||||
|
||||
core1 --- mem
|
||||
|
||||
exi --- rtc
|
||||
rtc{{rtc}}
|
||||
</pre
|
||||
>
|
||||
<br />
|
||||
<pre id="diagram" class="mermaid">
|
||||
flowchart TB
|
||||
%% I could not figure out how to use double quotes in labels in Mermaid
|
||||
subgraph ibm[IBM Espresso CPU]
|
||||
core0[IBM PowerPC Broadway Core 0]
|
||||
core1[IBM PowerPC Broadway Core 1]
|
||||
core2[IBM PowerPC Broadway Core 2]
|
||||
|
||||
rom[16 KB ROM]
|
||||
|
||||
core0 --- core2
|
||||
|
||||
rom --> core2
|
||||
end
|
||||
|
||||
subgraph amd[AMD Latte GPU]
|
||||
mem[Memory & I/O Bridge]
|
||||
dram[DRAM Controller]
|
||||
edram[32 MB EDRAM MEM1]
|
||||
rom[512 B SEEPROM]
|
||||
|
||||
sata[SATA IF]
|
||||
exi[EXI]
|
||||
|
||||
subgraph gx[GX]
|
||||
sram[3 MB 1T-SRAM]
|
||||
end
|
||||
|
||||
radeon[AMD Radeon R7xx GX2]
|
||||
|
||||
mem --- gx
|
||||
mem --- radeon
|
||||
|
||||
rom --- mem
|
||||
|
||||
mem --- sata
|
||||
mem --- exi
|
||||
|
||||
dram --- sata
|
||||
dram --- exi
|
||||
end
|
||||
|
||||
ddr3[2 GB DDR3 RAM MEM2]
|
||||
|
||||
mem --- ddr3
|
||||
dram --- ddr3
|
||||
edram --- ddr3
|
||||
|
||||
core1 --- mem
|
||||
|
||||
exi --- rtc
|
||||
rtc{{rtc}}
|
||||
</pre
|
||||
>
|
||||
<br />
|
||||
|
||||
<pre id="diagram" class="mermaid2">
|
||||
flowchart LR
|
||||
B1 --be be--x B2
|
||||
B1 --bo bo--o B3
|
||||
subgraph Ugge
|
||||
B2
|
||||
B3
|
||||
subgraph inner
|
||||
B4
|
||||
B5
|
||||
end
|
||||
subgraph inner2
|
||||
subgraph deeper
|
||||
C4
|
||||
C5
|
||||
end
|
||||
C6
|
||||
end
|
||||
|
||||
B4 --> C4
|
||||
|
||||
B3 -- X --> B4
|
||||
B2 --> inner
|
||||
|
||||
C4 --> C5
|
||||
end
|
||||
|
||||
subgraph outer
|
||||
B6
|
||||
end
|
||||
B6 --> B5
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram" class="mermaid2">
|
||||
sequenceDiagram
|
||||
Customer->>+Stripe: Makes a payment request
|
||||
Stripe->>+Bank: Forwards the payment request to the bank
|
||||
Bank->>+Customer: Asks for authorization
|
||||
Customer->>+Bank: Provides authorization
|
||||
Bank->>+Stripe: Sends a response with payment details
|
||||
Stripe->>+Merchant: Sends a notification of payment receipt
|
||||
Merchant->>+Stripe: Confirms the payment
|
||||
Stripe->>+Customer: Sends a confirmation of payment
|
||||
Customer->>+Merchant: Receives goods or services
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram" class="mermaid2">
|
||||
gantt
|
||||
title Style today marker (vertical line should be 5px wide and half-transparent blue)
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat %d
|
||||
todayMarker stroke-width:5px,stroke:#00f,opacity:0.5
|
||||
section Section1
|
||||
Today: 1, -1h
|
||||
</pre>
|
||||
|
||||
<!-- <div id="cy"></div> -->
|
||||
<!-- <script src="http://localhost:9000/packages/mermaid-mindmap/dist/mermaid-mindmap-detector.js"></script> -->
|
||||
<!-- <script src="./mermaid-example-diagram-detector.js"></script> -->
|
||||
<!-- <script src="//cdn.jsdelivr.net/npm/mermaid@9.1.7/dist/mermaid.min.js"></script> -->
|
||||
<!-- <script src="./mermaid.js"></script> -->
|
||||
|
||||
<script type="module">
|
||||
import mindmap from '../../packages/mermaid-mindmap/src/detector';
|
||||
// import example from '../../packages/mermaid-example-diagram/src/detector';
|
||||
import mermaid from '../../packages/mermaid/src/mermaid';
|
||||
await mermaid.registerExternalDiagrams([mindmap]);
|
||||
mermaid.parseError = function (err, hash) {
|
||||
// console.error('Mermaid error: ', err);
|
||||
};
|
||||
mermaid.initialize({
|
||||
logLevel: 1,
|
||||
// theme: 'forest',
|
||||
startOnLoad: true,
|
||||
logLevel: 0,
|
||||
flowchart: {
|
||||
// defaultRenderer: 'elk',
|
||||
useMaxWidth: false,
|
||||
htmlLabels: true,
|
||||
},
|
||||
gantt: {
|
||||
useMaxWidth: false,
|
||||
},
|
||||
useMaxWidth: false,
|
||||
});
|
||||
function callback() {
|
||||
alert('It worked');
|
||||
}
|
||||
mermaid.parseError = function (err, hash) {
|
||||
console.error('In parse error:');
|
||||
console.error(err);
|
||||
};
|
||||
// mermaid.test1('first_slow', 1200).then((r) => console.info(r));
|
||||
// mermaid.test1('second_fast', 200).then((r) => console.info(r));
|
||||
// mermaid.test1('third_fast', 200).then((r) => console.info(r));
|
||||
// mermaid.test1('forth_slow', 1200).then((r) => console.info(r));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,6 +5,13 @@ function b64ToUtf8(str) {
|
||||
return decodeURIComponent(escape(window.atob(str)));
|
||||
}
|
||||
|
||||
// Adds a rendered flag to window when rendering is done, so cypress can wait for it.
|
||||
function markRendered() {
|
||||
if (window.Cypress) {
|
||||
window.rendered = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ##contentLoaded Callback function that is called when page is loaded. This functions fetches
|
||||
* configuration for mermaid rendering and calls init for rendering the mermaid diagrams on the
|
||||
@@ -39,7 +46,8 @@ const contentLoaded = async function () {
|
||||
|
||||
await mermaid2.registerExternalDiagrams([mindmap]);
|
||||
mermaid2.initialize(graphObj.mermaid);
|
||||
mermaid2.init();
|
||||
await mermaid2.init();
|
||||
markRendered();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -128,6 +136,7 @@ const contentLoadedApi = function () {
|
||||
);
|
||||
}
|
||||
}
|
||||
markRendered();
|
||||
};
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
@@ -142,7 +151,7 @@ if (typeof document !== 'undefined') {
|
||||
contentLoadedApi();
|
||||
} else {
|
||||
this.console.log('Not using api');
|
||||
contentLoaded();
|
||||
void contentLoaded();
|
||||
}
|
||||
},
|
||||
false
|
||||
|
||||
@@ -71,6 +71,44 @@
|
||||
</pre>
|
||||
<hr />
|
||||
|
||||
<pre class="mermaid">
|
||||
erDiagram
|
||||
"HOSPITAL" {
|
||||
int id PK
|
||||
int doctor_id PK, FK
|
||||
string address UK
|
||||
string name
|
||||
string phone_number
|
||||
string fax_number
|
||||
}
|
||||
</pre
|
||||
>
|
||||
<hr />
|
||||
|
||||
<pre class="mermaid">
|
||||
erDiagram
|
||||
CAR ||--o{ NAMED-DRIVER : allows
|
||||
CAR {
|
||||
string registrationNumber PK
|
||||
string make
|
||||
string model
|
||||
string[] parts
|
||||
}
|
||||
PERSON ||--o{ NAMED-DRIVER : is
|
||||
PERSON {
|
||||
string driversLicense PK "The license #"
|
||||
string(99) firstName "Only 99 characters are allowed"
|
||||
string lastName
|
||||
string phone UK
|
||||
int age
|
||||
}
|
||||
NAMED-DRIVER {
|
||||
string carRegistrationNumber PK, FK
|
||||
string driverLicence PK,FK
|
||||
}
|
||||
MANUFACTURER only one to zero or more CAR : makes
|
||||
</pre>
|
||||
|
||||
<script src="./mermaid.js"></script>
|
||||
<script type="module">
|
||||
mermaid.initialize({
|
||||
|
||||
@@ -128,6 +128,22 @@
|
||||
</pre>
|
||||
<hr />
|
||||
|
||||
<pre class="mermaid">
|
||||
sequenceDiagram
|
||||
box lightgreen Alice & John
|
||||
participant A
|
||||
participant J
|
||||
end
|
||||
box Another Group very very long description not wrapped
|
||||
participant B
|
||||
end
|
||||
A->>J: Hello John, how are you?
|
||||
J->>A: Great!
|
||||
A->>B: Hello Bob, how are you ?
|
||||
</pre
|
||||
>
|
||||
<hr />
|
||||
|
||||
<script src="./mermaid.js"></script>
|
||||
<script>
|
||||
mermaid.initialize({
|
||||
|
||||
@@ -161,12 +161,19 @@
|
||||
First --> Second
|
||||
First --> Third
|
||||
|
||||
state First {
|
||||
state "the first composite" as First {
|
||||
[*] --> 1st
|
||||
1st --> [*]
|
||||
state innerFirst {
|
||||
state "1 in innerFirst" as 1st1st
|
||||
1st2nd: 2 in innerFirst
|
||||
[*] --> 1st1st
|
||||
1st1st --> 1st2nd
|
||||
%% 1st2nd --> 1st
|
||||
}
|
||||
1st --> innerFirst
|
||||
innerFirst --> 2nd
|
||||
}
|
||||
state Second {
|
||||
[*] --> 2nd
|
||||
2nd --> [*]
|
||||
}
|
||||
state Third {
|
||||
|
||||
38
demos/timeline.html
Normal file
38
demos/timeline.html
Normal file
@@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>Mermaid Quick Test Page</title>
|
||||
<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>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<pre class="mermaid">
|
||||
timeline
|
||||
title My day
|
||||
section Go to work
|
||||
1930 : first step : second step
|
||||
: third step
|
||||
1940 : fourth step : fifth step
|
||||
</pre>
|
||||
|
||||
<script src="./mermaid.js"></script>
|
||||
<script>
|
||||
mermaid.initialize({
|
||||
theme: 'forest',
|
||||
logLevel: 1,
|
||||
securityLevel: 'loose',
|
||||
flowchart: { curve: 'basis' },
|
||||
gantt: { axisFormat: '%m/%d/%Y' },
|
||||
sequence: { actorMargin: 50 },
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,7 +2,7 @@
|
||||
>
|
||||
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||
>
|
||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/misc/faq.md](../../packages/mermaid/src/docs/misc/faq.md).
|
||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/faq.md](../../packages/mermaid/src/docs/config/faq.md).
|
||||
|
||||
# Frequently Asked Questions
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
#### Defined in
|
||||
|
||||
[defaultConfig.ts:1934](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L1934)
|
||||
[defaultConfig.ts:2084](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L2084)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ Renames and re-exports [mermaidAPI](mermaidAPI.md#mermaidapi)
|
||||
|
||||
#### Defined in
|
||||
|
||||
[mermaidAPI.ts:73](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L73)
|
||||
[mermaidAPI.ts:74](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L74)
|
||||
|
||||
## Variables
|
||||
|
||||
@@ -90,7 +90,7 @@ mermaid.initialize(config);
|
||||
|
||||
#### Defined in
|
||||
|
||||
[mermaidAPI.ts:962](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L962)
|
||||
[mermaidAPI.ts:886](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L886)
|
||||
|
||||
## Functions
|
||||
|
||||
@@ -121,7 +121,7 @@ Return the last node appended
|
||||
|
||||
#### 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)
|
||||
|
||||
---
|
||||
|
||||
@@ -147,7 +147,7 @@ the cleaned up svgCode
|
||||
|
||||
#### Defined in
|
||||
|
||||
[mermaidAPI.ts:237](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L237)
|
||||
[mermaidAPI.ts:238](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L238)
|
||||
|
||||
---
|
||||
|
||||
@@ -173,7 +173,7 @@ the string with all the user styles
|
||||
|
||||
#### Defined in
|
||||
|
||||
[mermaidAPI.ts:166](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L166)
|
||||
[mermaidAPI.ts:167](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L167)
|
||||
|
||||
---
|
||||
|
||||
@@ -196,7 +196,7 @@ the string with all the user styles
|
||||
|
||||
#### Defined in
|
||||
|
||||
[mermaidAPI.ts:214](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L214)
|
||||
[mermaidAPI.ts:215](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L215)
|
||||
|
||||
---
|
||||
|
||||
@@ -223,7 +223,7 @@ with an enclosing block that has each of the cssClasses followed by !important;
|
||||
|
||||
#### Defined in
|
||||
|
||||
[mermaidAPI.ts:150](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L150)
|
||||
[mermaidAPI.ts:151](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L151)
|
||||
|
||||
---
|
||||
|
||||
@@ -243,7 +243,7 @@ with an enclosing block that has each of the cssClasses followed by !important;
|
||||
|
||||
#### Defined in
|
||||
|
||||
[mermaidAPI.ts:130](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L130)
|
||||
[mermaidAPI.ts:131](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L131)
|
||||
|
||||
---
|
||||
|
||||
@@ -263,7 +263,7 @@ with an enclosing block that has each of the cssClasses followed by !important;
|
||||
|
||||
#### Defined in
|
||||
|
||||
[mermaidAPI.ts:101](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L101)
|
||||
[mermaidAPI.ts:102](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L102)
|
||||
|
||||
---
|
||||
|
||||
@@ -289,7 +289,7 @@ Put the svgCode into an iFrame. Return the iFrame code
|
||||
|
||||
#### Defined in
|
||||
|
||||
[mermaidAPI.ts:265](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L265)
|
||||
[mermaidAPI.ts:266](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L266)
|
||||
|
||||
---
|
||||
|
||||
@@ -314,4 +314,4 @@ Remove any existing elements from the given document
|
||||
|
||||
#### Defined in
|
||||
|
||||
[mermaidAPI.ts:336](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L336)
|
||||
[mermaidAPI.ts:337](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L337)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
>
|
||||
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||
>
|
||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/misc/integrations.md](../../packages/mermaid/src/docs/misc/integrations.md).
|
||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/ecosystem/integrations.md](../../packages/mermaid/src/docs/ecosystem/integrations.md).
|
||||
|
||||
# Integrations
|
||||
|
||||
@@ -95,9 +95,10 @@ They also serve as proof of concept, for the variety of things that can be built
|
||||
|
||||
## Editor Plugins
|
||||
|
||||
- [Vs Code](https://code.visualstudio.com/)
|
||||
- [VS Code](https://code.visualstudio.com/)
|
||||
- [Markdown Preview Mermaid Support](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-mermaid)
|
||||
- [Mermaid Preview](https://marketplace.visualstudio.com/items?itemName=vstirbu.vscode-mermaid-preview)
|
||||
- [Markdown Preview Enhanced](https://marketplace.visualstudio.com/items?itemName=shd101wyy.markdown-preview-enhanced)
|
||||
- [Mermaid Markdown Syntax Highlighting](https://marketplace.visualstudio.com/items?itemName=bpruitt-goddard.mermaid-markdown-syntax-highlighting)
|
||||
- [Mermaid Editor](https://marketplace.visualstudio.com/items?itemName=tomoyukim.vscode-mermaid-editor)
|
||||
- [Mermaid Export](https://marketplace.visualstudio.com/items?itemName=Gruntfuggly.mermaid-export)
|
||||
9
docs/ecosystem/showcases.md
Normal file
9
docs/ecosystem/showcases.md
Normal file
@@ -0,0 +1,9 @@
|
||||
> **Warning**
|
||||
>
|
||||
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||
>
|
||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/ecosystem/showcases.md](../../packages/mermaid/src/docs/ecosystem/showcases.md).
|
||||
|
||||
# Showcases
|
||||
|
||||
- [Swimm - Up-to-date diagrams with Swimm, the knowledge management tool for code](https://docs.swimm.io/Features/diagrams-and-charts).
|
||||
@@ -200,14 +200,13 @@ The `type` and `name` values must begin with an alphabetic character and may con
|
||||
|
||||
#### Attribute Keys and Comments
|
||||
|
||||
Attributes may also have a `key` or comment defined. Keys can be "PK", "FK" or "UK", for Primary Key, Foreign Key or Unique Key. And a `comment` is defined by double quotes at the end of an attribute. Comments themselves cannot have double-quote characters in them.
|
||||
Attributes may also have a `key` or comment defined. Keys can be `PK`, `FK` or `UK`, for Primary Key, Foreign Key or Unique Key. To specify multiple key constraints on a single attribute, separate them with a comma (e.g., `PK, FK`).. A `comment` is defined by double quotes at the end of an attribute. Comments themselves cannot have double-quote characters in them.
|
||||
|
||||
```mermaid-example
|
||||
erDiagram
|
||||
CAR ||--o{ NAMED-DRIVER : allows
|
||||
CAR {
|
||||
string allowedDriver FK "The license of the allowed driver"
|
||||
string registrationNumber UK
|
||||
string registrationNumber PK
|
||||
string make
|
||||
string model
|
||||
string[] parts
|
||||
@@ -217,17 +216,21 @@ erDiagram
|
||||
string driversLicense PK "The license #"
|
||||
string(99) firstName "Only 99 characters are allowed"
|
||||
string lastName
|
||||
string phone UK
|
||||
int age
|
||||
}
|
||||
MANUFACTURER only one to zero or more CAR
|
||||
NAMED-DRIVER {
|
||||
string carRegistrationNumber PK, FK
|
||||
string driverLicence PK, FK
|
||||
}
|
||||
MANUFACTURER only one to zero or more CAR : makes
|
||||
```
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
CAR ||--o{ NAMED-DRIVER : allows
|
||||
CAR {
|
||||
string allowedDriver FK "The license of the allowed driver"
|
||||
string registrationNumber UK
|
||||
string registrationNumber PK
|
||||
string make
|
||||
string model
|
||||
string[] parts
|
||||
@@ -237,9 +240,14 @@ erDiagram
|
||||
string driversLicense PK "The license #"
|
||||
string(99) firstName "Only 99 characters are allowed"
|
||||
string lastName
|
||||
string phone UK
|
||||
int age
|
||||
}
|
||||
MANUFACTURER only one to zero or more CAR
|
||||
NAMED-DRIVER {
|
||||
string carRegistrationNumber PK, FK
|
||||
string driverLicence PK, FK
|
||||
}
|
||||
MANUFACTURER only one to zero or more CAR : makes
|
||||
```
|
||||
|
||||
### Other Things
|
||||
|
||||
@@ -842,8 +842,8 @@ In the example below the style defined in the linkStyle statement will belong to
|
||||
### Styling line curves
|
||||
|
||||
It is possible to style the type of curve used for lines between items, if the default method does not meet your needs.
|
||||
Available curve styles include `basis`, `bump`, `linear`, `monotoneX`, `monotoneY`, `natural`, `step`, `stepAfter`,
|
||||
and `stepBefore`.
|
||||
Available curve styles include `basis`, `bumpX`, `bumpY`, `cardinal`, `catmullRom`, `linear`, `monotoneX`, `monotoneY`,
|
||||
`natural`, `step`, `stepAfter`, and `stepBefore`.
|
||||
|
||||
In this example, a left-to-right graph uses the `stepBefore` curve style:
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ In this way we can use a text outline to generate a hierarchical mindmap.
|
||||
|
||||
## Different shapes
|
||||
|
||||
Mermaids mindmaps can show node using different shapes. When specifying a shape for a node the syntax for the is similar to flowchart nodes, with an id followed by the shape definition and with the text within the shape delimiters. Where possible we try/will try to keep the same shapes as for flowcharts even though they are not all supported from the start.
|
||||
Mermaid mindmaps can show nodes using different shapes. When specifying a shape for a node the syntax is similar to flowchart nodes, with an id followed by the shape definition and with the text within the shape delimiters. Where possible we try/will try to keep the same shapes as for flowcharts, even though they are not all supported from the start.
|
||||
|
||||
Mindmap can show the following shapes:
|
||||
|
||||
|
||||
@@ -94,6 +94,59 @@ sequenceDiagram
|
||||
J->>A: Great!
|
||||
```
|
||||
|
||||
### Grouping / Box
|
||||
|
||||
The actor(s) can be grouped in vertical boxes. You can define a color (if not, it will be transparent) and/or a descriptive label using the following notation:
|
||||
|
||||
box Aqua Group Description
|
||||
... actors ...
|
||||
end
|
||||
box Group without description
|
||||
... actors ...
|
||||
end
|
||||
box rgb(33,66,99)
|
||||
... actors ...
|
||||
end
|
||||
|
||||
> **Note**
|
||||
> If your group name is a color you can force the color to be transparent:
|
||||
|
||||
box transparent Aqua
|
||||
... actors ...
|
||||
end
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
box Purple Alice & John
|
||||
participant A
|
||||
participant J
|
||||
end
|
||||
box Another Group
|
||||
participant B
|
||||
participant C
|
||||
end
|
||||
A->>J: Hello John, how are you?
|
||||
J->>A: Great!
|
||||
A->>B: Hello Bob, how is Charly ?
|
||||
B->>C: Hello Charly, how are you?
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
box Purple Alice & John
|
||||
participant A
|
||||
participant J
|
||||
end
|
||||
box Another Group
|
||||
participant B
|
||||
participant C
|
||||
end
|
||||
A->>J: Hello John, how are you?
|
||||
J->>A: Great!
|
||||
A->>B: Hello Bob, how is Charly ?
|
||||
B->>C: Hello Charly, how are you?
|
||||
```
|
||||
|
||||
## Messages
|
||||
|
||||
Messages can be of two displayed either solid or with a dotted line.
|
||||
|
||||
462
docs/syntax/timeline.md
Normal file
462
docs/syntax/timeline.md
Normal file
@@ -0,0 +1,462 @@
|
||||
> **Warning**
|
||||
>
|
||||
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||
>
|
||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/syntax/timeline.md](../../packages/mermaid/src/docs/syntax/timeline.md).
|
||||
|
||||
# Timeline Diagram
|
||||
|
||||
> Timeline: This is an experimental diagram for now. The syntax and properties can change in future releases. The syntax is stable except for the icon integration which is the experimental part.
|
||||
|
||||
"A timeline is a type of diagram used to illustrate a chronology of events, dates, or periods of time. It is usually presented graphically to indicate the passing of time, and it is usually organized chronologically. A basic timeline presents a list of events in chronological order, usually using dates as markers. A timeline can also be used to show the relationship between events, such as the relationship between the events of a person's life. A timeline can also be used to show the relationship between events, such as the relationship between the events of a person's life." Wikipedia
|
||||
|
||||
### An example of a timeline.
|
||||
|
||||
```mermaid-example
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook
|
||||
: Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
```
|
||||
|
||||
```mermaid
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook
|
||||
: Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
```
|
||||
|
||||
## Syntax
|
||||
|
||||
The syntax for creating Timeline diagram is simple. You always start with the `timeline` keyword to let mermaid know that you want to create a timeline diagram.
|
||||
|
||||
After that there is a possibility to add a title to the timeline. This is done by adding a line with the keyword `title` followed by the title text.
|
||||
|
||||
Then you add the timeline data, where you always start with a time period, followed by a colon and then the text for the event. Optionally you can add a second colon and then the text for the event. So, you can have one or more events per time period.
|
||||
|
||||
```json
|
||||
{time period} : {event}
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```json
|
||||
{time period} : {event} : {event}
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```json
|
||||
{time period} : {event}
|
||||
: {event}
|
||||
: {event}
|
||||
```
|
||||
|
||||
NOTE: Both time period and event are simple text, and not limited to numbers.
|
||||
|
||||
Let us look at the syntax for the example above.
|
||||
|
||||
```mermaid-example
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
```
|
||||
|
||||
```mermaid
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
```
|
||||
|
||||
In this way we can use a text outline to generate a timeline diagram.
|
||||
The sequence of time period and events is important, as it will be used to draw the timeline. The first time period will be placed at the left side of the timeline, and the last time period will be placed at the right side of the timeline.
|
||||
|
||||
Similarly, the first event will be placed at the top for that specific time period, and the last event will be placed at the bottom.
|
||||
|
||||
## Grouping of time periods in sections/ages
|
||||
|
||||
You can group time periods in sections/ages. This is done by adding a line with the keyword `section` followed by the section name.
|
||||
|
||||
All subsequent time periods will be placed in this section until a new section is defined.
|
||||
|
||||
If no section is defined, all time periods will be placed in the default section.
|
||||
|
||||
Let us look at an example, where we have grouped the time periods in sections.
|
||||
|
||||
```mermaid-example
|
||||
timeline
|
||||
title Timeline of Industrial Revolution
|
||||
section 17th-20th century
|
||||
Industry 1.0 : Machinery, Water power, Steam <br>power
|
||||
Industry 2.0 : Electricity, Internal combustion engine, Mass production
|
||||
Industry 3.0 : Electronics, Computers, Automation
|
||||
section 21st century
|
||||
Industry 4.0 : Internet, Robotics, Internet of Things
|
||||
Industry 5.0 : Artificial intelligence, Big data,3D printing
|
||||
```
|
||||
|
||||
```mermaid
|
||||
timeline
|
||||
title Timeline of Industrial Revolution
|
||||
section 17th-20th century
|
||||
Industry 1.0 : Machinery, Water power, Steam <br>power
|
||||
Industry 2.0 : Electricity, Internal combustion engine, Mass production
|
||||
Industry 3.0 : Electronics, Computers, Automation
|
||||
section 21st century
|
||||
Industry 4.0 : Internet, Robotics, Internet of Things
|
||||
Industry 5.0 : Artificial intelligence, Big data,3D printing
|
||||
```
|
||||
|
||||
As you can see, the time periods are placed in the sections, and the sections are placed in the order they are defined.
|
||||
|
||||
All time periods and events under a given section follow a similar color scheme. This is done to make it easier to see the relationship between time periods and events.
|
||||
|
||||
## Wrapping of text for long time-periods or events
|
||||
|
||||
By default, the text for time-periods and events will be wrapped if it is too long. This is done to avoid that the text is drawn outside the diagram.
|
||||
|
||||
You can also use `<br>` to force a line break.
|
||||
|
||||
Let us look at another example, where we have a long time period, and a long event.
|
||||
|
||||
```mermaid-example
|
||||
timeline
|
||||
title England's History Timeline
|
||||
section Stone Age
|
||||
7600 BC : Britain's oldest known house was built in Orkney, Scotland
|
||||
6000 BC : Sea levels rise and Britain becomes an island.<br> The people who live here are hunter-gatherers.
|
||||
section Broze Age
|
||||
2300 BC : People arrive from Europe and settle in Britain. <br>They bring farming and metalworking.
|
||||
: New styles of pottery and ways of burying the dead appear.
|
||||
2200 BC : The last major building works are completed at Stonehenge.<br> People now bury their dead in stone circles.
|
||||
: The first metal objects are made in Britain.Some other nice things happen. it is a good time to be alive.
|
||||
|
||||
```
|
||||
|
||||
```mermaid
|
||||
timeline
|
||||
title England's History Timeline
|
||||
section Stone Age
|
||||
7600 BC : Britain's oldest known house was built in Orkney, Scotland
|
||||
6000 BC : Sea levels rise and Britain becomes an island.<br> The people who live here are hunter-gatherers.
|
||||
section Broze Age
|
||||
2300 BC : People arrive from Europe and settle in Britain. <br>They bring farming and metalworking.
|
||||
: New styles of pottery and ways of burying the dead appear.
|
||||
2200 BC : The last major building works are completed at Stonehenge.<br> People now bury their dead in stone circles.
|
||||
: The first metal objects are made in Britain.Some other nice things happen. it is a good time to be alive.
|
||||
|
||||
```
|
||||
|
||||
```mermaid-example
|
||||
timeline
|
||||
title MermaidChart 2023 Timeline
|
||||
section 2023 Q1 <br> Release Personal Tier
|
||||
Buttet 1 : sub-point 1a : sub-point 1b
|
||||
: sub-point 1c
|
||||
Bullet 2 : sub-point 2a : sub-point 2b
|
||||
section 2023 Q2 <br> Release XYZ Tier
|
||||
Buttet 3 : sub-point <br> 3a : sub-point 3b
|
||||
: sub-point 3c
|
||||
Bullet 4 : sub-point 4a : sub-point 4b
|
||||
```
|
||||
|
||||
```mermaid
|
||||
timeline
|
||||
title MermaidChart 2023 Timeline
|
||||
section 2023 Q1 <br> Release Personal Tier
|
||||
Buttet 1 : sub-point 1a : sub-point 1b
|
||||
: sub-point 1c
|
||||
Bullet 2 : sub-point 2a : sub-point 2b
|
||||
section 2023 Q2 <br> Release XYZ Tier
|
||||
Buttet 3 : sub-point <br> 3a : sub-point 3b
|
||||
: sub-point 3c
|
||||
Bullet 4 : sub-point 4a : sub-point 4b
|
||||
```
|
||||
|
||||
## Styling of time periods and events
|
||||
|
||||
As explained earlier, each section has a color scheme, and each time period and event under a section follow the similar color scheme.
|
||||
|
||||
However, if there is no section defined, then we have two possibilities:
|
||||
|
||||
1. Style time periods individually, i.e. each time period(and its coressponding events) will have its own color scheme. This is the DEFAULT behavior.
|
||||
|
||||
```mermaid-example
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
|
||||
```
|
||||
|
||||
```mermaid
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
|
||||
```
|
||||
|
||||
Note that this is no, section defined, and each time period and its corresponding events will have its own color scheme.
|
||||
|
||||
2. Disable the multiColor option using the `disableMultiColor` option. This will make all time periods and events follow the same color scheme.
|
||||
|
||||
You will need to add this option either via mermaid.intialize function or directives.
|
||||
|
||||
```javascript
|
||||
mermaid.initialize({
|
||||
theme: 'base',
|
||||
startOnLoad: true,
|
||||
logLevel: 0,
|
||||
timeline: {
|
||||
disableMulticolor: false,
|
||||
},
|
||||
...
|
||||
...
|
||||
```
|
||||
|
||||
let us look at same example, where we have disabled the multiColor option.
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'timeline': {'disableMulticolor': true}}}%%
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'timeline': {'disableMulticolor': true}}}%%
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
|
||||
```
|
||||
|
||||
### Customizing Color scheme
|
||||
|
||||
You can customize the color scheme using the `cScale0` to `cScale11` theme variables. Mermaid allows you to set unique colors for up-to 12, where `cScale0` variable will drive the value of the first section or time-period, `cScale1` will drive the value of the second section and so on.
|
||||
In case you have more than 12 sections, the color scheme will start to repeat.
|
||||
|
||||
NOTE: Default values for these theme variables are picked from the selected theme. If you want to override the default values, you can use the `initialize` call to add your custom theme variable values.
|
||||
|
||||
Example:
|
||||
|
||||
Now let's override the default values for the `cScale0` to `cScale2` variables:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
|
||||
'cScale0': '#ff0000',
|
||||
'cScale1': '#00ff00',
|
||||
'cScale2': '#0000ff'
|
||||
} } }%%
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008 : Instagram
|
||||
2010 : Pinterest
|
||||
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
|
||||
'cScale0': '#ff0000',
|
||||
'cScale1': '#00ff00',
|
||||
'cScale2': '#0000ff'
|
||||
} } }%%
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008 : Instagram
|
||||
2010 : Pinterest
|
||||
|
||||
```
|
||||
|
||||
See how the colors are changed to the values specified in the theme variables.
|
||||
|
||||
## Themes
|
||||
|
||||
Mermaid supports a bunch of pre-defined themes which you can use to find the right one for you. PS: you can actually override an existing theme's variable to get your own custom theme going. Learn more about theming your diagram [here](../config/theming.md).
|
||||
|
||||
The following are the different pre-defined theme options:
|
||||
|
||||
- `base`
|
||||
- `forest`
|
||||
- `dark`
|
||||
- `default`
|
||||
- `neutral`
|
||||
|
||||
**NOTE**: To change theme you can either use the `initialize` call or _directives_. Learn more about [directives](../config/directives.md)
|
||||
Let's put them to use, and see how our sample diagram looks in different themes:
|
||||
|
||||
### Base Theme
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base' } }%%
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008 : Instagram
|
||||
2010 : Pinterest
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base' } }%%
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008 : Instagram
|
||||
2010 : Pinterest
|
||||
```
|
||||
|
||||
### Forest Theme
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'forest' } }%%
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008 : Instagram
|
||||
2010 : Pinterest
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'forest' } }%%
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008 : Instagram
|
||||
2010 : Pinterest
|
||||
```
|
||||
|
||||
### Dark Theme
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'dark' } }%%
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008 : Instagram
|
||||
2010 : Pinterest
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'dark' } }%%
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008 : Instagram
|
||||
2010 : Pinterest
|
||||
```
|
||||
|
||||
### Default Theme
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' } }%%
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008 : Instagram
|
||||
2010 : Pinterest
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' } }%%
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008 : Instagram
|
||||
2010 : Pinterest
|
||||
```
|
||||
|
||||
### Neutral Theme
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'neutral' } }%%
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008 : Instagram
|
||||
2010 : Pinterest
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'neutral' } }%%
|
||||
timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008 : Instagram
|
||||
2010 : Pinterest
|
||||
```
|
||||
|
||||
You can also refer the implementation in the live editor [here](https://github.com/mermaid-js/mermaid-live-editor/blob/fcf53c98c25604c90a218104268c339be53035a6/src/lib/util/mermaid.ts) to see how the async loading is done.
|
||||
12
package.json
12
package.json
@@ -4,7 +4,7 @@
|
||||
"version": "9.3.0-rc1",
|
||||
"description": "Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@7.25.0",
|
||||
"packageManager": "pnpm@7.25.1",
|
||||
"keywords": [
|
||||
"diagram",
|
||||
"markdown",
|
||||
@@ -23,7 +23,7 @@
|
||||
"build": "pnpm run -r clean && concurrently \"pnpm build:vite\" \"pnpm build:types\"",
|
||||
"dev": "concurrently \"pnpm build:vite --watch\" \"ts-node-esm .vite/server.ts\"",
|
||||
"release": "pnpm build",
|
||||
"lint": "eslint --cache --ignore-path .gitignore . && pnpm lint:jison && prettier --check .",
|
||||
"lint": "eslint --cache --cache-strategy content --ignore-path .gitignore . && pnpm lint:jison && prettier --cache --check .",
|
||||
"lint:fix": "eslint --fix --ignore-path .gitignore . && prettier --write . && ts-node-esm scripts/fixCSpell.ts",
|
||||
"lint:jison": "ts-node-esm ./scripts/jison/lint.mts",
|
||||
"cypress": "cypress run",
|
||||
@@ -67,8 +67,8 @@
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/prettier": "^2.7.1",
|
||||
"@types/rollup-plugin-visualizer": "^4.2.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.42.1",
|
||||
"@typescript-eslint/parser": "^5.42.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.2",
|
||||
"@typescript-eslint/parser": "^5.48.2",
|
||||
"@vitest/coverage-c8": "^0.27.0",
|
||||
"@vitest/ui": "^0.27.0",
|
||||
"concurrently": "^7.5.0",
|
||||
@@ -76,8 +76,8 @@
|
||||
"cypress": "^10.11.0",
|
||||
"cypress-image-snapshot": "^4.0.1",
|
||||
"esbuild": "^0.17.0",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint": "^8.32.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"eslint-plugin-html": "^7.1.0",
|
||||
"eslint-plugin-jest": "^27.1.5",
|
||||
|
||||
@@ -22,6 +22,7 @@ export const log: Record<keyof typeof LEVELS, typeof console.log> = {
|
||||
export let setLogLevel: (level: keyof typeof LEVELS | number | string) => void;
|
||||
export let getConfig: () => object;
|
||||
export let sanitizeText: (str: string) => string;
|
||||
export let commonDb: any;
|
||||
/**
|
||||
* Placeholder for the real function that will be injected by mermaid.
|
||||
*/
|
||||
@@ -41,15 +42,17 @@ export let setupGraphViewbox: (
|
||||
* @param _getConfig - getConfig from mermaid/src/diagramAPI.ts
|
||||
* @param _sanitizeText - sanitizeText from mermaid/src/diagramAPI.ts
|
||||
* @param _setupGraphViewbox - setupGraphViewbox from mermaid/src/diagramAPI.ts
|
||||
* @param _commonDb -`commonDb` from mermaid/src/diagramAPI.ts
|
||||
*/
|
||||
export const injectUtils = (
|
||||
_log: Record<keyof typeof LEVELS, typeof console.log>,
|
||||
_setLogLevel: typeof setLogLevel,
|
||||
_getConfig: typeof getConfig,
|
||||
_sanitizeText: typeof sanitizeText,
|
||||
_setupGraphViewbox: typeof setupGraphViewbox
|
||||
_setupGraphViewbox: typeof setupGraphViewbox,
|
||||
_commonDb: any
|
||||
) => {
|
||||
_log.debug('Mermaid utils injected into example-diagram');
|
||||
_log.info('Mermaid utils injected into timeline-diagram');
|
||||
log.trace = _log.trace;
|
||||
log.debug = _log.debug;
|
||||
log.info = _log.info;
|
||||
@@ -60,4 +63,5 @@ export const injectUtils = (
|
||||
getConfig = _getConfig;
|
||||
sanitizeText = _sanitizeText;
|
||||
setupGraphViewbox = _setupGraphViewbox;
|
||||
commonDb = _commonDb;
|
||||
};
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
{
|
||||
"name": "@mermaid-js/mermaid-flowchart-v3",
|
||||
"version": "9.4.0",
|
||||
"description": "An extension for the Mermaid diagramming library that utilizes elkjs for layout, enabling the creation of visually appealing and easy-to-understand flowcharts.",
|
||||
"module": "dist/mermaid-flowchart-v3.core.mjs",
|
||||
"types": "dist/detector.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/mermaid-flowchart-v3.core.mjs",
|
||||
"types": "./dist/detector.d.ts"
|
||||
},
|
||||
"./*": "./*"
|
||||
},
|
||||
"keywords": [
|
||||
"diagram",
|
||||
"markdown",
|
||||
"flowchart",
|
||||
"mermaid",
|
||||
"elkjs"
|
||||
],
|
||||
"scripts": {
|
||||
"prepublishOnly": "pnpm -w run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mermaid-js/mermaid"
|
||||
},
|
||||
"author": "Knut Sveidqvist",
|
||||
"license": "MIT",
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"**/parser/*.js",
|
||||
"dist/**/*.js",
|
||||
"cypress/**/*.js"
|
||||
],
|
||||
"globals": [
|
||||
"page"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^6.0.0",
|
||||
"graphlib": "^2.1.0",
|
||||
"dagre-d3-es": "7.0.6",
|
||||
"elkjs": "^0.8.2",
|
||||
"d3": "^7.0.0",
|
||||
"khroma": "^2.0.0",
|
||||
"non-layered-tidy-tree-layout": "^2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^7.5.0",
|
||||
"mermaid": "workspace:*",
|
||||
"rimraf": "^3.0.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"d3": "^7.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"sideEffects": [
|
||||
"**/*.css",
|
||||
"**/*.scss"
|
||||
]
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import type { ExternalDiagramDefinition } from 'mermaid';
|
||||
|
||||
const id = 'flowchart-v3';
|
||||
|
||||
const detector = (txt: string, config) => {
|
||||
if (config?.flowchart?.defaultRenderer === 'dagre-d3') {
|
||||
return false;
|
||||
}
|
||||
if (config?.flowchart?.defaultRenderer === 'dagre-wrapper') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we have configured to use dagre-wrapper then we should return true in this function for graph code thus making it use the new flowchart diagram
|
||||
if (txt.match(/^\s*graph/) !== null) {
|
||||
return true;
|
||||
}
|
||||
return txt.match(/^\s*flowchart/) !== null;
|
||||
};
|
||||
|
||||
const loader = async () => {
|
||||
const { diagram } = await import('./diagram-definition');
|
||||
return { id, diagram };
|
||||
};
|
||||
|
||||
const plugin: ExternalDiagramDefinition = {
|
||||
id,
|
||||
detector,
|
||||
loader,
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
@@ -1,14 +0,0 @@
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
import parser from '../../mermaid/src/diagrams/flowchart/parser/flow';
|
||||
import * as db from '../../mermaid/src/diagrams/flowchart/flowDb';
|
||||
import renderer from './flowRenderer-v3';
|
||||
import styles from './styles';
|
||||
import { injectUtils } from './mermaidUtils';
|
||||
|
||||
export const diagram = {
|
||||
db,
|
||||
renderer,
|
||||
parser,
|
||||
styles,
|
||||
injectUtils,
|
||||
};
|
||||
@@ -1,756 +0,0 @@
|
||||
import graphlib from 'graphlib';
|
||||
import { select, line, curveLinear, curveCardinal, curveBasis, selectAll } from 'd3';
|
||||
import { log, getConfig, setupGraphViewbox } from './mermaidUtils';
|
||||
import { insertNode } from '../../mermaid/src/dagre-wrapper/nodes.js';
|
||||
import insertMarkers from '../../mermaid/src/dagre-wrapper/markers.js';
|
||||
import dagre from 'cytoscape-dagre';
|
||||
|
||||
// Replace with other function to avoid dependency to dagre-d3
|
||||
import { addHtmlLabel } from 'dagre-d3-es/src/dagre-js/label/add-html-label.js';
|
||||
|
||||
import common, { evaluate } from '../../mermaid/src/diagrams/common/common';
|
||||
import { interpolateToCurve, getStylesFromArray } from '../../mermaid/src/utils';
|
||||
|
||||
import cytoscape from 'cytoscape';
|
||||
cytoscape.use(dagre);
|
||||
|
||||
const conf = {};
|
||||
export const setConf = function (cnf) {
|
||||
const keys = Object.keys(cnf);
|
||||
for (const key of keys) {
|
||||
conf[key] = cnf[key];
|
||||
}
|
||||
};
|
||||
|
||||
// /**
|
||||
// * Function that adds the vertices found during parsing to the graph to be rendered.
|
||||
// *
|
||||
// * @param vert Object containing the vertices.
|
||||
// * @param g The graph that is to be drawn.
|
||||
// * @param svgId
|
||||
// * @param root
|
||||
// * @param doc
|
||||
// * @param diagObj
|
||||
// */
|
||||
export const addVertices = function (vert, svgId, root, doc, diagObj, parentLookUpDb, graph) {
|
||||
const svg = root.select(`[id="${svgId}"]`);
|
||||
const nodes = svg.insert('g').attr('class', 'nodes');
|
||||
const keys = Object.keys(vert);
|
||||
|
||||
// Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
|
||||
keys.forEach(function (id) {
|
||||
const vertex = vert[id];
|
||||
|
||||
/**
|
||||
* Variable for storing the classes for the vertex
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
let classStr = 'default';
|
||||
if (vertex.classes.length > 0) {
|
||||
classStr = vertex.classes.join(' ');
|
||||
}
|
||||
|
||||
const styles = getStylesFromArray(vertex.styles);
|
||||
|
||||
// Use vertex id as text in the box if no text is provided by the graph definition
|
||||
let vertexText = vertex.text !== undefined ? vertex.text : vertex.id;
|
||||
|
||||
// We create a SVG label, either by delegating to addHtmlLabel or manually
|
||||
let vertexNode;
|
||||
if (evaluate(getConfig().flowchart.htmlLabels)) {
|
||||
// TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that?
|
||||
const node = {
|
||||
label: vertexText.replace(
|
||||
/fa[blrs]?:fa-[\w-]+/g,
|
||||
(s) => `<i class='${s.replace(':', ' ')}'></i>`
|
||||
),
|
||||
};
|
||||
vertexNode = addHtmlLabel(svg, node).node();
|
||||
vertexNode.parentNode.removeChild(vertexNode);
|
||||
} else {
|
||||
const svgLabel = doc.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
svgLabel.setAttribute('style', styles.labelStyle.replace('color:', 'fill:'));
|
||||
|
||||
const rows = vertexText.split(common.lineBreakRegex);
|
||||
|
||||
for (const row of rows) {
|
||||
const tspan = doc.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
||||
tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
|
||||
tspan.setAttribute('dy', '1em');
|
||||
tspan.setAttribute('x', '1');
|
||||
tspan.textContent = row;
|
||||
svgLabel.appendChild(tspan);
|
||||
}
|
||||
vertexNode = svgLabel;
|
||||
}
|
||||
|
||||
let radious = 0;
|
||||
let _shape = '';
|
||||
// Set the shape based parameters
|
||||
switch (vertex.type) {
|
||||
case 'round':
|
||||
radious = 5;
|
||||
_shape = 'rect';
|
||||
break;
|
||||
case 'square':
|
||||
_shape = 'rect';
|
||||
break;
|
||||
case 'diamond':
|
||||
_shape = 'question';
|
||||
break;
|
||||
case 'hexagon':
|
||||
_shape = 'hexagon';
|
||||
break;
|
||||
case 'odd':
|
||||
_shape = 'rect_left_inv_arrow';
|
||||
break;
|
||||
case 'lean_right':
|
||||
_shape = 'lean_right';
|
||||
break;
|
||||
case 'lean_left':
|
||||
_shape = 'lean_left';
|
||||
break;
|
||||
case 'trapezoid':
|
||||
_shape = 'trapezoid';
|
||||
break;
|
||||
case 'inv_trapezoid':
|
||||
_shape = 'inv_trapezoid';
|
||||
break;
|
||||
case 'odd_right':
|
||||
_shape = 'rect_left_inv_arrow';
|
||||
break;
|
||||
case 'circle':
|
||||
_shape = 'circle';
|
||||
break;
|
||||
case 'ellipse':
|
||||
_shape = 'ellipse';
|
||||
break;
|
||||
case 'stadium':
|
||||
_shape = 'stadium';
|
||||
break;
|
||||
case 'subroutine':
|
||||
_shape = 'subroutine';
|
||||
break;
|
||||
case 'cylinder':
|
||||
_shape = 'cylinder';
|
||||
break;
|
||||
case 'group':
|
||||
_shape = 'rect';
|
||||
break;
|
||||
case 'doublecircle':
|
||||
_shape = 'doublecircle';
|
||||
break;
|
||||
default:
|
||||
_shape = 'rect';
|
||||
}
|
||||
// // Add the node
|
||||
const node = {
|
||||
labelStyle: styles.labelStyle,
|
||||
shape: _shape,
|
||||
labelText: vertexText,
|
||||
rx: radious,
|
||||
ry: radious,
|
||||
class: classStr,
|
||||
style: styles.style,
|
||||
id: vertex.id,
|
||||
link: vertex.link,
|
||||
linkTarget: vertex.linkTarget,
|
||||
tooltip: diagObj.db.getTooltip(vertex.id) || '',
|
||||
domId: diagObj.db.lookUpDomId(vertex.id),
|
||||
haveCallback: vertex.haveCallback,
|
||||
width: vertex.type === 'group' ? 500 : undefined,
|
||||
dir: vertex.dir,
|
||||
type: vertex.type,
|
||||
props: vertex.props,
|
||||
padding: getConfig().flowchart.padding,
|
||||
};
|
||||
const nodeEl = insertNode(nodes, node, vertex.dir);
|
||||
const boundingBox = nodeEl.node().getBBox();
|
||||
const data = {
|
||||
id: vertex.id,
|
||||
labelStyle: styles.labelStyle,
|
||||
shape: _shape,
|
||||
labelText: vertexText,
|
||||
rx: radious,
|
||||
ry: radious,
|
||||
class: classStr,
|
||||
style: styles.style,
|
||||
link: vertex.link,
|
||||
linkTarget: vertex.linkTarget,
|
||||
tooltip: diagObj.db.getTooltip(vertex.id) || '',
|
||||
domId: diagObj.db.lookUpDomId(vertex.id),
|
||||
haveCallback: vertex.haveCallback,
|
||||
width: vertex.type === 'group' ? 500 : undefined,
|
||||
dir: vertex.dir,
|
||||
type: vertex.type,
|
||||
props: vertex.props,
|
||||
padding: getConfig().flowchart.padding,
|
||||
boundingBox,
|
||||
el: nodeEl,
|
||||
parent: parentLookUpDb.parentById[vertex.id],
|
||||
};
|
||||
// if (!Object.keys(parentLookUpDb.childrenById).includes(vertex.id)) {
|
||||
graph.elements.nodes.push({
|
||||
group: 'nodes',
|
||||
// data,
|
||||
data,
|
||||
});
|
||||
// }
|
||||
log.trace('setNode', {
|
||||
labelStyle: styles.labelStyle,
|
||||
shape: _shape,
|
||||
labelText: vertexText,
|
||||
rx: radious,
|
||||
ry: radious,
|
||||
class: classStr,
|
||||
style: styles.style,
|
||||
id: vertex.id,
|
||||
domId: diagObj.db.lookUpDomId(vertex.id),
|
||||
width: vertex.type === 'group' ? 500 : undefined,
|
||||
type: vertex.type,
|
||||
dir: vertex.dir,
|
||||
props: vertex.props,
|
||||
padding: getConfig().flowchart.padding,
|
||||
parent: parentLookUpDb.parentById[vertex.id],
|
||||
});
|
||||
});
|
||||
return graph;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add edges to graph based on parsed graph definition
|
||||
*
|
||||
* @param {object} edges The edges to add to the graph
|
||||
* @param {object} g The graph object
|
||||
* @param cy
|
||||
* @param diagObj
|
||||
* @param graph
|
||||
*/
|
||||
export const addEdges = function (edges, diagObj, graph) {
|
||||
// log.info('abc78 edges = ', edges);
|
||||
let cnt = 0;
|
||||
let linkIdCnt = {};
|
||||
|
||||
let defaultStyle;
|
||||
let defaultLabelStyle;
|
||||
|
||||
if (edges.defaultStyle !== undefined) {
|
||||
const defaultStyles = getStylesFromArray(edges.defaultStyle);
|
||||
defaultStyle = defaultStyles.style;
|
||||
defaultLabelStyle = defaultStyles.labelStyle;
|
||||
}
|
||||
|
||||
edges.forEach(function (edge) {
|
||||
cnt++;
|
||||
|
||||
// Identify Link
|
||||
var linkIdBase = 'L-' + edge.start + '-' + edge.end;
|
||||
// count the links from+to the same node to give unique id
|
||||
if (linkIdCnt[linkIdBase] === undefined) {
|
||||
linkIdCnt[linkIdBase] = 0;
|
||||
log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]);
|
||||
} else {
|
||||
linkIdCnt[linkIdBase]++;
|
||||
log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]);
|
||||
}
|
||||
let linkId = linkIdBase + '-' + linkIdCnt[linkIdBase];
|
||||
log.info('abc78 new link id to be used is', linkIdBase, linkId, linkIdCnt[linkIdBase]);
|
||||
var linkNameStart = 'LS-' + edge.start;
|
||||
var linkNameEnd = 'LE-' + edge.end;
|
||||
|
||||
const edgeData = { style: '', labelStyle: '' };
|
||||
edgeData.minlen = edge.length || 1;
|
||||
//edgeData.id = 'id' + cnt;
|
||||
|
||||
// Set link type for rendering
|
||||
if (edge.type === 'arrow_open') {
|
||||
edgeData.arrowhead = 'none';
|
||||
} else {
|
||||
edgeData.arrowhead = 'normal';
|
||||
}
|
||||
|
||||
// Check of arrow types, placed here in order not to break old rendering
|
||||
edgeData.arrowTypeStart = 'arrow_open';
|
||||
edgeData.arrowTypeEnd = 'arrow_open';
|
||||
|
||||
/* eslint-disable no-fallthrough */
|
||||
switch (edge.type) {
|
||||
case 'double_arrow_cross':
|
||||
edgeData.arrowTypeStart = 'arrow_cross';
|
||||
case 'arrow_cross':
|
||||
edgeData.arrowTypeEnd = 'arrow_cross';
|
||||
break;
|
||||
case 'double_arrow_point':
|
||||
edgeData.arrowTypeStart = 'arrow_point';
|
||||
case 'arrow_point':
|
||||
edgeData.arrowTypeEnd = 'arrow_point';
|
||||
break;
|
||||
case 'double_arrow_circle':
|
||||
edgeData.arrowTypeStart = 'arrow_circle';
|
||||
case 'arrow_circle':
|
||||
edgeData.arrowTypeEnd = 'arrow_circle';
|
||||
break;
|
||||
}
|
||||
|
||||
let style = '';
|
||||
let labelStyle = '';
|
||||
|
||||
switch (edge.stroke) {
|
||||
case 'normal':
|
||||
style = 'fill:none;';
|
||||
if (defaultStyle !== undefined) {
|
||||
style = defaultStyle;
|
||||
}
|
||||
if (defaultLabelStyle !== undefined) {
|
||||
labelStyle = defaultLabelStyle;
|
||||
}
|
||||
edgeData.thickness = 'normal';
|
||||
edgeData.pattern = 'solid';
|
||||
break;
|
||||
case 'dotted':
|
||||
edgeData.thickness = 'normal';
|
||||
edgeData.pattern = 'dotted';
|
||||
edgeData.style = 'fill:none;stroke-width:2px;stroke-dasharray:3;';
|
||||
break;
|
||||
case 'thick':
|
||||
edgeData.thickness = 'thick';
|
||||
edgeData.pattern = 'solid';
|
||||
edgeData.style = 'stroke-width: 3.5px;fill:none;';
|
||||
break;
|
||||
}
|
||||
if (edge.style !== undefined) {
|
||||
const styles = getStylesFromArray(edge.style);
|
||||
style = styles.style;
|
||||
labelStyle = styles.labelStyle;
|
||||
}
|
||||
|
||||
edgeData.style = edgeData.style += style;
|
||||
edgeData.labelStyle = edgeData.labelStyle += labelStyle;
|
||||
|
||||
if (edge.interpolate !== undefined) {
|
||||
edgeData.curve = interpolateToCurve(edge.interpolate, curveLinear);
|
||||
} else if (edges.defaultInterpolate !== undefined) {
|
||||
edgeData.curve = interpolateToCurve(edges.defaultInterpolate, curveLinear);
|
||||
} else {
|
||||
edgeData.curve = interpolateToCurve(conf.curve, curveLinear);
|
||||
}
|
||||
|
||||
if (edge.text === undefined) {
|
||||
if (edge.style !== undefined) {
|
||||
edgeData.arrowheadStyle = 'fill: #333';
|
||||
}
|
||||
} else {
|
||||
edgeData.arrowheadStyle = 'fill: #333';
|
||||
edgeData.labelpos = 'c';
|
||||
}
|
||||
|
||||
edgeData.labelType = 'text';
|
||||
edgeData.label = edge.text.replace(common.lineBreakRegex, '\n');
|
||||
|
||||
if (edge.style === undefined) {
|
||||
edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none;';
|
||||
}
|
||||
|
||||
edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:');
|
||||
|
||||
edgeData.id = linkId;
|
||||
edgeData.classes = 'flowchart-link ' + linkNameStart + ' ' + linkNameEnd;
|
||||
|
||||
// Add the edge to the graph
|
||||
graph.elements.edges.push({
|
||||
group: 'edges',
|
||||
data: { source: edge.start, target: edge.end, edgeData, id: cnt },
|
||||
});
|
||||
});
|
||||
return graph;
|
||||
};
|
||||
|
||||
const addmarkers = function (svgPath, edgeData, diagramType, arrowMarkerAbsolute) {
|
||||
// // TODO: Can we load this config only from the rendered graph type?
|
||||
let url;
|
||||
if (arrowMarkerAbsolute) {
|
||||
url =
|
||||
window.location.protocol +
|
||||
'//' +
|
||||
window.location.host +
|
||||
window.location.pathname +
|
||||
window.location.search;
|
||||
url = url.replace(/\(/g, '\\(');
|
||||
url = url.replace(/\)/g, '\\)');
|
||||
}
|
||||
switch (edgeData.arrowTypeStart) {
|
||||
case 'arrow_cross':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-crossStart' + ')');
|
||||
break;
|
||||
case 'arrow_point':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-pointStart' + ')');
|
||||
break;
|
||||
case 'arrow_barb':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-barbStart' + ')');
|
||||
break;
|
||||
case 'arrow_circle':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-circleStart' + ')');
|
||||
break;
|
||||
case 'aggregation':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-aggregationStart' + ')');
|
||||
break;
|
||||
case 'extension':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-extensionStart' + ')');
|
||||
break;
|
||||
case 'composition':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-compositionStart' + ')');
|
||||
break;
|
||||
case 'dependency':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-dependencyStart' + ')');
|
||||
break;
|
||||
case 'lollipop':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-lollipopStart' + ')');
|
||||
break;
|
||||
default:
|
||||
}
|
||||
switch (edgeData.arrowTypeEnd) {
|
||||
case 'arrow_cross':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-crossEnd' + ')');
|
||||
break;
|
||||
case 'arrow_point':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-pointEnd' + ')');
|
||||
break;
|
||||
case 'arrow_barb':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-barbEnd' + ')');
|
||||
break;
|
||||
case 'arrow_circle':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-circleEnd' + ')');
|
||||
break;
|
||||
case 'aggregation':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-aggregationEnd' + ')');
|
||||
break;
|
||||
case 'extension':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-extensionEnd' + ')');
|
||||
break;
|
||||
case 'composition':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-compositionEnd' + ')');
|
||||
break;
|
||||
case 'dependency':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-dependencyEnd' + ')');
|
||||
break;
|
||||
case 'lollipop':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-lollipopEnd' + ')');
|
||||
break;
|
||||
default:
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the all the styles from classDef statements in the graph definition.
|
||||
*
|
||||
* @param text
|
||||
* @param diagObj
|
||||
* @returns {object} ClassDef styles
|
||||
*/
|
||||
export const getClasses = function (text, diagObj) {
|
||||
log.info('Extracting classes');
|
||||
diagObj.db.clear('ver-2');
|
||||
try {
|
||||
// Parse the graph definition
|
||||
diagObj.parse(text);
|
||||
return diagObj.db.getClasses();
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const addSubGraphs = function (db) {
|
||||
const parentLookUpDb = { parentById: {}, childrenById: {} };
|
||||
const subgraphs = db.getSubGraphs();
|
||||
log.info('Subgraphs - ', subgraphs);
|
||||
subgraphs.forEach(function (subgraph) {
|
||||
subgraph.nodes.forEach(function (node) {
|
||||
parentLookUpDb.parentById[node] = subgraph.id;
|
||||
if (parentLookUpDb.childrenById[subgraph.id] === undefined) {
|
||||
parentLookUpDb.childrenById[subgraph.id] = [];
|
||||
}
|
||||
parentLookUpDb.childrenById[subgraph.id].push(node);
|
||||
});
|
||||
});
|
||||
|
||||
subgraphs.forEach(function (subgraph) {
|
||||
const data = { id: subgraph.id };
|
||||
if (parentLookUpDb.parentById[subgraph.id] !== undefined) {
|
||||
data.parent = parentLookUpDb.parentById[subgraph.id];
|
||||
}
|
||||
// cy.add({
|
||||
// group: 'nodes',
|
||||
// data,
|
||||
// });
|
||||
});
|
||||
return parentLookUpDb;
|
||||
};
|
||||
|
||||
const insertEdge = function (edgesEl, edge, edgeData, bounds, diagObj) {
|
||||
const src = edge.sourceEndpoint();
|
||||
const segments = edge.segmentPoints();
|
||||
// const dest = edge.target().position();
|
||||
const dest = edge.targetEndpoint();
|
||||
const segPoints = segments.map((segment) => [segment.x, segment.y]);
|
||||
const points = [
|
||||
[src.x, src.y],
|
||||
[segments[0].x, segments[0].y],
|
||||
[dest.x, dest.y],
|
||||
];
|
||||
// console.log('Edge ctrl points:', edge.segmentPoints(), 'Bounds:', bounds, edge.source(), points);
|
||||
// console.log('Edge ctrl points:', points);
|
||||
const curve = line().curve(curveCardinal);
|
||||
const edge2 = edgesEl
|
||||
.insert('path')
|
||||
.attr('d', curve(points))
|
||||
.attr('class', 'path')
|
||||
.attr('fill', 'none');
|
||||
addmarkers(edge2, edgeData, diagObj.type, diagObj.arrowMarkerAbsolute);
|
||||
// edgesEl
|
||||
// .append('circle')
|
||||
// .style('stroke', 'red')
|
||||
// .style('fill', 'red')
|
||||
// .attr('r', 1)
|
||||
// .attr('cx', src.x)
|
||||
// .attr('cy', src.y);
|
||||
// edgesEl
|
||||
// .append('circle')
|
||||
// .style('stroke', 'white')
|
||||
// .style('fill', 'white')
|
||||
// .attr('r', 1)
|
||||
// .attr('cx', segments[0].x)
|
||||
// .attr('cy', segments[0].y);
|
||||
// edgesEl
|
||||
// .append('circle')
|
||||
// .style('stroke', 'pink')
|
||||
// .style('fill', 'pink')
|
||||
// .attr('r', 1)
|
||||
// .attr('cx', dest.x)
|
||||
// .attr('cy', dest.y);
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a flowchart in the tag with id: id based on the graph definition in text.
|
||||
*
|
||||
* @param text
|
||||
* @param id
|
||||
*/
|
||||
|
||||
export const draw = function (text, id, _version, diagObj) {
|
||||
// Add temporary render element
|
||||
diagObj.db.clear();
|
||||
diagObj.db.setGen('gen-2');
|
||||
// Parse the graph definition
|
||||
diagObj.parser.parse(text);
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
const renderEl = select('body').append('div').attr('style', 'height:400px').attr('id', 'cy');
|
||||
// .attr('style', 'display:none')
|
||||
let graph = {
|
||||
styleEnabled: true,
|
||||
// animate: false,
|
||||
// ready: function () {
|
||||
// log.info('Ready', this);
|
||||
// },
|
||||
container: document.getElementById('cy'), // container to render in
|
||||
|
||||
boxSelectionEnabled: false,
|
||||
|
||||
style: [
|
||||
{
|
||||
selector: 'node',
|
||||
css: {
|
||||
content: 'data(id)',
|
||||
'text-valign': 'center',
|
||||
'text-halign': 'center',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: ':parent',
|
||||
css: {
|
||||
'text-valign': 'top',
|
||||
'text-halign': 'center',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: 'edge',
|
||||
css: {
|
||||
'curve-style': 'bezier',
|
||||
'target-arrow-shape': 'triangle',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
elements: {
|
||||
nodes: [
|
||||
{ data: { id: 'a', parent: 'b' } },
|
||||
{ data: { id: 'b' } },
|
||||
{ data: { id: 'c', parent: 'b' } },
|
||||
{ data: { id: 'd' } },
|
||||
{ data: { id: 'e' } },
|
||||
{ data: { id: 'f', parent: 'e' } },
|
||||
],
|
||||
edges: [
|
||||
{ data: { id: 'ad', source: 'a', target: 'd' } },
|
||||
{ data: { id: 'eb', source: 'e', target: 'b' } },
|
||||
],
|
||||
},
|
||||
};
|
||||
log.info('Drawing flowchart using v3 renderer');
|
||||
// Fetch the default direction, use TD if none was found
|
||||
let dir = diagObj.db.getDirection();
|
||||
if (dir === undefined) {
|
||||
dir = 'TD';
|
||||
}
|
||||
|
||||
const { securityLevel, flowchart: conf } = getConfig();
|
||||
|
||||
// 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;
|
||||
|
||||
const svg = root.select(`[id="${id}"]`);
|
||||
const markers = ['point', 'circle', 'cross'];
|
||||
insertMarkers(svg, markers, diagObj.type, diagObj.arrowMarkerAbsolute);
|
||||
// Fetch the vertices/nodes and edges/links from the parsed graph definition
|
||||
const vert = diagObj.db.getVertices();
|
||||
|
||||
let subG;
|
||||
const subGraphs = diagObj.db.getSubGraphs();
|
||||
log.info('Subgraphs - ', subGraphs);
|
||||
for (let i = subGraphs.length - 1; i >= 0; i--) {
|
||||
subG = subGraphs[i];
|
||||
log.info('Subgraph - ', subG);
|
||||
diagObj.db.addVertex(subG.id, subG.title, 'group', undefined, subG.classes, subG.dir);
|
||||
}
|
||||
|
||||
const parentLookUpDb = addSubGraphs(diagObj.db);
|
||||
graph = addVertices(vert, id, root, doc, diagObj, parentLookUpDb, graph);
|
||||
const edgesEl = svg.insert('g').attr('class', 'edges edgePath');
|
||||
const edges = diagObj.db.getEdges();
|
||||
graph = addEdges(edges, diagObj, graph);
|
||||
|
||||
const cy = cytoscape(graph);
|
||||
|
||||
// c.style();
|
||||
// Make cytoscape care about the dimensions of the nodes
|
||||
cy.nodes().forEach(function (n) {
|
||||
const boundingBox = n.data().boundingBox;
|
||||
if (boundingBox) {
|
||||
n.style('width', boundingBox.width);
|
||||
n.style('height', boundingBox.height);
|
||||
}
|
||||
n.style('shape', 'rectangle');
|
||||
// n.layoutDimensions = () => {
|
||||
// // console.log('Node dimensions', boundingBox.width, boundingBox.height);
|
||||
// if (boundingBox) {
|
||||
// return { w: boundingBox.width, h: boundingBox.height };
|
||||
// }
|
||||
// // return { w: boundingBox.width, h: boundingBox.height };
|
||||
|
||||
// // const data = n.data();
|
||||
// // return { w: data.width, h: data.height };
|
||||
|
||||
// return { w: 206, h: 160 };
|
||||
// };
|
||||
});
|
||||
|
||||
cy.layout({
|
||||
// name: 'dagre',
|
||||
// name: 'preset',
|
||||
// name: 'cose',
|
||||
// name: 'circle',
|
||||
name: 'concentric',
|
||||
headless: false,
|
||||
styleEnabled: true,
|
||||
animate: false,
|
||||
}).run();
|
||||
|
||||
// function runLayouts(fit, callBack) {
|
||||
// // step-1 position child nodes
|
||||
// var parentNodes = cy.nodes(':parent');
|
||||
// var grid_layout = parentNodes.descendants().layout({
|
||||
// name: 'grid',
|
||||
// cols: 1,
|
||||
// fit: fit,
|
||||
// });
|
||||
// grid_layout.promiseOn('layoutstop').then(function (event) {
|
||||
// // step-2 position parent nodes
|
||||
// var dagre_layout = parentNodes.layout({
|
||||
// name: 'dagre',
|
||||
// rankDir: 'TB',
|
||||
// fit: fit,
|
||||
// });
|
||||
// dagre_layout.promiseOn('layoutstop').then(function (event) {
|
||||
// if (callBack) {
|
||||
// callBack.call(cy, event);
|
||||
// }
|
||||
// });
|
||||
// dagre_layout.run();
|
||||
// });
|
||||
// grid_layout.run();
|
||||
// }
|
||||
// runLayouts();
|
||||
|
||||
// log.info('Positions', cy.nodes().positions());
|
||||
// window.cy = cy;
|
||||
cy.ready((e) => {
|
||||
log.info('Ready', e, cy.data());
|
||||
// // setTimeout(() => {
|
||||
cy.nodes().map((node, id) => {
|
||||
const data = node.data();
|
||||
|
||||
log.info(
|
||||
'Position: (',
|
||||
node.position().x,
|
||||
', ',
|
||||
node.position().y,
|
||||
')',
|
||||
data,
|
||||
cy.elements()[0].renderedBoundingBox()
|
||||
);
|
||||
if (data.el) {
|
||||
data.el.attr('transform', `translate(${node.position().x}, ${node.position().y})`);
|
||||
// document
|
||||
// .querySelector(`[id="${data.domId}"]`)
|
||||
// .setAttribute('transform', `translate(${node.position().x}, ${node.position().y})`);
|
||||
log.info('Id = ', data.domId, svg.select(`[id="${data.domId}"]`), data.el.node());
|
||||
}
|
||||
// else {
|
||||
// // console.log('No element found for node', data, node.position(), node.size());
|
||||
// }
|
||||
});
|
||||
|
||||
cy.edges().map((edge, id) => {
|
||||
const data = edge.data();
|
||||
if (edge[0]._private.bodyBounds) {
|
||||
const bounds = edge[0]._private.rscratch;
|
||||
// insertEdge(edgesEl, edge, data.edgeData, bounds, diagObj);
|
||||
}
|
||||
});
|
||||
|
||||
log.info(cy.json());
|
||||
setupGraphViewbox({}, svg, conf.diagramPadding, conf.useMaxWidth);
|
||||
// Remove element after layout
|
||||
// renderEl.remove();
|
||||
resolve();
|
||||
// }, 500);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
// setConf,
|
||||
// addVertices,
|
||||
// addEdges,
|
||||
getClasses,
|
||||
draw,
|
||||
};
|
||||
@@ -1,56 +0,0 @@
|
||||
const warning = (s: string) => {
|
||||
// Todo remove debug code
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Log function was called before initialization', s);
|
||||
};
|
||||
|
||||
export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
||||
|
||||
export const LEVELS: Record<LogLevel, number> = {
|
||||
trace: 0,
|
||||
debug: 1,
|
||||
info: 2,
|
||||
warn: 3,
|
||||
error: 4,
|
||||
fatal: 5,
|
||||
};
|
||||
|
||||
export const log: Record<keyof typeof LEVELS, typeof console.log> = {
|
||||
trace: warning,
|
||||
debug: warning,
|
||||
info: warning,
|
||||
warn: warning,
|
||||
error: warning,
|
||||
fatal: warning,
|
||||
};
|
||||
|
||||
export let setLogLevel: (level: keyof typeof LEVELS | number | string) => void;
|
||||
export let getConfig: () => object;
|
||||
export let sanitizeText: (str: string) => string;
|
||||
// eslint-disable @typescript-eslint/no-explicit-any
|
||||
export let setupGraphViewbox: (
|
||||
graph: any,
|
||||
svgElem: any,
|
||||
padding: any,
|
||||
useMaxWidth: boolean
|
||||
) => void;
|
||||
|
||||
export const injectUtils = (
|
||||
_log: Record<keyof typeof LEVELS, typeof console.log>,
|
||||
_setLogLevel: any,
|
||||
_getConfig: any,
|
||||
_sanitizeText: any,
|
||||
_setupGraphViewbox: any
|
||||
) => {
|
||||
_log.info('Mermaid utils injected');
|
||||
log.trace = _log.trace;
|
||||
log.debug = _log.debug;
|
||||
log.info = _log.info;
|
||||
log.warn = _log.warn;
|
||||
log.error = _log.error;
|
||||
log.fatal = _log.fatal;
|
||||
setLogLevel = _setLogLevel;
|
||||
getConfig = _getConfig;
|
||||
sanitizeText = _sanitizeText;
|
||||
setupGraphViewbox = _setupGraphViewbox;
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
import { findCommonAncestor } from './render-utils';
|
||||
describe('when rendering a flowchart using elk ', function () {
|
||||
let lookupDb;
|
||||
beforeEach(function () {
|
||||
lookupDb = JSON.parse(
|
||||
'{"parentById":{"B4":"inner","B5":"inner","C4":"inner2","C5":"inner2","B2":"Ugge","B3":"Ugge","inner":"Ugge","inner2":"Ugge","B6":"outer"},"childrenById":{"inner":["B4","B5"],"inner2":["C4","C5"],"Ugge":["B2","B3","inner","inner2"],"outer":["B6"]}}'
|
||||
);
|
||||
});
|
||||
it('Sieblings in a subgraph', function () {
|
||||
expect(findCommonAncestor('B4', 'B5', lookupDb)).toBe('inner');
|
||||
});
|
||||
it('Find an uncle', function () {
|
||||
expect(findCommonAncestor('B4', 'B2', lookupDb)).toBe('Ugge');
|
||||
});
|
||||
it('Find a cousin', function () {
|
||||
expect(findCommonAncestor('B4', 'C4', lookupDb)).toBe('Ugge');
|
||||
});
|
||||
it('Find a grandparent', function () {
|
||||
expect(findCommonAncestor('B4', 'B6', lookupDb)).toBe('root');
|
||||
});
|
||||
it('Sieblings in the root', function () {
|
||||
expect(findCommonAncestor('B1', 'outer', lookupDb)).toBe('root');
|
||||
});
|
||||
});
|
||||
@@ -47,6 +47,7 @@
|
||||
"non-layered-tidy-tree-layout": "^2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cytoscape": "^3.19.9",
|
||||
"concurrently": "^7.5.0",
|
||||
"mermaid": "workspace:*",
|
||||
"rimraf": "^3.0.2"
|
||||
|
||||
@@ -27,6 +27,7 @@ export const log: Record<keyof typeof LEVELS, typeof console.log> = {
|
||||
export let setLogLevel: (level: keyof typeof LEVELS | number | string) => void;
|
||||
export let getConfig: () => object;
|
||||
export let sanitizeText: (str: string) => string;
|
||||
export let commonDb: () => object;
|
||||
// eslint-disable @typescript-eslint/no-explicit-any
|
||||
export let setupGraphViewbox: (
|
||||
graph: any,
|
||||
@@ -40,7 +41,8 @@ export const injectUtils = (
|
||||
_setLogLevel: any,
|
||||
_getConfig: any,
|
||||
_sanitizeText: any,
|
||||
_setupGraphViewbox: any
|
||||
_setupGraphViewbox: any,
|
||||
_commonDb: any
|
||||
) => {
|
||||
_log.info('Mermaid utils injected');
|
||||
log.trace = _log.trace;
|
||||
@@ -53,4 +55,5 @@ export const injectUtils = (
|
||||
getConfig = _getConfig;
|
||||
sanitizeText = _sanitizeText;
|
||||
setupGraphViewbox = _setupGraphViewbox;
|
||||
commonDb = _commonDb;
|
||||
};
|
||||
|
||||
@@ -347,4 +347,40 @@ root
|
||||
expect(child.children.length).toEqual(2);
|
||||
expect(child.children[1].nodeId).toEqual('b');
|
||||
});
|
||||
it('MMP-23 Rows with only spaces should not interfere', function () {
|
||||
let str = 'mindmap\nroot\n A\n \n\n B';
|
||||
mindmap.parse(str);
|
||||
const mm = mindmap.yy.getMindmap();
|
||||
expect(mm.nodeId).toEqual('root');
|
||||
expect(mm.children.length).toEqual(2);
|
||||
|
||||
const child = mm.children[0];
|
||||
expect(child.nodeId).toEqual('A');
|
||||
const child2 = mm.children[1];
|
||||
expect(child2.nodeId).toEqual('B');
|
||||
});
|
||||
it('MMP-24 Handle rows above the mindmap declarations', function () {
|
||||
let str = '\n \nmindmap\nroot\n A\n \n\n B';
|
||||
mindmap.parse(str);
|
||||
const mm = mindmap.yy.getMindmap();
|
||||
expect(mm.nodeId).toEqual('root');
|
||||
expect(mm.children.length).toEqual(2);
|
||||
|
||||
const child = mm.children[0];
|
||||
expect(child.nodeId).toEqual('A');
|
||||
const child2 = mm.children[1];
|
||||
expect(child2.nodeId).toEqual('B');
|
||||
});
|
||||
it('MMP-25 Handle rows above the mindmap declarations, no space', function () {
|
||||
let str = '\n\n\nmindmap\nroot\n A\n \n\n B';
|
||||
mindmap.parse(str);
|
||||
const mm = mindmap.yy.getMindmap();
|
||||
expect(mm.nodeId).toEqual('root');
|
||||
expect(mm.children.length).toEqual(2);
|
||||
|
||||
const child = mm.children[0];
|
||||
expect(child.nodeId).toEqual('A');
|
||||
const child2 = mm.children[1];
|
||||
expect(child2.nodeId).toEqual('B');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,6 +89,7 @@ function addNodes(mindmap, cy, conf, level) {
|
||||
/**
|
||||
* @param node
|
||||
* @param conf
|
||||
* @param cy
|
||||
*/
|
||||
function layoutMindmap(node, conf) {
|
||||
return new Promise((resolve) => {
|
||||
@@ -131,7 +132,10 @@ function layoutMindmap(node, conf) {
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @param node
|
||||
* @param cy
|
||||
* @param positionedMindmap
|
||||
* @param conf
|
||||
*/
|
||||
function positionNodes(cy) {
|
||||
cy.nodes().map((node, id) => {
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<CLASS>\n { this.popState();}
|
||||
// [\s]*"::icon(" { this.begin('ICON'); }
|
||||
"::icon(" { yy.getLogger().trace('Begin icon');this.begin('ICON'); }
|
||||
[\s]+[\n] {yy.getLogger().trace('SPACELINE');return 'SPACELINE' /* skip all whitespace */ ;}
|
||||
[\n]+ return 'NL';
|
||||
<ICON>[^\)]+ { return 'ICON'; }
|
||||
<ICON>\) {yy.getLogger().trace('end icon');this.popState();}
|
||||
@@ -64,14 +65,25 @@
|
||||
|
||||
start
|
||||
// %{ : info document 'EOF' { return yy; } }
|
||||
: MINDMAP document { return yy; }
|
||||
| MINDMAP NL document { return yy; }
|
||||
| SPACELIST MINDMAP document { return yy; }
|
||||
;
|
||||
: mindMap
|
||||
| spaceLines mindMap
|
||||
;
|
||||
|
||||
spaceLines
|
||||
: SPACELINE
|
||||
| spaceLines SPACELINE
|
||||
| spaceLines NL
|
||||
;
|
||||
|
||||
mindMap
|
||||
: MINDMAP document { return yy; }
|
||||
| MINDMAP NL document { return yy; }
|
||||
;
|
||||
|
||||
stop
|
||||
: NL {yy.getLogger().trace('Stop NL ');}
|
||||
| EOF {yy.getLogger().trace('Stop EOF ');}
|
||||
| SPACELINE
|
||||
| stop NL {yy.getLogger().trace('Stop NL2 ');}
|
||||
| stop EOF {yy.getLogger().trace('Stop EOF2 ');}
|
||||
;
|
||||
@@ -81,9 +93,10 @@ document
|
||||
;
|
||||
|
||||
statement
|
||||
: SPACELIST node { yy.getLogger().trace('Node: ',$2.id);yy.addNode($1.length, $2.id, $2.descr, $2.type); }
|
||||
: SPACELIST node { yy.getLogger().info('Node: ',$2.id);yy.addNode($1.length, $2.id, $2.descr, $2.type); }
|
||||
| SPACELIST ICON { yy.getLogger().trace('Icon: ',$2);yy.decorateNode({icon: $2}); }
|
||||
| SPACELIST CLASS { yy.decorateNode({class: $2}); }
|
||||
| SPACELINE { yy.getLogger().trace('SPACELIST');}
|
||||
| node { yy.getLogger().trace('Node: ',$1.id);yy.addNode(0, $1.id, $1.descr, $1.type); }
|
||||
| ICON { yy.decorateNode({icon: $1}); }
|
||||
| CLASS { yy.decorateNode({class: $1}); }
|
||||
|
||||
@@ -203,15 +203,14 @@ const roundedRectBkg = function (elem, node) {
|
||||
* @returns {number} The height nodes dom element
|
||||
*/
|
||||
export const drawNode = function (elem, node, fullSection, conf) {
|
||||
const section = (fullSection % MAX_SECTIONS) - 1;
|
||||
const section = fullSection % (MAX_SECTIONS - 1);
|
||||
const nodeElem = elem.append('g');
|
||||
node.section = section;
|
||||
nodeElem.attr(
|
||||
'class',
|
||||
(node.class ? node.class + ' ' : '') +
|
||||
'mindmap-node ' +
|
||||
(section < 0 ? 'section-root' : 'section-' + section)
|
||||
);
|
||||
let sectionClass = 'section-' + section;
|
||||
if (section < 0) {
|
||||
sectionClass += ' section-root';
|
||||
}
|
||||
nodeElem.attr('class', (node.class ? node.class + ' ' : '') + 'mindmap-node ' + sectionClass);
|
||||
const bkgElem = nodeElem.append('g');
|
||||
|
||||
// Create the wrapped text element
|
||||
@@ -305,7 +304,7 @@ export const drawNode = function (elem, node, fullSection, conf) {
|
||||
};
|
||||
|
||||
export const drawEdge = function drawEdge(edgesElem, mindmap, parent, depth, fullSection) {
|
||||
const section = (fullSection % MAX_SECTIONS) - 1;
|
||||
const section = fullSection % (MAX_SECTIONS - 1);
|
||||
const sx = parent.x + parent.width / 2;
|
||||
const sy = parent.y + parent.height / 2;
|
||||
const ex = mindmap.x + mindmap.width / 2;
|
||||
|
||||
@@ -54,11 +54,12 @@
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^6.0.0",
|
||||
"d3": "^7.0.0",
|
||||
"dagre-d3-es": "7.0.6",
|
||||
"dagre-d3-es": "7.0.8",
|
||||
"dompurify": "2.4.3",
|
||||
"elkjs": "^0.8.2",
|
||||
"khroma": "^2.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"moment-mini": "^2.24.0",
|
||||
"moment": "^2.29.4",
|
||||
"non-layered-tidy-tree-layout": "^2.0.2",
|
||||
"stylis": "^4.1.2",
|
||||
"ts-dedent": "^2.2.0",
|
||||
@@ -85,10 +86,10 @@
|
||||
"js-base64": "^3.7.2",
|
||||
"jsdom": "^20.0.2",
|
||||
"micromatch": "^4.0.5",
|
||||
"moment": "^2.29.4",
|
||||
"path-browserify": "^1.0.1",
|
||||
"prettier": "^2.7.1",
|
||||
"remark": "^14.0.2",
|
||||
"remark-frontmatter": "^4.0.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"start-server-and-test": "^1.14.0",
|
||||
|
||||
@@ -6,7 +6,7 @@ import { extractFrontMatter } from './diagram-api/frontmatter';
|
||||
import { isDetailedError } from './utils';
|
||||
import type { DetailedError } from './utils';
|
||||
|
||||
export type ParseErrorFunction = (err: string | DetailedError, hash?: any) => void;
|
||||
export type ParseErrorFunction = (err: string | DetailedError | unknown, hash?: any) => void;
|
||||
|
||||
export class Diagram {
|
||||
type = 'graph';
|
||||
@@ -44,7 +44,7 @@ export class Diagram {
|
||||
this.parser.parser.yy = this.db;
|
||||
if (diagram.init) {
|
||||
diagram.init(cnf);
|
||||
log.debug('Initialized diagram ' + this.type, cnf);
|
||||
log.info('Initialized diagram ' + this.type, cnf);
|
||||
}
|
||||
this.txt += '\n';
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface MermaidConfig {
|
||||
sequence?: SequenceDiagramConfig;
|
||||
gantt?: GanttDiagramConfig;
|
||||
journey?: JourneyDiagramConfig;
|
||||
timeline?: TimelineDiagramConfig;
|
||||
class?: ClassDiagramConfig;
|
||||
state?: StateDiagramConfig;
|
||||
er?: ErDiagramConfig;
|
||||
@@ -292,6 +293,30 @@ export interface JourneyDiagramConfig extends BaseDiagramConfig {
|
||||
sectionColours?: string[];
|
||||
}
|
||||
|
||||
export interface TimelineDiagramConfig extends BaseDiagramConfig {
|
||||
diagramMarginX?: number;
|
||||
diagramMarginY?: number;
|
||||
leftMargin?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
boxMargin?: number;
|
||||
boxTextMargin?: number;
|
||||
noteMargin?: number;
|
||||
messageMargin?: number;
|
||||
messageAlign?: string;
|
||||
bottomMarginAdj?: number;
|
||||
rightAngles?: boolean;
|
||||
taskFontSize?: string | number;
|
||||
taskFontFamily?: string;
|
||||
taskMargin?: number;
|
||||
activationWidth?: number;
|
||||
textPlacement?: string;
|
||||
actorColours?: string[];
|
||||
sectionFills?: string[];
|
||||
sectionColours?: string[];
|
||||
disableMulticolor?: boolean;
|
||||
}
|
||||
|
||||
export interface GanttDiagramConfig extends BaseDiagramConfig {
|
||||
titleTopMargin?: number;
|
||||
barHeight?: number;
|
||||
|
||||
@@ -862,6 +862,156 @@ const config: Partial<MermaidConfig> = {
|
||||
sectionFills: ['#191970', '#8B008B', '#4B0082', '#2F4F4F', '#800000', '#8B4513', '#00008B'],
|
||||
sectionColours: ['#fff'],
|
||||
},
|
||||
/** The object containing configurations specific for timeline diagrams */
|
||||
timeline: {
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | -------------- | ---------------------------------------------------- | ------- | -------- | ------------------ |
|
||||
* | diagramMarginX | Margin to the right and left of the sequence diagram | Integer | Required | Any Positive Value |
|
||||
*
|
||||
* **Notes:** Default value: 50
|
||||
*/
|
||||
diagramMarginX: 50,
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | -------------- | -------------------------------------------------- | ------- | -------- | ------------------ |
|
||||
* | diagramMarginY | Margin to the over and under the sequence diagram. | Integer | Required | Any Positive Value |
|
||||
*
|
||||
* **Notes:** Default value: 10
|
||||
*/
|
||||
diagramMarginY: 10,
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ----------- | --------------------- | ------- | -------- | ------------------ |
|
||||
* | actorMargin | Margin between actors | Integer | Required | Any Positive Value |
|
||||
*
|
||||
* **Notes:** Default value: 50
|
||||
*/
|
||||
leftMargin: 150,
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | --------- | -------------------- | ------- | -------- | ------------------ |
|
||||
* | width | Width of actor boxes | Integer | Required | Any Positive Value |
|
||||
*
|
||||
* **Notes:** Default value: 150
|
||||
*/
|
||||
width: 150,
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | --------- | --------------------- | ------- | -------- | ------------------ |
|
||||
* | height | Height of actor boxes | Integer | Required | Any Positive Value |
|
||||
*
|
||||
* **Notes:** Default value: 65
|
||||
*/
|
||||
height: 50,
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | --------- | ------------------------ | ------- | -------- | ------------------ |
|
||||
* | boxMargin | Margin around loop boxes | Integer | Required | Any Positive Value |
|
||||
*
|
||||
* **Notes:** Default value: 10
|
||||
*/
|
||||
boxMargin: 10,
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ------------- | -------------------------------------------- | ------- | -------- | ------------------ |
|
||||
* | boxTextMargin | Margin around the text in loop/alt/opt boxes | Integer | Required | Any Positive Value |
|
||||
*
|
||||
* **Notes:** Default value: 5
|
||||
*/
|
||||
boxTextMargin: 5,
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ---------- | ------------------- | ------- | -------- | ------------------ |
|
||||
* | noteMargin | Margin around notes | Integer | Required | Any Positive Value |
|
||||
*
|
||||
* **Notes:** Default value: 10
|
||||
*/
|
||||
noteMargin: 10,
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ------------- | ----------------------- | ------- | -------- | ------------------ |
|
||||
* | messageMargin | Space between messages. | Integer | Required | Any Positive Value |
|
||||
*
|
||||
* **Notes:**
|
||||
*
|
||||
* Space between messages.
|
||||
*
|
||||
* Default value: 35
|
||||
*/
|
||||
messageMargin: 35,
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ------------ | --------------------------- | ---- | -------- | ------------------------- |
|
||||
* | messageAlign | Multiline message alignment | 3 | 4 | 'left', 'center', 'right' |
|
||||
*
|
||||
* **Notes:** Default value: 'center'
|
||||
*/
|
||||
messageAlign: 'center',
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | --------------- | ------------------------------------------ | ------- | -------- | ------------------ |
|
||||
* | bottomMarginAdj | Prolongs the edge of the diagram downwards | Integer | 4 | Any Positive Value |
|
||||
*
|
||||
* **Notes:**
|
||||
*
|
||||
* Depending on css styling this might need adjustment.
|
||||
*
|
||||
* Default value: 1
|
||||
*/
|
||||
bottomMarginAdj: 1,
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ----------- | ----------- | ------- | -------- | ----------- |
|
||||
* | useMaxWidth | See notes | boolean | 4 | true, false |
|
||||
*
|
||||
* **Notes:**
|
||||
*
|
||||
* When this flag is set the height and width is set to 100% and is then scaling with the
|
||||
* available space if not the absolute space required is used.
|
||||
*
|
||||
* Default value: true
|
||||
*/
|
||||
useMaxWidth: true,
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ----------- | --------------------------------- | ---- | -------- | ----------- |
|
||||
* | rightAngles | Curved Arrows become Right Angles | 3 | 4 | true, false |
|
||||
*
|
||||
* **Notes:**
|
||||
*
|
||||
* This will display arrows that start and begin at the same node as right angles, rather than a
|
||||
* curves
|
||||
*
|
||||
* Default value: false
|
||||
*/
|
||||
rightAngles: false,
|
||||
taskFontSize: 14,
|
||||
taskFontFamily: '"Open Sans", sans-serif',
|
||||
taskMargin: 50,
|
||||
// width of activation box
|
||||
activationWidth: 10,
|
||||
|
||||
// text placement as: tspan | fo | old only text as before
|
||||
textPlacement: 'fo',
|
||||
actorColours: ['#8FBC8F', '#7CFC00', '#00FFFF', '#20B2AA', '#B0E0E6', '#FFFFE0'],
|
||||
|
||||
sectionFills: ['#191970', '#8B008B', '#4B0082', '#2F4F4F', '#800000', '#8B4513', '#00008B'],
|
||||
sectionColours: ['#fff'],
|
||||
disableMulticolor: false,
|
||||
},
|
||||
class: {
|
||||
/**
|
||||
* ### titleTopMargin
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { MermaidConfig } from '../config.type';
|
||||
import { log } from '../logger';
|
||||
import { DetectorRecord, DiagramDetector, DiagramLoader } from './types';
|
||||
import type {
|
||||
DetectorRecord,
|
||||
DiagramDetector,
|
||||
DiagramLoader,
|
||||
ExternalDiagramDefinition,
|
||||
} from './types';
|
||||
import { frontMatterRegex } from './frontmatter';
|
||||
|
||||
const directive = /%{2}{\s*(?:(\w+)\s*:|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi;
|
||||
@@ -42,6 +47,12 @@ export const detectType = function (text: string, config?: MermaidConfig): strin
|
||||
throw new Error(`No diagram type detected for text: ${text}`);
|
||||
};
|
||||
|
||||
export const registerLazyLoadedDiagrams = (...diagrams: ExternalDiagramDefinition[]) => {
|
||||
for (const { id, detector, loader } of diagrams) {
|
||||
addDetector(id, detector, loader);
|
||||
}
|
||||
};
|
||||
|
||||
export const addDetector = (key: string, detector: DiagramDetector, loader?: DiagramLoader) => {
|
||||
if (detectors[key]) {
|
||||
throw new Error(`Detector with key ${key} already exists`);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { registerDiagram } from './diagramAPI';
|
||||
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
import gitGraphParser from '../diagrams/git/parser/gitGraph';
|
||||
import { gitGraphDetector } from '../diagrams/git/gitGraphDetector';
|
||||
@@ -94,6 +93,10 @@ import { setConfig } from '../config';
|
||||
import errorRenderer from '../diagrams/error/errorRenderer';
|
||||
import errorStyles from '../diagrams/error/styles';
|
||||
|
||||
import flowchartElk from '../diagrams/flowchart/elk/detector';
|
||||
import { registerLazyLoadedDiagrams } from './detectType';
|
||||
|
||||
import timelineDetector from '../diagrams/timeline/detector';
|
||||
let hasLoadedDiagrams = false;
|
||||
export const addDiagrams = () => {
|
||||
if (hasLoadedDiagrams) {
|
||||
@@ -102,6 +105,8 @@ export const addDiagrams = () => {
|
||||
// This is added here to avoid race-conditions.
|
||||
// We could optimize the loading logic somehow.
|
||||
hasLoadedDiagrams = true;
|
||||
registerLazyLoadedDiagrams(flowchartElk, timelineDetector);
|
||||
|
||||
registerDiagram(
|
||||
'error',
|
||||
// Special diagram with error messages but setup as a regular diagram
|
||||
|
||||
@@ -5,6 +5,8 @@ import { sanitizeText as _sanitizeText } from '../diagrams/common/common';
|
||||
import { setupGraphViewbox as _setupGraphViewbox } from '../setupGraphViewbox';
|
||||
import { addStylesForDiagram } from '../styles';
|
||||
import { DiagramDefinition, DiagramDetector } from './types';
|
||||
import * as _commonDb from '../commonDb';
|
||||
import { parseDirective as _parseDirective } from '../directiveUtils';
|
||||
|
||||
/*
|
||||
Packaging and exposing resources for external diagrams so that they can import
|
||||
@@ -16,6 +18,11 @@ export const setLogLevel = _setLogLevel;
|
||||
export const getConfig = _getConfig;
|
||||
export const sanitizeText = (text: string) => _sanitizeText(text, getConfig());
|
||||
export const setupGraphViewbox = _setupGraphViewbox;
|
||||
export const getCommonDb = () => {
|
||||
return _commonDb;
|
||||
};
|
||||
export const parseDirective = (p: any, statement: string, context: string, type: string) =>
|
||||
_parseDirective(p, statement, context, type);
|
||||
|
||||
const diagrams: Record<string, DiagramDefinition> = {};
|
||||
export interface Detectors {
|
||||
@@ -46,7 +53,15 @@ export const registerDiagram = (
|
||||
addStylesForDiagram(id, diagram.styles);
|
||||
|
||||
if (diagram.injectUtils) {
|
||||
diagram.injectUtils(log, setLogLevel, getConfig, sanitizeText, setupGraphViewbox);
|
||||
diagram.injectUtils(
|
||||
log,
|
||||
setLogLevel,
|
||||
getConfig,
|
||||
sanitizeText,
|
||||
setupGraphViewbox,
|
||||
getCommonDb(),
|
||||
parseDirective
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ export interface InjectUtils {
|
||||
_getConfig: any;
|
||||
_sanitizeText: any;
|
||||
_setupGraphViewbox: any;
|
||||
_commonDb: any;
|
||||
_parseDirective: any;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,7 +31,9 @@ export interface DiagramDefinition {
|
||||
_setLogLevel: InjectUtils['_setLogLevel'],
|
||||
_getConfig: InjectUtils['_getConfig'],
|
||||
_sanitizeText: InjectUtils['_sanitizeText'],
|
||||
_setupGraphViewbox: InjectUtils['_setupGraphViewbox']
|
||||
_setupGraphViewbox: InjectUtils['_setupGraphViewbox'],
|
||||
_commonDb: InjectUtils['_commonDb'],
|
||||
_parseDirective: InjectUtils['_parseDirective']
|
||||
) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => {
|
||||
|
||||
// Check to see if any of the attributes has a key or a comment
|
||||
attributes.forEach((item) => {
|
||||
if (item.attributeKeyType !== undefined) {
|
||||
if (item.attributeKeyTypeList !== undefined && item.attributeKeyTypeList.length > 0) {
|
||||
hasKeyType = true;
|
||||
}
|
||||
|
||||
@@ -112,6 +112,9 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => {
|
||||
nodeHeight = Math.max(typeBBox.height, nameBBox.height);
|
||||
|
||||
if (hasKeyType) {
|
||||
const keyTypeNodeText =
|
||||
item.attributeKeyTypeList !== undefined ? item.attributeKeyTypeList.join(',') : '';
|
||||
|
||||
const keyTypeNode = groupNode
|
||||
.append('text')
|
||||
.classed('er entityLabel', true)
|
||||
@@ -122,7 +125,7 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => {
|
||||
.style('text-anchor', 'left')
|
||||
.style('font-family', getConfig().fontFamily)
|
||||
.style('font-size', attrFontSize + 'px')
|
||||
.text(item.attributeKeyType || '');
|
||||
.text(keyTypeNodeText);
|
||||
|
||||
attributeNode.kn = keyTypeNode;
|
||||
const keyTypeBBox = keyTypeNode.node().getBBox();
|
||||
|
||||
@@ -28,10 +28,11 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
||||
\"[^"]*\" return 'WORD';
|
||||
"erDiagram" return 'ER_DIAGRAM';
|
||||
"{" { this.begin("block"); return 'BLOCK_START'; }
|
||||
<block>"," return 'COMMA';
|
||||
<block>\s+ /* skip whitespace in block */
|
||||
<block>\b((?:PK)|(?:FK)|(?:UK))\b return 'ATTRIBUTE_KEY'
|
||||
<block>(.*?)[~](.*?)*[~] return 'ATTRIBUTE_WORD';
|
||||
<block>[A-Za-z][A-Za-z0-9\-_\[\]\(\)]* return 'ATTRIBUTE_WORD'
|
||||
<block>[A-Za-z_][A-Za-z0-9\-_\[\]\(\)]* return 'ATTRIBUTE_WORD'
|
||||
<block>\"[^"]*\" return 'COMMENT';
|
||||
<block>[\n]+ /* nothing */
|
||||
<block>"}" { this.popState(); return 'BLOCK_STOP'; }
|
||||
@@ -80,7 +81,7 @@ start
|
||||
|
||||
document
|
||||
: /* empty */ { $$ = [] }
|
||||
| document line {$1.push($2);$$ = $1}
|
||||
| document line {$1.push($2);$$ = $1}
|
||||
;
|
||||
|
||||
line
|
||||
@@ -131,11 +132,12 @@ attributes
|
||||
|
||||
attribute
|
||||
: attributeType attributeName { $$ = { attributeType: $1, attributeName: $2 }; }
|
||||
| attributeType attributeName attributeKeyType { $$ = { attributeType: $1, attributeName: $2, attributeKeyType: $3 }; }
|
||||
| attributeType attributeName attributeKeyTypeList { $$ = { attributeType: $1, attributeName: $2, attributeKeyTypeList: $3 }; }
|
||||
| attributeType attributeName attributeComment { $$ = { attributeType: $1, attributeName: $2, attributeComment: $3 }; }
|
||||
| attributeType attributeName attributeKeyType attributeComment { $$ = { attributeType: $1, attributeName: $2, attributeKeyType: $3, attributeComment: $4 }; }
|
||||
| attributeType attributeName attributeKeyTypeList attributeComment { $$ = { attributeType: $1, attributeName: $2, attributeKeyTypeList: $3, attributeComment: $4 }; }
|
||||
;
|
||||
|
||||
|
||||
attributeType
|
||||
: ATTRIBUTE_WORD { $$=$1; }
|
||||
;
|
||||
@@ -144,6 +146,11 @@ attributeName
|
||||
: ATTRIBUTE_WORD { $$=$1; }
|
||||
;
|
||||
|
||||
attributeKeyTypeList
|
||||
: attributeKeyType { $$ = [$1]; }
|
||||
| attributeKeyTypeList COMMA attributeKeyType { $1.push($3); $$ = $1; }
|
||||
;
|
||||
|
||||
attributeKeyType
|
||||
: ATTRIBUTE_KEY { $$=$1; }
|
||||
;
|
||||
|
||||
@@ -135,6 +135,37 @@ describe('when parsing ER diagram it...', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('attribute name', () => {
|
||||
it('should allow alphanumeric characters, dashes, underscores and brackets (not leading chars)', function () {
|
||||
const entity = 'BOOK';
|
||||
const attribute1 = 'string myBookTitle';
|
||||
const attribute2 = 'string MYBOOKSUBTITLE_1';
|
||||
const attribute3 = 'string author-ref[name](1)';
|
||||
|
||||
erDiagram.parser.parse(
|
||||
`erDiagram\n${entity} {\n${attribute1}\n${attribute2}\n${attribute3}\n}`
|
||||
);
|
||||
const entities = erDb.getEntities();
|
||||
|
||||
expect(Object.keys(entities).length).toBe(1);
|
||||
expect(entities[entity].attributes.length).toBe(3);
|
||||
expect(entities[entity].attributes[0].attributeName).toBe('myBookTitle');
|
||||
expect(entities[entity].attributes[1].attributeName).toBe('MYBOOKSUBTITLE_1');
|
||||
expect(entities[entity].attributes[2].attributeName).toBe('author-ref[name](1)');
|
||||
});
|
||||
|
||||
it('should not allow leading numbers, dashes or brackets', function () {
|
||||
const entity = 'BOOK';
|
||||
const nonLeadingChars = '0-[]()';
|
||||
[...nonLeadingChars].forEach((nonLeadingChar) => {
|
||||
expect(() => {
|
||||
const attribute = `string ${nonLeadingChar}author`;
|
||||
erDiagram.parser.parse(`erDiagram\n${entity} {\n${attribute}\n}`);
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow an entity with a single attribute to be defined', function () {
|
||||
const entity = 'BOOK';
|
||||
const attribute = 'string title';
|
||||
@@ -190,6 +221,28 @@ describe('when parsing ER diagram it...', function () {
|
||||
expect(entities[entity].attributes.length).toBe(4);
|
||||
});
|
||||
|
||||
it('should allow an entity with attributes that have many constraints and comments', function () {
|
||||
const entity = 'CUSTOMER';
|
||||
const attribute1 = 'int customer_number PK, FK "comment1"';
|
||||
const attribute2 = 'datetime customer_status_start_datetime PK,UK, FK';
|
||||
const attribute3 = 'datetime customer_status_end_datetime PK , UK "comment3"';
|
||||
const attribute4 = 'string customer_firstname';
|
||||
const attribute5 = 'string customer_lastname "comment5"';
|
||||
|
||||
erDiagram.parser.parse(
|
||||
`erDiagram\n${entity} {\n${attribute1}\n${attribute2}\n${attribute3}\n${attribute4}\n${attribute5}\n}`
|
||||
);
|
||||
const entities = erDb.getEntities();
|
||||
expect(entities[entity].attributes[0].attributeKeyTypeList).toEqual(['PK', 'FK']);
|
||||
expect(entities[entity].attributes[0].attributeComment).toBe('comment1');
|
||||
expect(entities[entity].attributes[1].attributeKeyTypeList).toEqual(['PK', 'UK', 'FK']);
|
||||
expect(entities[entity].attributes[2].attributeKeyTypeList).toEqual(['PK', 'UK']);
|
||||
expect(entities[entity].attributes[2].attributeComment).toBe('comment3');
|
||||
expect(entities[entity].attributes[3].attributeKeyTypeList).toBeUndefined();
|
||||
expect(entities[entity].attributes[4].attributeKeyTypeList).toBeUndefined();
|
||||
expect(entities[entity].attributes[4].attributeComment).toBe('comment5');
|
||||
});
|
||||
|
||||
it('should allow an entity with attribute that has a generic type', function () {
|
||||
const entity = 'BOOK';
|
||||
const attribute1 = 'type~T~ type';
|
||||
|
||||
55
packages/mermaid/src/diagrams/flowchart/elk/detector.spec.ts
Normal file
55
packages/mermaid/src/diagrams/flowchart/elk/detector.spec.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import plugin from './detector';
|
||||
import { describe, it } from 'vitest';
|
||||
|
||||
const { detector } = plugin;
|
||||
|
||||
describe('flowchart-elk detector', () => {
|
||||
it('should fail for dagre-d3', () => {
|
||||
expect(
|
||||
detector('flowchart', {
|
||||
flowchart: {
|
||||
defaultRenderer: 'dagre-d3',
|
||||
},
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
it('should fail for dagre-wrapper', () => {
|
||||
expect(
|
||||
detector('flowchart', {
|
||||
flowchart: {
|
||||
defaultRenderer: 'dagre-wrapper',
|
||||
},
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
it('should succeed for elk', () => {
|
||||
expect(
|
||||
detector('flowchart', {
|
||||
flowchart: {
|
||||
defaultRenderer: 'elk',
|
||||
},
|
||||
})
|
||||
).toBe(true);
|
||||
expect(
|
||||
detector('graph', {
|
||||
flowchart: {
|
||||
defaultRenderer: 'elk',
|
||||
},
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect flowchart-elk', () => {
|
||||
expect(detector('flowchart-elk')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not detect class with defaultRenderer set to elk', () => {
|
||||
expect(
|
||||
detector('class', {
|
||||
flowchart: {
|
||||
defaultRenderer: 'elk',
|
||||
},
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
29
packages/mermaid/src/diagrams/flowchart/elk/detector.ts
Normal file
29
packages/mermaid/src/diagrams/flowchart/elk/detector.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { MermaidConfig } from '../../../config.type';
|
||||
import type { ExternalDiagramDefinition, DiagramDetector } from '../../../diagram-api/types';
|
||||
|
||||
const id = 'flowchart-elk';
|
||||
|
||||
const detector: DiagramDetector = (txt: string, config?: MermaidConfig): boolean => {
|
||||
if (
|
||||
// If diagram explicitly states flowchart-elk
|
||||
txt.match(/^\s*flowchart-elk/) ||
|
||||
// If a flowchart/graph diagram has their default renderer set to elk
|
||||
(txt.match(/^\s*flowchart|graph/) && config?.flowchart?.defaultRenderer === 'elk')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const loader = async () => {
|
||||
const { diagram } = await import('./diagram-definition');
|
||||
return { id, diagram };
|
||||
};
|
||||
|
||||
const plugin: ExternalDiagramDefinition = {
|
||||
id,
|
||||
detector,
|
||||
loader,
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
@@ -0,0 +1,13 @@
|
||||
// @ts-ignore: JISON typing missing
|
||||
import parser from '../parser/flow';
|
||||
|
||||
import * as db from '../flowDb';
|
||||
import renderer from './flowRenderer-elk';
|
||||
import styles from './styles';
|
||||
|
||||
export const diagram = {
|
||||
db,
|
||||
renderer,
|
||||
parser,
|
||||
styles,
|
||||
};
|
||||
@@ -1,21 +1,15 @@
|
||||
import graphlib from 'graphlib';
|
||||
import { select, line, curveLinear, curveCardinal, curveBasis, selectAll } from 'd3';
|
||||
import { log, getConfig, setupGraphViewbox } from './mermaidUtils';
|
||||
import { insertNode } from '../../mermaid/src/dagre-wrapper/nodes.js';
|
||||
import insertMarkers from '../../mermaid/src/dagre-wrapper/markers.js';
|
||||
import createLabel from '../../mermaid/src/dagre-wrapper/createLabel';
|
||||
import { insertEdgeLabel, positionEdgeLabel } from '../../mermaid/src/dagre-wrapper/edges.js';
|
||||
import { select, line, curveLinear } from 'd3';
|
||||
import { insertNode } from '../../../dagre-wrapper/nodes.js';
|
||||
import insertMarkers from '../../../dagre-wrapper/markers.js';
|
||||
import { insertEdgeLabel } from '../../../dagre-wrapper/edges.js';
|
||||
import { findCommonAncestor } from './render-utils';
|
||||
// Replace with other function to avoid dependency to dagre-d3
|
||||
import { addHtmlLabel } from 'dagre-d3-es/src/dagre-js/label/add-html-label.js';
|
||||
import { getConfig } from '../../../config';
|
||||
import { log } from '../../../logger';
|
||||
import { setupGraphViewbox } from '../../../setupGraphViewbox';
|
||||
import common, { evaluate } from '../../common/common';
|
||||
import { interpolateToCurve, getStylesFromArray } from '../../../utils';
|
||||
|
||||
import common, { evaluate } from '../../mermaid/src/diagrams/common/common';
|
||||
import { interpolateToCurve, getStylesFromArray } from '../../mermaid/src/utils';
|
||||
|
||||
// import ELK from 'elkjs/lib/elk-api';
|
||||
// const elk = new ELK({
|
||||
// workerUrl: './elk-worker.min.js',
|
||||
// });
|
||||
import ELK from 'elkjs/lib/elk.bundled.js';
|
||||
const elk = new ELK();
|
||||
|
||||
@@ -195,8 +189,8 @@ export const addVertices = function (vert, svgId, root, doc, diagObj, parentLook
|
||||
labelText: vertexText,
|
||||
labelData,
|
||||
// labels: [{ text: vertexText }],
|
||||
// rx: radious,
|
||||
// ry: radious,
|
||||
// rx: radius,
|
||||
// ry: radius,
|
||||
// class: classStr,
|
||||
// style: styles.style,
|
||||
// link: vertex.link,
|
||||
@@ -224,8 +218,8 @@ export const addVertices = function (vert, svgId, root, doc, diagObj, parentLook
|
||||
// labelStyle: styles.labelStyle,
|
||||
// shape: _shape,
|
||||
// labelText: vertexText,
|
||||
// rx: radious,
|
||||
// ry: radious,
|
||||
// rx: radius,
|
||||
// ry: radius,
|
||||
// class: classStr,
|
||||
// style: styles.style,
|
||||
// id: vertex.id,
|
||||
@@ -254,7 +248,6 @@ export const addVertices = function (vert, svgId, root, doc, diagObj, parentLook
|
||||
export const addEdges = function (edges, diagObj, graph, svg) {
|
||||
// log.info('abc78 edges = ', edges);
|
||||
const labelsEl = svg.insert('g').attr('class', 'edgeLabels');
|
||||
let cnt = 0;
|
||||
let linkIdCnt = {};
|
||||
|
||||
let defaultStyle;
|
||||
@@ -267,8 +260,6 @@ export const addEdges = function (edges, diagObj, graph, svg) {
|
||||
}
|
||||
|
||||
edges.forEach(function (edge) {
|
||||
cnt++;
|
||||
|
||||
// Identify Link
|
||||
var linkIdBase = 'L-' + edge.start + '-' + edge.end;
|
||||
// count the links from+to the same node to give unique id
|
||||
@@ -382,7 +373,6 @@ export const addEdges = function (edges, diagObj, graph, svg) {
|
||||
edgeData.id = linkId;
|
||||
edgeData.classes = 'flowchart-link ' + linkNameStart + ' ' + linkNameEnd;
|
||||
|
||||
const edgesNode = select(edges);
|
||||
const labelEl = insertEdgeLabel(labelsEl, edgeData);
|
||||
// console.log('labelEl', labelEl, edgeData.width);
|
||||
// Add the edge to the graph
|
||||
@@ -515,7 +505,7 @@ export const getClasses = function (text, diagObj) {
|
||||
diagObj.parse(text);
|
||||
return diagObj.db.getClasses();
|
||||
} catch (e) {
|
||||
return;
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -542,45 +532,17 @@ const addSubGraphs = function (db) {
|
||||
return parentLookupDb;
|
||||
};
|
||||
|
||||
/* Reverse engineered with trial and error */
|
||||
const calcOffsetOld = function (src, dest, sourceId, targetId, srcDepth, targetDepth, so, to) {
|
||||
// if (src === dest) {
|
||||
// return src;
|
||||
// }
|
||||
// if (sourceId === 'B6') {
|
||||
// return 0;
|
||||
// }
|
||||
// if (sourceId === 'B4') {
|
||||
// return 318;
|
||||
// }
|
||||
if (srcDepth < targetDepth) {
|
||||
return src;
|
||||
}
|
||||
if (srcDepth > targetDepth) {
|
||||
return dest;
|
||||
}
|
||||
if (srcDepth === targetDepth) {
|
||||
return src;
|
||||
}
|
||||
// if (src < dest) {
|
||||
// return dest + src;
|
||||
// }
|
||||
return 0;
|
||||
};
|
||||
|
||||
const calcOffset = function (src, dest, parentLookupDb) {
|
||||
const ancestor = findCommonAncestor(src, dest, parentLookupDb);
|
||||
if (ancestor === undefined || ancestor === 'root') {
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
const ancestoprOffset = nodeDb[ancestor].offset;
|
||||
return { x: ancestoprOffset.posX, y: ancestoprOffset.posY };
|
||||
const ancestorOffset = nodeDb[ancestor].offset;
|
||||
return { x: ancestorOffset.posX, y: ancestorOffset.posY };
|
||||
};
|
||||
|
||||
const insertEdge = function (edgesEl, edge, edgeData, diagObj, parentLookupDb) {
|
||||
const srcOffset = nodeDb[edge.sources[0]].offset;
|
||||
const targetOffset = nodeDb[edge.targets[0]].offset;
|
||||
const offset = calcOffset(edge.sources[0], edge.targets[0], parentLookupDb);
|
||||
|
||||
const src = edge.sections[0].startPoint;
|
||||
@@ -647,7 +609,7 @@ const insertChildren = (nodeArray, parentLookupDb) => {
|
||||
* @param id
|
||||
*/
|
||||
|
||||
export const draw = function (text, id, _version, diagObj) {
|
||||
export const draw = async function (text, id, _version, diagObj) {
|
||||
// Add temporary render element
|
||||
diagObj.db.clear();
|
||||
nodeDb = {};
|
||||
@@ -655,149 +617,128 @@ export const draw = function (text, id, _version, diagObj) {
|
||||
// Parse the graph definition
|
||||
diagObj.parser.parse(text);
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
const renderEl = select('body').append('div').attr('style', 'height:400px').attr('id', 'cy');
|
||||
// .attr('style', 'display:none')
|
||||
let graph = {
|
||||
id: 'root',
|
||||
layoutOptions: {
|
||||
'elk.hierarchyHandling': 'INCLUDE_CHILDREN',
|
||||
// 'elk.hierarchyHandling': 'SEPARATE_CHILDREN',
|
||||
'org.eclipse.elk.padding': '[top=100, left=100, bottom=110, right=110]',
|
||||
// 'org.eclipse.elk.layered.spacing.nodeNodeBetweenLayers': 120,
|
||||
// 'elk.layered.spacing.nodeNodeBetweenLayers': '140',
|
||||
'elk.layered.spacing.edgeNodeBetweenLayers': '30',
|
||||
// 'elk.algorithm': 'layered',
|
||||
'elk.direction': 'DOWN',
|
||||
// 'elk.port.side': 'SOUTH',
|
||||
// 'nodePlacement.strategy': 'SIMPLE',
|
||||
// 'org.eclipse.elk.spacing.labelLabel': 120,
|
||||
// 'org.eclipse.elk.graphviz.concentrate': true,
|
||||
// 'org.eclipse.elk.spacing.nodeNode': 120,
|
||||
// 'org.eclipse.elk.spacing.edgeEdge': 120,
|
||||
// 'org.eclipse.elk.spacing.edgeNode': 120,
|
||||
// 'org.eclipse.elk.spacing.nodeEdge': 120,
|
||||
// 'org.eclipse.elk.spacing.componentComponent': 120,
|
||||
},
|
||||
children: [],
|
||||
edges: [],
|
||||
};
|
||||
log.info('Drawing flowchart using v3 renderer');
|
||||
const renderEl = select('body').append('div').attr('style', 'height:400px').attr('id', 'cy');
|
||||
let graph = {
|
||||
id: 'root',
|
||||
layoutOptions: {
|
||||
'elk.hierarchyHandling': 'INCLUDE_CHILDREN',
|
||||
'org.eclipse.elk.padding': '[top=100, left=100, bottom=110, right=110]',
|
||||
'elk.layered.spacing.edgeNodeBetweenLayers': '30',
|
||||
'elk.direction': 'DOWN',
|
||||
},
|
||||
children: [],
|
||||
edges: [],
|
||||
};
|
||||
log.info('Drawing flowchart using v3 renderer');
|
||||
|
||||
// Set the direction,
|
||||
// Fetch the default direction, use TD if none was found
|
||||
let dir = diagObj.db.getDirection();
|
||||
switch (dir) {
|
||||
case 'BT':
|
||||
graph.layoutOptions['elk.direction'] = 'UP';
|
||||
break;
|
||||
case 'TB':
|
||||
graph.layoutOptions['elk.direction'] = 'DOWN';
|
||||
break;
|
||||
case 'LR':
|
||||
graph.layoutOptions['elk.direction'] = 'RIGHT';
|
||||
break;
|
||||
case 'RL':
|
||||
graph.layoutOptions['elk.direction'] = 'LEFT';
|
||||
break;
|
||||
// Set the direction,
|
||||
// Fetch the default direction, use TD if none was found
|
||||
let dir = diagObj.db.getDirection();
|
||||
switch (dir) {
|
||||
case 'BT':
|
||||
graph.layoutOptions['elk.direction'] = 'UP';
|
||||
break;
|
||||
case 'TB':
|
||||
graph.layoutOptions['elk.direction'] = 'DOWN';
|
||||
break;
|
||||
case 'LR':
|
||||
graph.layoutOptions['elk.direction'] = 'RIGHT';
|
||||
break;
|
||||
case 'RL':
|
||||
graph.layoutOptions['elk.direction'] = 'LEFT';
|
||||
break;
|
||||
}
|
||||
const { securityLevel, flowchart: conf } = getConfig();
|
||||
|
||||
// Find the root dom node to ne used in rendering
|
||||
// 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;
|
||||
|
||||
const svg = root.select(`[id="${id}"]`);
|
||||
|
||||
// Define the supported markers for the diagram
|
||||
const markers = ['point', 'circle', 'cross'];
|
||||
|
||||
// Add the marker definitions to the svg as marker tags
|
||||
insertMarkers(svg, markers, diagObj.type, diagObj.arrowMarkerAbsolute);
|
||||
|
||||
// Fetch the vertices/nodes and edges/links from the parsed graph definition
|
||||
const vert = diagObj.db.getVertices();
|
||||
|
||||
// Setup nodes from the subgraphs with type group, these will be used
|
||||
// as nodes with children in the subgraph
|
||||
let subG;
|
||||
const subGraphs = diagObj.db.getSubGraphs();
|
||||
log.info('Subgraphs - ', subGraphs);
|
||||
for (let i = subGraphs.length - 1; i >= 0; i--) {
|
||||
subG = subGraphs[i];
|
||||
diagObj.db.addVertex(subG.id, subG.title, 'group', undefined, subG.classes, subG.dir);
|
||||
}
|
||||
|
||||
// Add an element in the svg to be used to hold the subgraphs container
|
||||
// elements
|
||||
const subGraphsEl = svg.insert('g').attr('class', 'subgraphs');
|
||||
|
||||
// Create the lookup db for the subgraphs and their children to used when creating
|
||||
// the tree structured graph
|
||||
const parentLookupDb = addSubGraphs(diagObj.db);
|
||||
|
||||
// Add the nodes to the graph, this will entail creating the actual nodes
|
||||
// in order to get the size of the node. You can't get the size of a node
|
||||
// that is not in the dom so we need to add it to the dom, get the size
|
||||
// we will position the nodes when we get the layout from elkjs
|
||||
graph = addVertices(vert, id, root, doc, diagObj, parentLookupDb, graph);
|
||||
|
||||
// Time for the edges, we start with adding an element in the node to hold the edges
|
||||
const edgesEl = svg.insert('g').attr('class', 'edges edgePath');
|
||||
// Fetch the edges form the parsed graph definition
|
||||
const edges = diagObj.db.getEdges();
|
||||
|
||||
// Add the edges to the graph, this will entail creating the actual edges
|
||||
graph = addEdges(edges, diagObj, graph, svg);
|
||||
|
||||
// Iterate through all nodes and add the top level nodes to the graph
|
||||
const nodes = Object.keys(nodeDb);
|
||||
nodes.forEach((nodeId) => {
|
||||
const node = nodeDb[nodeId];
|
||||
if (!node.parent) {
|
||||
graph.children.push(node);
|
||||
}
|
||||
const { securityLevel, flowchart: conf } = getConfig();
|
||||
|
||||
// Find the root dom node to ne used in rendering
|
||||
// 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;
|
||||
|
||||
const svg = root.select(`[id="${id}"]`);
|
||||
|
||||
// Define the supported markers for the diagram
|
||||
const markers = ['point', 'circle', 'cross'];
|
||||
|
||||
// Add the marker definitions to the svg as marker tags
|
||||
insertMarkers(svg, markers, diagObj.type, diagObj.arrowMarkerAbsolute);
|
||||
|
||||
// Fetch the vertices/nodes and edges/links from the parsed graph definition
|
||||
const vert = diagObj.db.getVertices();
|
||||
|
||||
// Setup nodes from the subgraphs with type group, these will be used
|
||||
// as nodes with children in the subgraph
|
||||
let subG;
|
||||
const subGraphs = diagObj.db.getSubGraphs();
|
||||
log.info('Subgraphs - ', subGraphs);
|
||||
for (let i = subGraphs.length - 1; i >= 0; i--) {
|
||||
subG = subGraphs[i];
|
||||
diagObj.db.addVertex(subG.id, subG.title, 'group', undefined, subG.classes, subG.dir);
|
||||
}
|
||||
|
||||
// Add an element in the svg to be used to hold the subgraphs container
|
||||
// elements
|
||||
const subGraphsEl = svg.insert('g').attr('class', 'subgraphs');
|
||||
|
||||
// Create the lookup db for the subgraphs and their children to used when creating
|
||||
// the tree structured graph
|
||||
const parentLookupDb = addSubGraphs(diagObj.db);
|
||||
|
||||
// Add the nodes to the graph, this will entail creating the actual nodes
|
||||
// in order to get the size of the node. You can't get the size of a node
|
||||
// that is not in the dom so we need to add it to the dom, get the size
|
||||
// we will position the nodes when we get the layout from elkjs
|
||||
graph = addVertices(vert, id, root, doc, diagObj, parentLookupDb, graph);
|
||||
|
||||
// Time for the edges, we start with adding an element in the node to hold the edges
|
||||
const edgesEl = svg.insert('g').attr('class', 'edges edgePath');
|
||||
// Fetch the edges form the parsed graph definition
|
||||
const edges = diagObj.db.getEdges();
|
||||
|
||||
// Add the edges to the graph, this will entail creating the actual edges
|
||||
graph = addEdges(edges, diagObj, graph, svg);
|
||||
|
||||
// Iterate through all nodes and add the top level nodes to the graph
|
||||
const nodes = Object.keys(nodeDb);
|
||||
nodes.forEach((nodeId) => {
|
||||
const node = nodeDb[nodeId];
|
||||
if (!node.parent) {
|
||||
graph.children.push(node);
|
||||
}
|
||||
// node.nodePadding = [120, 50, 50, 50];
|
||||
// node['org.eclipse.elk.spacing.nodeNode'] = 120;
|
||||
// Subgraph
|
||||
if (parentLookupDb.childrenById[nodeId] !== undefined) {
|
||||
node.labels = [
|
||||
{
|
||||
text: node.labelText,
|
||||
layoutOptions: {
|
||||
'nodeLabels.placement': '[H_CENTER, V_TOP, INSIDE]',
|
||||
},
|
||||
width: node.labelData.width,
|
||||
height: node.labelData.height,
|
||||
// Subgraph
|
||||
if (parentLookupDb.childrenById[nodeId] !== undefined) {
|
||||
node.labels = [
|
||||
{
|
||||
text: node.labelText,
|
||||
layoutOptions: {
|
||||
'nodeLabels.placement': '[H_CENTER, V_TOP, INSIDE]',
|
||||
},
|
||||
];
|
||||
delete node.x;
|
||||
delete node.y;
|
||||
delete node.width;
|
||||
delete node.height;
|
||||
}
|
||||
});
|
||||
insertChildren(graph.children, parentLookupDb);
|
||||
elk.layout(graph).then(function (g) {
|
||||
drawNodes(0, 0, g.children, svg, subGraphsEl, diagObj, 0);
|
||||
|
||||
g.edges.map((edge, id) => {
|
||||
insertEdge(edgesEl, edge, edge.edgeData, diagObj, parentLookupDb);
|
||||
});
|
||||
setupGraphViewbox({}, svg, conf.diagramPadding, conf.useMaxWidth);
|
||||
resolve();
|
||||
});
|
||||
// Remove element after layout
|
||||
renderEl.remove();
|
||||
width: node.labelData.width,
|
||||
height: node.labelData.height,
|
||||
},
|
||||
];
|
||||
delete node.x;
|
||||
delete node.y;
|
||||
delete node.width;
|
||||
delete node.height;
|
||||
}
|
||||
});
|
||||
insertChildren(graph.children, parentLookupDb);
|
||||
const g = await elk.layout(graph);
|
||||
drawNodes(0, 0, g.children, svg, subGraphsEl, diagObj, 0);
|
||||
g.edges?.map((edge) => {
|
||||
insertEdge(edgesEl, edge, edge.edgeData, diagObj, parentLookupDb);
|
||||
});
|
||||
setupGraphViewbox({}, svg, conf.diagramPadding, conf.useMaxWidth);
|
||||
// Remove element after layout
|
||||
renderEl.remove();
|
||||
};
|
||||
|
||||
const drawNodes = (relX, relY, nodeArray, svg, subgraphsEl, diagObj, depth) => {
|
||||
@@ -846,9 +787,6 @@ const drawNodes = (relX, relY, nodeArray, svg, subgraphsEl, diagObj, depth) => {
|
||||
};
|
||||
|
||||
export default {
|
||||
// setConf,
|
||||
// addVertices,
|
||||
// addEdges,
|
||||
getClasses,
|
||||
draw,
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { findCommonAncestor, TreeData } from './render-utils';
|
||||
describe('when rendering a flowchart using elk ', () => {
|
||||
let lookupDb: TreeData;
|
||||
beforeEach(() => {
|
||||
lookupDb = {
|
||||
parentById: {
|
||||
B4: 'inner',
|
||||
B5: 'inner',
|
||||
C4: 'inner2',
|
||||
C5: 'inner2',
|
||||
B2: 'Ugge',
|
||||
B3: 'Ugge',
|
||||
inner: 'Ugge',
|
||||
inner2: 'Ugge',
|
||||
B6: 'outer',
|
||||
},
|
||||
childrenById: {
|
||||
inner: ['B4', 'B5'],
|
||||
inner2: ['C4', 'C5'],
|
||||
Ugge: ['B2', 'B3', 'inner', 'inner2'],
|
||||
outer: ['B6'],
|
||||
},
|
||||
};
|
||||
});
|
||||
it('to find parent of siblings in a subgraph', () => {
|
||||
expect(findCommonAncestor('B4', 'B5', lookupDb)).toBe('inner');
|
||||
});
|
||||
it('to find an uncle', () => {
|
||||
expect(findCommonAncestor('B4', 'B2', lookupDb)).toBe('Ugge');
|
||||
});
|
||||
it('to find a cousin', () => {
|
||||
expect(findCommonAncestor('B4', 'C4', lookupDb)).toBe('Ugge');
|
||||
});
|
||||
it('to find a grandparent', () => {
|
||||
expect(findCommonAncestor('B4', 'B6', lookupDb)).toBe('root');
|
||||
});
|
||||
it('to find ancestor of siblings in the root', () => {
|
||||
expect(findCommonAncestor('B1', 'outer', lookupDb)).toBe('root');
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,9 @@
|
||||
export const findCommonAncestor = (id1, id2, treeData) => {
|
||||
export interface TreeData {
|
||||
parentById: Record<string, string>;
|
||||
childrenById: Record<string, string[]>;
|
||||
}
|
||||
|
||||
export const findCommonAncestor = (id1: string, id2: string, treeData: TreeData) => {
|
||||
const { parentById } = treeData;
|
||||
const visited = new Set();
|
||||
let currentId = id1;
|
||||
@@ -13,9 +13,10 @@ export interface FlowChartStyleOptions {
|
||||
tertiaryColor: string;
|
||||
textColor: string;
|
||||
titleColor: string;
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
const genSections = (options) => {
|
||||
const genSections = (options: FlowChartStyleOptions) => {
|
||||
let sections = '';
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
@@ -82,6 +82,7 @@ that id.
|
||||
<click>[\s\n] this.popState();
|
||||
<click>[^\s\n]* return 'CLICK';
|
||||
|
||||
"flowchart-elk" {if(yy.lex.firstGraph()){this.begin("dir");} return 'GRAPH';}
|
||||
"graph" {if(yy.lex.firstGraph()){this.begin("dir");} return 'GRAPH';}
|
||||
"flowchart" {if(yy.lex.firstGraph()){this.begin("dir");} return 'GRAPH';}
|
||||
"subgraph" return 'subgraph';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import moment from 'moment-mini';
|
||||
import moment from 'moment';
|
||||
import { sanitizeUrl } from '@braintree/sanitize-url';
|
||||
import { log } from '../../logger';
|
||||
import * as configApi from '../../config';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @ts-nocheck TODO: Fix TS
|
||||
import moment from 'moment-mini';
|
||||
import moment from 'moment';
|
||||
import ganttDb from './ganttDb';
|
||||
import { convert } from '../../tests/util';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import moment from 'moment-mini';
|
||||
import moment from 'moment';
|
||||
import { log } from '../../logger';
|
||||
import {
|
||||
select,
|
||||
|
||||
@@ -35,8 +35,9 @@
|
||||
\%%(?!\{)[^\n]* /* skip comments */
|
||||
[^\}]\%\%[^\n]* /* skip comments */
|
||||
[0-9]+(?=[ \n]+) return 'NUM';
|
||||
"box" { this.begin('LINE'); return 'box'; }
|
||||
"participant" { this.begin('ID'); return 'participant'; }
|
||||
"actor" { this.begin('ID'); return 'participant_actor'; }
|
||||
"actor" { this.begin('ID'); return 'participant_actor'; }
|
||||
<ID>[^\->:\n,;]+?([\-]*[^\->:\n,;]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
|
||||
<ALIAS>"as" { this.popState(); this.popState(); this.begin('LINE'); return 'AS'; }
|
||||
<ALIAS>(?:) { this.popState(); this.popState(); return 'NEWLINE'; }
|
||||
@@ -117,16 +118,30 @@ line
|
||||
| NEWLINE { $$=[]; }
|
||||
;
|
||||
|
||||
box_section
|
||||
: /* empty */ { $$ = [] }
|
||||
| box_section box_line {$1.push($2);$$ = $1}
|
||||
;
|
||||
|
||||
box_line
|
||||
: SPACE participant_statement { $$ = $2 }
|
||||
| participant_statement { $$ = $1 }
|
||||
| NEWLINE { $$=[]; }
|
||||
;
|
||||
|
||||
|
||||
directive
|
||||
: openDirective typeDirective closeDirective 'NEWLINE'
|
||||
| openDirective typeDirective ':' argDirective closeDirective 'NEWLINE'
|
||||
;
|
||||
|
||||
statement
|
||||
: 'participant' actor 'AS' restOfLine 'NEWLINE' {$2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
|
||||
| 'participant' actor 'NEWLINE' {$2.type='addParticipant';$$=$2;}
|
||||
| 'participant_actor' actor 'AS' restOfLine 'NEWLINE' {$2.type='addActor';$2.description=yy.parseMessage($4); $$=$2;}
|
||||
| 'participant_actor' actor 'NEWLINE' {$2.type='addActor'; $$=$2;}
|
||||
: participant_statement
|
||||
| 'box' restOfLine box_section end
|
||||
{
|
||||
$3.unshift({type: 'boxStart', boxData:yy.parseBoxData($2) });
|
||||
$3.push({type: 'boxEnd', boxText:$2});
|
||||
$$=$3;}
|
||||
| signal 'NEWLINE'
|
||||
| autonumber NUM NUM 'NEWLINE' { $$= {type:'sequenceIndex',sequenceIndex: Number($2), sequenceIndexStep:Number($3), sequenceVisible:true, signalType:yy.LINETYPE.AUTONUMBER};}
|
||||
| autonumber NUM 'NEWLINE' { $$ = {type:'sequenceIndex',sequenceIndex: Number($2), sequenceIndexStep:1, sequenceVisible:true, signalType:yy.LINETYPE.AUTONUMBER};}
|
||||
@@ -209,6 +224,13 @@ else_sections
|
||||
{ $$ = $1.concat([{type: 'else', altText:yy.parseMessage($3), signalType: yy.LINETYPE.ALT_ELSE}, $4]); }
|
||||
;
|
||||
|
||||
participant_statement
|
||||
: 'participant' actor 'AS' restOfLine 'NEWLINE' {$2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
|
||||
| 'participant' actor 'NEWLINE' {$2.type='addParticipant';$$=$2;}
|
||||
| 'participant_actor' actor 'AS' restOfLine 'NEWLINE' {$2.type='addActor';$2.description=yy.parseMessage($4); $$=$2;}
|
||||
| 'participant_actor' actor 'NEWLINE' {$2.type='addActor'; $$=$2;}
|
||||
;
|
||||
|
||||
note_statement
|
||||
: 'note' placement actor text2
|
||||
{
|
||||
|
||||
@@ -14,20 +14,52 @@ import {
|
||||
|
||||
let prevActor = undefined;
|
||||
let actors = {};
|
||||
let boxes = [];
|
||||
let messages = [];
|
||||
const notes = [];
|
||||
let sequenceNumbersEnabled = false;
|
||||
let wrapEnabled;
|
||||
let currentBox = undefined;
|
||||
|
||||
export const parseDirective = function (statement, context, type) {
|
||||
mermaidAPI.parseDirective(this, statement, context, type);
|
||||
};
|
||||
|
||||
export const addBox = function (data) {
|
||||
boxes.push({
|
||||
name: data.text,
|
||||
wrap: (data.wrap === undefined && autoWrap()) || !!data.wrap,
|
||||
fill: data.color,
|
||||
actorKeys: [],
|
||||
});
|
||||
currentBox = boxes.slice(-1)[0];
|
||||
};
|
||||
|
||||
export const addActor = function (id, name, description, type) {
|
||||
// Don't allow description nulling
|
||||
let assignedBox = currentBox;
|
||||
const old = actors[id];
|
||||
if (old && name === old.name && description == null) {
|
||||
return;
|
||||
if (old) {
|
||||
// If already set and trying to set to a new one throw error
|
||||
if (currentBox && old.box && currentBox !== old.box) {
|
||||
throw new Error(
|
||||
'A same participant should only be defined in one Box: ' +
|
||||
old.name +
|
||||
" can't be in '" +
|
||||
old.box.name +
|
||||
"' and in '" +
|
||||
currentBox.name +
|
||||
"' at the same time."
|
||||
);
|
||||
}
|
||||
|
||||
// Don't change the box if already
|
||||
assignedBox = old.box ? old.box : currentBox;
|
||||
old.box = assignedBox;
|
||||
|
||||
// Don't allow description nulling
|
||||
if (old && name === old.name && description == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't allow null descriptions, either
|
||||
@@ -39,6 +71,7 @@ export const addActor = function (id, name, description, type) {
|
||||
}
|
||||
|
||||
actors[id] = {
|
||||
box: assignedBox,
|
||||
name: name,
|
||||
description: description.text,
|
||||
wrap: (description.wrap === undefined && autoWrap()) || !!description.wrap,
|
||||
@@ -53,6 +86,9 @@ export const addActor = function (id, name, description, type) {
|
||||
actors[prevActor].nextActor = id;
|
||||
}
|
||||
|
||||
if (currentBox) {
|
||||
currentBox.actorKeys.push(id);
|
||||
}
|
||||
prevActor = id;
|
||||
};
|
||||
|
||||
@@ -111,10 +147,21 @@ export const addSignal = function (
|
||||
return true;
|
||||
};
|
||||
|
||||
export const hasAtLeastOneBox = function () {
|
||||
return boxes.length > 0;
|
||||
};
|
||||
|
||||
export const hasAtLeastOneBoxWithTitle = function () {
|
||||
return boxes.some((b) => b.name);
|
||||
};
|
||||
|
||||
export const getMessages = function () {
|
||||
return messages;
|
||||
};
|
||||
|
||||
export const getBoxes = function () {
|
||||
return boxes;
|
||||
};
|
||||
export const getActors = function () {
|
||||
return actors;
|
||||
};
|
||||
@@ -147,6 +194,7 @@ export const autoWrap = () => {
|
||||
|
||||
export const clear = function () {
|
||||
actors = {};
|
||||
boxes = [];
|
||||
messages = [];
|
||||
sequenceNumbersEnabled = false;
|
||||
commonClear();
|
||||
@@ -167,6 +215,47 @@ export const parseMessage = function (str) {
|
||||
return message;
|
||||
};
|
||||
|
||||
// We expect the box statement to be color first then description
|
||||
// The color can be rgb,rgba,hsl,hsla, or css code names #hex codes are not supported for now because of the way the char # is handled
|
||||
// We extract first segment as color, the rest of the line is considered as text
|
||||
export const parseBoxData = function (str) {
|
||||
const match = str.match(/^((?:rgba?|hsla?)\s*\(.*\)|\w*)(.*)$/);
|
||||
let color = match != null && match[1] ? match[1].trim() : 'transparent';
|
||||
let title = match != null && match[2] ? match[2].trim() : undefined;
|
||||
|
||||
// check that the string is a color
|
||||
if (window && window.CSS) {
|
||||
if (!window.CSS.supports('color', color)) {
|
||||
color = 'transparent';
|
||||
title = str.trim();
|
||||
}
|
||||
} else {
|
||||
const style = new Option().style;
|
||||
style.color = color;
|
||||
if (style.color !== color) {
|
||||
color = 'transparent';
|
||||
title = str.trim();
|
||||
}
|
||||
}
|
||||
|
||||
const boxData = {
|
||||
color: color,
|
||||
text:
|
||||
title !== undefined
|
||||
? sanitizeText(title.replace(/^:?(?:no)?wrap:/, ''), configApi.getConfig())
|
||||
: undefined,
|
||||
wrap:
|
||||
title !== undefined
|
||||
? title.match(/^:?wrap:/) !== null
|
||||
? true
|
||||
: title.match(/^:?nowrap:/) !== null
|
||||
? false
|
||||
: undefined
|
||||
: undefined,
|
||||
};
|
||||
return boxData;
|
||||
};
|
||||
|
||||
export const LINETYPE = {
|
||||
SOLID: 0,
|
||||
DOTTED: 1,
|
||||
@@ -311,6 +400,13 @@ function insertProperties(actor, properties) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function boxEnd() {
|
||||
currentBox = undefined;
|
||||
}
|
||||
|
||||
export const addDetails = function (actorId, text) {
|
||||
// find the actor
|
||||
const actor = getActor(actorId);
|
||||
@@ -391,6 +487,12 @@ export const apply = function (param) {
|
||||
case 'addMessage':
|
||||
addSignal(param.from, param.to, param.msg, param.signalType);
|
||||
break;
|
||||
case 'boxStart':
|
||||
addBox(param.boxData);
|
||||
break;
|
||||
case 'boxEnd':
|
||||
boxEnd();
|
||||
break;
|
||||
case 'loopStart':
|
||||
addSignal(undefined, undefined, param.loopText, param.signalType);
|
||||
break;
|
||||
@@ -467,12 +569,14 @@ export default {
|
||||
getActorKeys,
|
||||
getActorProperty,
|
||||
getAccTitle,
|
||||
getBoxes,
|
||||
getDiagramTitle,
|
||||
setDiagramTitle,
|
||||
parseDirective,
|
||||
getConfig: () => configApi.getConfig().sequence,
|
||||
clear,
|
||||
parseMessage,
|
||||
parseBoxData,
|
||||
LINETYPE,
|
||||
ARROWTYPE,
|
||||
PLACEMENT,
|
||||
@@ -481,4 +585,6 @@ export default {
|
||||
apply,
|
||||
setAccDescription,
|
||||
getAccDescription,
|
||||
hasAtLeastOneBox,
|
||||
hasAtLeastOneBoxWithTitle,
|
||||
};
|
||||
|
||||
@@ -52,8 +52,16 @@ vi.mock('d3', () => {
|
||||
curveBasis: 'basis',
|
||||
curveBasisClosed: 'basisClosed',
|
||||
curveBasisOpen: 'basisOpen',
|
||||
curveLinear: 'linear',
|
||||
curveBumpX: 'bumpX',
|
||||
curveBumpY: 'bumpY',
|
||||
curveBundle: 'bundle',
|
||||
curveCardinalClosed: 'cardinalClosed',
|
||||
curveCardinalOpen: 'cardinalOpen',
|
||||
curveCardinal: 'cardinal',
|
||||
curveCatmullRomClosed: 'catmullRomClosed',
|
||||
curveCatmullRomOpen: 'catmullRomOpen',
|
||||
curveCatmullRom: 'catmullRom',
|
||||
curveLinear: 'linear',
|
||||
curveLinearClosed: 'linearClosed',
|
||||
curveMonotoneX: 'monotoneX',
|
||||
curveMonotoneY: 'monotoneY',
|
||||
@@ -1299,8 +1307,76 @@ properties b: {"class": "external-service-actor", "icon": "@computer"}
|
||||
expect(actors.b.properties['icon']).toBe('@computer');
|
||||
expect(actors.c.properties['class']).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle box', function () {
|
||||
const str = `
|
||||
sequenceDiagram
|
||||
box green Group 1
|
||||
participant a as Alice
|
||||
participant b as Bob
|
||||
end
|
||||
participant c as Charlie
|
||||
links a: { "Repo": "https://repo.contoso.com/", "Dashboard": "https://dashboard.contoso.com/" }
|
||||
links b: { "Dashboard": "https://dashboard.contoso.com/" }
|
||||
links a: { "On-Call": "https://oncall.contoso.com/?svc=alice" }
|
||||
link a: Endpoint @ https://alice.contoso.com
|
||||
link a: Swagger @ https://swagger.contoso.com
|
||||
link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com
|
||||
`;
|
||||
|
||||
mermaidAPI.parse(str);
|
||||
const boxes = diagram.db.getBoxes();
|
||||
expect(boxes[0].name).toEqual('Group 1');
|
||||
expect(boxes[0].actorKeys).toEqual(['a', 'b']);
|
||||
expect(boxes[0].fill).toEqual('green');
|
||||
});
|
||||
|
||||
it('should handle box without color', function () {
|
||||
const str = `
|
||||
sequenceDiagram
|
||||
box Group 1
|
||||
participant a as Alice
|
||||
participant b as Bob
|
||||
end
|
||||
participant c as Charlie
|
||||
links a: { "Repo": "https://repo.contoso.com/", "Dashboard": "https://dashboard.contoso.com/" }
|
||||
links b: { "Dashboard": "https://dashboard.contoso.com/" }
|
||||
links a: { "On-Call": "https://oncall.contoso.com/?svc=alice" }
|
||||
link a: Endpoint @ https://alice.contoso.com
|
||||
link a: Swagger @ https://swagger.contoso.com
|
||||
link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com
|
||||
`;
|
||||
|
||||
mermaidAPI.parse(str);
|
||||
const boxes = diagram.db.getBoxes();
|
||||
expect(boxes[0].name).toEqual('Group 1');
|
||||
expect(boxes[0].actorKeys).toEqual(['a', 'b']);
|
||||
expect(boxes[0].fill).toEqual('transparent');
|
||||
});
|
||||
|
||||
it('should handle box without description', function () {
|
||||
const str = `
|
||||
sequenceDiagram
|
||||
box Aqua
|
||||
participant a as Alice
|
||||
participant b as Bob
|
||||
end
|
||||
participant c as Charlie
|
||||
links a: { "Repo": "https://repo.contoso.com/", "Dashboard": "https://dashboard.contoso.com/" }
|
||||
links b: { "Dashboard": "https://dashboard.contoso.com/" }
|
||||
links a: { "On-Call": "https://oncall.contoso.com/?svc=alice" }
|
||||
link a: Endpoint @ https://alice.contoso.com
|
||||
link a: Swagger @ https://swagger.contoso.com
|
||||
link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com
|
||||
`;
|
||||
|
||||
mermaidAPI.parse(str);
|
||||
const boxes = diagram.db.getBoxes();
|
||||
expect(boxes[0].name).toBeFalsy();
|
||||
expect(boxes[0].actorKeys).toEqual(['a', 'b']);
|
||||
expect(boxes[0].fill).toEqual('Aqua');
|
||||
});
|
||||
});
|
||||
describe('when checking the bounds in a sequenceDiagram', function () {
|
||||
beforeAll(() => {
|
||||
let conf = {
|
||||
@@ -1573,6 +1649,24 @@ Alice->Bob: Hello Bob, how are you?`;
|
||||
expect(bounds.stopx).toBe(conf.width * 2 + conf.actorMargin);
|
||||
expect(bounds.stopy).toBe(models.lastMessage().stopy + 10);
|
||||
});
|
||||
it('should handle two actors in a box', function () {
|
||||
const str = `
|
||||
sequenceDiagram
|
||||
box rgb(34, 56, 0) Group1
|
||||
participant Alice
|
||||
participant Bob
|
||||
end
|
||||
Alice->Bob: Hello Bob, how are you?`;
|
||||
|
||||
mermaidAPI.parse(str);
|
||||
diagram.renderer.draw(str, 'tst', '1.2.3', diagram);
|
||||
|
||||
const { bounds, models } = diagram.renderer.bounds.getBounds();
|
||||
expect(bounds.startx).toBe(0);
|
||||
expect(bounds.starty).toBe(0);
|
||||
expect(bounds.stopx).toBe(conf.width * 2 + conf.actorMargin + conf.boxTextMargin * 2);
|
||||
expect(bounds.stopy).toBe(models.lastMessage().stopy + 20);
|
||||
});
|
||||
it('should handle two actors with init directive', function () {
|
||||
const str = `
|
||||
%%{init: {'logLevel': 0}}%%
|
||||
|
||||
@@ -10,6 +10,7 @@ import assignWithDepth from '../../assignWithDepth';
|
||||
import utils from '../../utils';
|
||||
import { configureSvgSize } from '../../setupGraphViewbox';
|
||||
import Diagram from '../../Diagram';
|
||||
import { convert } from '../../tests/util';
|
||||
|
||||
let conf = {};
|
||||
|
||||
@@ -43,10 +44,14 @@ export const bounds = {
|
||||
},
|
||||
clear: function () {
|
||||
this.actors = [];
|
||||
this.boxes = [];
|
||||
this.loops = [];
|
||||
this.messages = [];
|
||||
this.notes = [];
|
||||
},
|
||||
addBox: function (boxModel) {
|
||||
this.boxes.push(boxModel);
|
||||
},
|
||||
addActor: function (actorModel) {
|
||||
this.actors.push(actorModel);
|
||||
},
|
||||
@@ -72,6 +77,7 @@ export const bounds = {
|
||||
return this.notes[this.notes.length - 1];
|
||||
},
|
||||
actors: [],
|
||||
boxes: [],
|
||||
loops: [],
|
||||
messages: [],
|
||||
notes: [],
|
||||
@@ -465,7 +471,8 @@ export const drawActors = function (
|
||||
actorKeys,
|
||||
verticalPos,
|
||||
configuration,
|
||||
messages
|
||||
messages,
|
||||
isFooter
|
||||
) {
|
||||
if (configuration.hideUnusedParticipants === true) {
|
||||
const newActors = new Set();
|
||||
@@ -480,8 +487,28 @@ export const drawActors = function (
|
||||
let prevWidth = 0;
|
||||
let prevMargin = 0;
|
||||
let maxHeight = 0;
|
||||
let prevBox = undefined;
|
||||
|
||||
for (const actorKey of actorKeys) {
|
||||
const actor = actors[actorKey];
|
||||
const box = actor.box;
|
||||
|
||||
// end of box
|
||||
if (prevBox && prevBox != box) {
|
||||
if (!isFooter) {
|
||||
bounds.models.addBox(prevBox);
|
||||
}
|
||||
prevMargin += conf.boxMargin + prevBox.margin;
|
||||
}
|
||||
|
||||
// new box
|
||||
if (box && box != prevBox) {
|
||||
if (!isFooter) {
|
||||
box.x = prevWidth + prevMargin;
|
||||
box.y = verticalPos;
|
||||
}
|
||||
prevMargin += box.margin;
|
||||
}
|
||||
|
||||
// Add some rendering data to the object
|
||||
actor.width = actor.width || conf.width;
|
||||
@@ -489,18 +516,27 @@ export const drawActors = function (
|
||||
actor.margin = actor.margin || conf.actorMargin;
|
||||
|
||||
actor.x = prevWidth + prevMargin;
|
||||
actor.y = verticalPos;
|
||||
actor.y = bounds.getVerticalPos();
|
||||
|
||||
// Draw the box with the attached line
|
||||
const height = svgDraw.drawActor(diagram, actor, conf);
|
||||
const height = svgDraw.drawActor(diagram, actor, conf, isFooter);
|
||||
maxHeight = Math.max(maxHeight, height);
|
||||
bounds.insert(actor.x, verticalPos, actor.x + actor.width, actor.height);
|
||||
|
||||
prevWidth += actor.width;
|
||||
prevMargin += actor.margin;
|
||||
prevWidth += actor.width + prevMargin;
|
||||
if (actor.box) {
|
||||
actor.box.width = prevWidth + box.margin - actor.box.x;
|
||||
}
|
||||
prevMargin = actor.margin;
|
||||
prevBox = actor.box;
|
||||
bounds.models.addActor(actor);
|
||||
}
|
||||
|
||||
// end of box
|
||||
if (prevBox && !isFooter) {
|
||||
bounds.models.addBox(prevBox);
|
||||
}
|
||||
|
||||
// Add a margin between the actor boxes and the first arrow
|
||||
bounds.bumpVerticalPos(maxHeight);
|
||||
};
|
||||
@@ -614,18 +650,27 @@ export const draw = function (_text: string, id: string, _version: string, diagO
|
||||
|
||||
// Fetch data from the parsing
|
||||
const actors = diagObj.db.getActors();
|
||||
const boxes = diagObj.db.getBoxes();
|
||||
const actorKeys = diagObj.db.getActorKeys();
|
||||
const messages = diagObj.db.getMessages();
|
||||
const title = diagObj.db.getDiagramTitle();
|
||||
|
||||
const hasBoxes = diagObj.db.hasAtLeastOneBox();
|
||||
const hasBoxTitles = diagObj.db.hasAtLeastOneBoxWithTitle();
|
||||
const maxMessageWidthPerActor = getMaxMessageWidthPerActor(actors, messages, diagObj);
|
||||
conf.height = calculateActorMargins(actors, maxMessageWidthPerActor);
|
||||
conf.height = calculateActorMargins(actors, maxMessageWidthPerActor, boxes);
|
||||
|
||||
svgDraw.insertComputerIcon(diagram);
|
||||
svgDraw.insertDatabaseIcon(diagram);
|
||||
svgDraw.insertClockIcon(diagram);
|
||||
|
||||
drawActors(diagram, actors, actorKeys, 0, conf, messages);
|
||||
if (hasBoxes) {
|
||||
bounds.bumpVerticalPos(conf.boxMargin);
|
||||
if (hasBoxTitles) {
|
||||
bounds.bumpVerticalPos(boxes[0].textMaxHeight);
|
||||
}
|
||||
}
|
||||
|
||||
drawActors(diagram, actors, actorKeys, 0, conf, messages, false);
|
||||
const loopWidths = calculateLoopBounds(messages, actors, maxMessageWidthPerActor, diagObj);
|
||||
|
||||
// The arrow head definition is attached to the svg once
|
||||
@@ -847,11 +892,26 @@ export const draw = function (_text: string, id: string, _version: string, diagO
|
||||
if (conf.mirrorActors) {
|
||||
// Draw actors below diagram
|
||||
bounds.bumpVerticalPos(conf.boxMargin * 2);
|
||||
drawActors(diagram, actors, actorKeys, bounds.getVerticalPos(), conf, messages);
|
||||
drawActors(diagram, actors, actorKeys, bounds.getVerticalPos(), conf, messages, true);
|
||||
bounds.bumpVerticalPos(conf.boxMargin);
|
||||
fixLifeLineHeights(diagram, bounds.getVerticalPos());
|
||||
}
|
||||
|
||||
bounds.models.boxes.forEach(function (box) {
|
||||
box.height = bounds.getVerticalPos() - box.y;
|
||||
bounds.insert(box.x, box.y, box.x + box.width, box.height);
|
||||
box.startx = box.x;
|
||||
box.starty = box.y;
|
||||
box.stopx = box.startx + box.width;
|
||||
box.stopy = box.starty + box.height;
|
||||
box.stroke = 'rgb(0,0,0, 0.5)';
|
||||
svgDraw.drawBox(diagram, box, conf);
|
||||
});
|
||||
|
||||
if (hasBoxes) {
|
||||
bounds.bumpVerticalPos(conf.boxMargin);
|
||||
}
|
||||
|
||||
// only draw popups for the top row of actors.
|
||||
const requiredBoxSize = drawActorsPopup(diagram, actors, actorKeys, doc);
|
||||
|
||||
@@ -1039,10 +1099,12 @@ const getRequiredPopupWidth = function (actor) {
|
||||
*
|
||||
* @param actors - The actors map to calculate margins for
|
||||
* @param actorToMessageWidth - A map of actor key → max message width it holds
|
||||
* @param boxes - The boxes around the actors if any
|
||||
*/
|
||||
function calculateActorMargins(
|
||||
actors: { [id: string]: any },
|
||||
actorToMessageWidth: ReturnType<typeof getMaxMessageWidthPerActor>
|
||||
actorToMessageWidth: ReturnType<typeof getMaxMessageWidthPerActor>,
|
||||
boxes
|
||||
) {
|
||||
let maxHeight = 0;
|
||||
Object.keys(actors).forEach((prop) => {
|
||||
@@ -1074,6 +1136,9 @@ function calculateActorMargins(
|
||||
|
||||
// No need to space out an actor that doesn't have a next link
|
||||
if (!nextActor) {
|
||||
const messageWidth = actorToMessageWidth[actorKey];
|
||||
const actorWidth = messageWidth + conf.actorMargin - actor.width / 2;
|
||||
actor.margin = Math.max(actorWidth, conf.actorMargin);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1083,6 +1148,29 @@ function calculateActorMargins(
|
||||
actor.margin = Math.max(actorWidth, conf.actorMargin);
|
||||
}
|
||||
|
||||
let maxBoxHeight = 0;
|
||||
boxes.forEach((box) => {
|
||||
const textFont = messageFont(conf);
|
||||
let totalWidth = box.actorKeys.reduce((total, aKey) => {
|
||||
return (total += actors[aKey].width + (actors[aKey].margin || 0));
|
||||
}, 0);
|
||||
|
||||
totalWidth -= 2 * conf.boxTextMargin;
|
||||
if (box.wrap) {
|
||||
box.name = utils.wrapLabel(box.name, totalWidth - 2 * conf.wrapPadding, textFont);
|
||||
}
|
||||
|
||||
const boxMsgDimensions = utils.calculateTextDimensions(box.name, textFont);
|
||||
maxBoxHeight = Math.max(boxMsgDimensions.height, maxBoxHeight);
|
||||
const minWidth = Math.max(totalWidth, boxMsgDimensions.width + 2 * conf.wrapPadding);
|
||||
box.margin = conf.boxTextMargin;
|
||||
if (totalWidth < minWidth) {
|
||||
const missing = (minWidth - totalWidth) / 2;
|
||||
box.margin += missing;
|
||||
}
|
||||
});
|
||||
boxes.forEach((box) => (box.textMaxHeight = maxBoxHeight));
|
||||
|
||||
return Math.max(maxHeight, conf.height);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import common from '../common/common';
|
||||
import { addFunction } from '../../interactionDb';
|
||||
import { parseFontSize } from '../../utils';
|
||||
import { sanitizeUrl } from '@braintree/sanitize-url';
|
||||
|
||||
export const drawRect = function (elem, rectData) {
|
||||
@@ -156,6 +157,8 @@ export const drawText = function (elem, textData) {
|
||||
textHeight = 0;
|
||||
const lines = textData.text.split(common.lineBreakRegex);
|
||||
|
||||
const [_textFontSize, _textFontSizePx] = parseFontSize(textData.fontSize);
|
||||
|
||||
let textElems = [];
|
||||
let dy = 0;
|
||||
let yfunc = () => textData.y;
|
||||
@@ -218,9 +221,9 @@ export const drawText = function (elem, textData) {
|
||||
if (
|
||||
textData.textMargin !== undefined &&
|
||||
textData.textMargin === 0 &&
|
||||
textData.fontSize !== undefined
|
||||
_textFontSize !== undefined
|
||||
) {
|
||||
dy = i * textData.fontSize;
|
||||
dy = i * _textFontSize;
|
||||
}
|
||||
|
||||
const textElem = elem.append('text');
|
||||
@@ -235,8 +238,8 @@ export const drawText = function (elem, textData) {
|
||||
if (textData.fontFamily !== undefined) {
|
||||
textElem.style('font-family', textData.fontFamily);
|
||||
}
|
||||
if (textData.fontSize !== undefined) {
|
||||
textElem.style('font-size', textData.fontSize);
|
||||
if (_textFontSizePx !== undefined) {
|
||||
textElem.style('font-size', _textFontSizePx);
|
||||
}
|
||||
if (textData.fontWeight !== undefined) {
|
||||
textElem.style('font-weight', textData.fontWeight);
|
||||
@@ -338,19 +341,21 @@ export const fixLifeLineHeights = (diagram, bounds) => {
|
||||
* @param {any} elem - The diagram we'll draw to.
|
||||
* @param {any} actor - The actor to draw.
|
||||
* @param {any} conf - DrawText implementation discriminator object
|
||||
* @param {boolean} isFooter - If the actor is the footer one
|
||||
*/
|
||||
const drawActorTypeParticipant = function (elem, actor, conf) {
|
||||
const drawActorTypeParticipant = function (elem, actor, conf, isFooter) {
|
||||
const center = actor.x + actor.width / 2;
|
||||
const centerY = actor.y + 5;
|
||||
|
||||
const boxpluslineGroup = elem.append('g');
|
||||
var g = boxpluslineGroup;
|
||||
|
||||
if (actor.y === 0) {
|
||||
if (!isFooter) {
|
||||
actorCnt++;
|
||||
g.append('line')
|
||||
.attr('id', 'actor' + actorCnt)
|
||||
.attr('x1', center)
|
||||
.attr('y1', 5)
|
||||
.attr('y1', centerY)
|
||||
.attr('x2', center)
|
||||
.attr('y2', 2000)
|
||||
.attr('class', 'actor-line')
|
||||
@@ -413,16 +418,17 @@ const drawActorTypeParticipant = function (elem, actor, conf) {
|
||||
return height;
|
||||
};
|
||||
|
||||
const drawActorTypeActor = function (elem, actor, conf) {
|
||||
const drawActorTypeActor = function (elem, actor, conf, isFooter) {
|
||||
const center = actor.x + actor.width / 2;
|
||||
const centerY = actor.y + 80;
|
||||
|
||||
if (actor.y === 0) {
|
||||
if (!isFooter) {
|
||||
actorCnt++;
|
||||
elem
|
||||
.append('line')
|
||||
.attr('id', 'actor' + actorCnt)
|
||||
.attr('x1', center)
|
||||
.attr('y1', 80)
|
||||
.attr('y1', centerY)
|
||||
.attr('x2', center)
|
||||
.attr('y2', 2000)
|
||||
.attr('class', 'actor-line')
|
||||
@@ -495,15 +501,34 @@ const drawActorTypeActor = function (elem, actor, conf) {
|
||||
return actor.height;
|
||||
};
|
||||
|
||||
export const drawActor = function (elem, actor, conf) {
|
||||
export const drawActor = function (elem, actor, conf, isFooter) {
|
||||
switch (actor.type) {
|
||||
case 'actor':
|
||||
return drawActorTypeActor(elem, actor, conf);
|
||||
return drawActorTypeActor(elem, actor, conf, isFooter);
|
||||
case 'participant':
|
||||
return drawActorTypeParticipant(elem, actor, conf);
|
||||
return drawActorTypeParticipant(elem, actor, conf, isFooter);
|
||||
}
|
||||
};
|
||||
|
||||
export const drawBox = function (elem, box, conf) {
|
||||
const boxplustextGroup = elem.append('g');
|
||||
const g = boxplustextGroup;
|
||||
drawBackgroundRect(g, box);
|
||||
if (box.name) {
|
||||
_drawTextCandidateFunc(conf)(
|
||||
box.name,
|
||||
g,
|
||||
box.x,
|
||||
box.y + (box.textMaxHeight || 0) / 2,
|
||||
box.width,
|
||||
0,
|
||||
{ class: 'text' },
|
||||
conf
|
||||
);
|
||||
}
|
||||
g.lower();
|
||||
};
|
||||
|
||||
export const anchorElement = function (elem) {
|
||||
return elem.append('g');
|
||||
};
|
||||
@@ -642,6 +667,7 @@ export const drawBackgroundRect = function (elem, bounds) {
|
||||
width: bounds.stopx - bounds.startx,
|
||||
height: bounds.stopy - bounds.starty,
|
||||
fill: bounds.fill,
|
||||
stroke: bounds.stroke,
|
||||
class: 'rect',
|
||||
});
|
||||
rectElem.lower();
|
||||
@@ -840,8 +866,7 @@ const _drawTextCandidateFunc = (function () {
|
||||
function byTspan(content, g, x, y, width, height, textAttrs, conf) {
|
||||
const { actorFontSize, actorFontFamily, actorFontWeight } = conf;
|
||||
|
||||
let _actorFontSize =
|
||||
actorFontSize && actorFontSize.replace ? actorFontSize.replace('px', '') : actorFontSize;
|
||||
const [_actorFontSize, _actorFontSizePx] = parseFontSize(actorFontSize);
|
||||
|
||||
const lines = content.split(common.lineBreakRegex);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
@@ -851,7 +876,7 @@ const _drawTextCandidateFunc = (function () {
|
||||
.attr('x', x + width / 2)
|
||||
.attr('y', y)
|
||||
.style('text-anchor', 'middle')
|
||||
.style('font-size', actorFontSize)
|
||||
.style('font-size', _actorFontSizePx)
|
||||
.style('font-weight', actorFontWeight)
|
||||
.style('font-family', actorFontFamily);
|
||||
text
|
||||
@@ -1035,6 +1060,7 @@ export default {
|
||||
drawText,
|
||||
drawLabel,
|
||||
drawActor,
|
||||
drawBox,
|
||||
drawPopup,
|
||||
drawImage,
|
||||
drawEmbeddedImage,
|
||||
|
||||
@@ -125,6 +125,30 @@ describe('svgDraw', function () {
|
||||
expect(text3.attr).toHaveBeenCalledWith('y', 10);
|
||||
expect(text3.text).toHaveBeenCalledWith('fine lines');
|
||||
});
|
||||
it('should work with numeral font sizes', function () {
|
||||
const svg = MockD3('svg');
|
||||
svgDraw.drawText(svg, {
|
||||
x: 10,
|
||||
y: 10,
|
||||
dy: '1em',
|
||||
text: 'One fine text message',
|
||||
class: 'noteText',
|
||||
fontFamily: 'courier',
|
||||
fontSize: 10,
|
||||
fontWeight: '500',
|
||||
});
|
||||
expect(svg.__children.length).toBe(1);
|
||||
const text = svg.__children[0];
|
||||
expect(text.__name).toBe('text');
|
||||
expect(text.attr).toHaveBeenCalledWith('x', 10);
|
||||
expect(text.attr).toHaveBeenCalledWith('y', 10);
|
||||
expect(text.attr).toHaveBeenCalledWith('dy', '1em');
|
||||
expect(text.attr).toHaveBeenCalledWith('class', 'noteText');
|
||||
expect(text.text).toHaveBeenCalledWith('One fine text message');
|
||||
expect(text.style).toHaveBeenCalledWith('font-family', 'courier');
|
||||
expect(text.style).toHaveBeenCalledWith('font-size', '10px');
|
||||
expect(text.style).toHaveBeenCalledWith('font-weight', '500');
|
||||
});
|
||||
});
|
||||
describe('drawBackgroundRect', function () {
|
||||
it('should append a rect before the previous element within a given bound', function () {
|
||||
|
||||
133
packages/mermaid/src/diagrams/state/parser/state-parser.spec.js
Normal file
133
packages/mermaid/src/diagrams/state/parser/state-parser.spec.js
Normal file
@@ -0,0 +1,133 @@
|
||||
import stateDb from '../stateDb';
|
||||
import stateDiagram from './stateDiagram';
|
||||
import { setConfig } from '../../../config';
|
||||
|
||||
setConfig({
|
||||
securityLevel: 'strict',
|
||||
});
|
||||
|
||||
describe('state parser can parse...', () => {
|
||||
beforeEach(function () {
|
||||
stateDiagram.parser.yy = stateDb;
|
||||
stateDiagram.parser.yy.clear();
|
||||
});
|
||||
|
||||
describe('states with id displayed as a (name)', () => {
|
||||
describe('syntax 1: stateID as "name in quotes"', () => {
|
||||
it('stateID as "some name"', () => {
|
||||
const diagramText = `stateDiagram-v2
|
||||
state "Small State 1" as namedState1`;
|
||||
stateDiagram.parser.parse(diagramText);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
|
||||
const states = stateDiagram.parser.yy.getStates();
|
||||
expect(states['namedState1']).not.toBeUndefined();
|
||||
expect(states['namedState1'].descriptions.join(' ')).toEqual('Small State 1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('syntax 2: stateID: "name in quotes" [colon after the id]', () => {
|
||||
it('space before and after the colon', () => {
|
||||
const diagramText = `stateDiagram-v2
|
||||
namedState1 : Small State 1`;
|
||||
stateDiagram.parser.parse(diagramText);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
|
||||
const states = stateDiagram.parser.yy.getStates();
|
||||
expect(states['namedState1']).not.toBeUndefined();
|
||||
expect(states['namedState1'].descriptions.join(' ')).toEqual('Small State 1');
|
||||
});
|
||||
|
||||
it('no spaces before and after the colon', () => {
|
||||
const diagramText = `stateDiagram-v2
|
||||
namedState1:Small State 1`;
|
||||
stateDiagram.parser.parse(diagramText);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
|
||||
const states = stateDiagram.parser.yy.getStates();
|
||||
expect(states['namedState1']).not.toBeUndefined();
|
||||
expect(states['namedState1'].descriptions.join(' ')).toEqual('Small State 1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('can handle "as" in a state name', () => {
|
||||
it('assemble, assemblies, state assemble, state assemblies', function () {
|
||||
const diagramText = `stateDiagram-v2
|
||||
assemble
|
||||
assemblies
|
||||
state assemble
|
||||
state assemblies
|
||||
`;
|
||||
stateDiagram.parser.parse(diagramText);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
const states = stateDiagram.parser.yy.getStates();
|
||||
expect(states['assemble']).not.toBeUndefined();
|
||||
expect(states['assemblies']).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('state "as" as as', function () {
|
||||
const diagramText = `stateDiagram-v2
|
||||
state "as" as as
|
||||
`;
|
||||
stateDiagram.parser.parse(diagramText);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
const states = stateDiagram.parser.yy.getStates();
|
||||
expect(states['as']).not.toBeUndefined();
|
||||
expect(states['as'].descriptions.join(' ')).toEqual('as');
|
||||
});
|
||||
});
|
||||
|
||||
describe('groups (clusters/containers)', () => {
|
||||
it('state "Group Name" as stateIdentifier', () => {
|
||||
const diagramText = `stateDiagram-v2
|
||||
state "Small State 1" as namedState1
|
||||
%% Notice that this is named "Big State 1" with an "as"
|
||||
state "Big State 1" as bigState1 {
|
||||
bigState1InternalState
|
||||
}
|
||||
namedState1 --> bigState1: should point to \\nBig State 1 container
|
||||
|
||||
state "Small State 2" as namedState2
|
||||
%% Notice that bigState2 does not have a name; no "as"
|
||||
state bigState2 {
|
||||
bigState2InternalState
|
||||
}
|
||||
namedState2 --> bigState2: should point to \\nbigState2 container`;
|
||||
|
||||
stateDiagram.parser.parse(diagramText);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
|
||||
const states = stateDiagram.parser.yy.getStates();
|
||||
expect(states['namedState1']).not.toBeUndefined();
|
||||
expect(states['bigState1']).not.toBeUndefined();
|
||||
expect(states['bigState1'].doc[0].id).toEqual('bigState1InternalState');
|
||||
expect(states['namedState2']).not.toBeUndefined();
|
||||
expect(states['bigState2']).not.toBeUndefined();
|
||||
expect(states['bigState2'].doc[0].id).toEqual('bigState2InternalState');
|
||||
const relationships = stateDiagram.parser.yy.getRelations();
|
||||
expect(relationships[0].id1).toEqual('namedState1');
|
||||
expect(relationships[0].id2).toEqual('bigState1');
|
||||
expect(relationships[1].id1).toEqual('namedState2');
|
||||
expect(relationships[1].id2).toEqual('bigState2');
|
||||
});
|
||||
|
||||
it('group has a state with stateID AS "state name" and state2ID: "another state name"', () => {
|
||||
const diagramText = `stateDiagram-v2
|
||||
state "Big State 1" as bigState1 {
|
||||
state "inner state 1" as inner1
|
||||
inner2: inner state 2
|
||||
inner1 --> inner2
|
||||
}`;
|
||||
stateDiagram.parser.parse(diagramText);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
|
||||
const states = stateDiagram.parser.yy.getStates();
|
||||
expect(states['bigState1']).not.toBeUndefined();
|
||||
expect(states['bigState1'].doc[0].id).toEqual('inner1');
|
||||
expect(states['bigState1'].doc[0].description).toEqual('inner state 1');
|
||||
expect(states['bigState1'].doc[1].id).toEqual('inner2');
|
||||
expect(states['bigState1'].doc[1].description).toEqual('inner state 2');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -65,14 +65,14 @@
|
||||
\%%[^\n]* /* skip comments */
|
||||
"scale"\s+ { this.pushState('SCALE'); /* console.log('Got scale', yytext);*/ return 'scale'; }
|
||||
<SCALE>\d+ return 'WIDTH';
|
||||
<SCALE>\s+"width" {this.popState();}
|
||||
<SCALE>\s+"width" { this.popState(); }
|
||||
|
||||
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
|
||||
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
|
||||
accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; }
|
||||
<acc_descr>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_descr_value"; }
|
||||
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
|
||||
<acc_descr_multiline>[\}] { this.popState(); }
|
||||
accDescr\s*"{"\s* { this.begin("acc_descr_multiline"); }
|
||||
<acc_descr_multiline>[\}] { this.popState(); }
|
||||
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
|
||||
|
||||
<INITIAL,struct>"classDef"\s+ { this.pushState('CLASSDEF'); return 'classDef'; }
|
||||
@@ -81,57 +81,60 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
||||
<CLASSDEFID>[^\n]* { this.popState(); return 'CLASSDEF_STYLEOPTS' }
|
||||
|
||||
<INITIAL,struct>"class"\s+ { this.pushState('CLASS'); return 'class'; }
|
||||
<CLASS>(\w+)+((","\s*\w+)*) { this.popState(); this.pushState('CLASS_STYLE'); return 'CLASSENTITY_IDS' }
|
||||
<CLASS>(\w+)+((","\s*\w+)*) { this.popState(); this.pushState('CLASS_STYLE'); return 'CLASSENTITY_IDS' }
|
||||
<CLASS_STYLE>[^\n]* { this.popState(); return 'STYLECLASS' }
|
||||
|
||||
"scale"\s+ { this.pushState('SCALE'); /* console.log('Got scale', yytext);*/ return 'scale'; }
|
||||
<SCALE>\d+ return 'WIDTH';
|
||||
<SCALE>\s+"width" {this.popState();}
|
||||
|
||||
<INITIAL,struct>"state"\s+ { /* console.log('Starting STATE '); */ this.pushState('STATE'); }
|
||||
|
||||
<INITIAL,struct>"state"\s+ { /*console.log('Starting STATE zxzx'+yy.getDirection());*/this.pushState('STATE'); }
|
||||
<STATE>.*"<<fork>>" {this.popState();yytext=yytext.slice(0,-8).trim(); /*console.warn('Fork Fork: ',yytext);*/return 'FORK';}
|
||||
<STATE>.*"<<join>>" {this.popState();yytext=yytext.slice(0,-8).trim();/*console.warn('Fork Join: ',yytext);*/return 'JOIN';}
|
||||
<STATE>.*"<<choice>>" {this.popState();yytext=yytext.slice(0,-10).trim();/*console.warn('Fork Join: ',yytext);*/return 'CHOICE';}
|
||||
<STATE>.*"<<choice>>" {this.popState();yytext=yytext.slice(0,-10).trim();/*console.warn('Fork Join: ',yytext);*/return 'CHOICE';}
|
||||
<STATE>.*"[[fork]]" {this.popState();yytext=yytext.slice(0,-8).trim();/*console.warn('Fork Fork: ',yytext);*/return 'FORK';}
|
||||
<STATE>.*"[[join]]" {this.popState();yytext=yytext.slice(0,-8).trim();/*console.warn('Fork Join: ',yytext);*/return 'JOIN';}
|
||||
<STATE>.*"[[choice]]" {this.popState();yytext=yytext.slice(0,-10).trim();/*console.warn('Fork Join: ',yytext);*/return 'CHOICE';}
|
||||
<STATE>.*"[[choice]]" {this.popState();yytext=yytext.slice(0,-10).trim();/*console.warn('Fork Join: ',yytext);*/return 'CHOICE';}
|
||||
|
||||
<struct>.*direction\s+TB[^\n]* { return 'direction_tb';}
|
||||
<struct>.*direction\s+BT[^\n]* { return 'direction_bt';}
|
||||
<struct>.*direction\s+RL[^\n]* { return 'direction_rl';}
|
||||
<struct>.*direction\s+LR[^\n]* { return 'direction_lr';}
|
||||
|
||||
<STATE>["] { /*console.log('Starting STATE_STRING zxzx');*/this.begin("STATE_STRING");}
|
||||
<STATE>\s*"as"\s+ {this.popState();this.pushState('STATE_ID');return "AS";}
|
||||
<STATE_ID>[^\n\{]* {this.popState();/* console.log('STATE_ID', yytext);*/return "ID";}
|
||||
<STATE_STRING>["] this.popState();
|
||||
<STATE_STRING>[^"]* { /*console.log('Long description:', yytext);*/return "STATE_DESCR";}
|
||||
<STATE>[^\n\s\{]+ {/*console.log('COMPOSIT_STATE', yytext);*/return 'COMPOSIT_STATE';}
|
||||
<STATE>\n {this.popState();}
|
||||
<INITIAL,STATE>\{ {this.popState();this.pushState('struct'); /*console.log('begin struct', yytext);*/return 'STRUCT_START';}
|
||||
<struct>\%\%(?!\{)[^\n]* /* skip comments inside state*/
|
||||
<struct>\} { /*console.log('Ending struct');*/ this.popState(); return 'STRUCT_STOP';}}
|
||||
<struct>[\n] /* nothing */
|
||||
<STATE>["] { /* console.log('Starting STATE_STRING'); */ this.pushState("STATE_STRING"); }
|
||||
<STATE>\s*"as"\s+ { this.pushState('STATE_ID'); /* console.log('pushState(STATE_ID)'); */ return "AS"; }
|
||||
<STATE_ID>[^\n\{]* { this.popState(); /* console.log('STATE_ID', yytext); */ return "ID"; }
|
||||
<STATE_STRING>["] { this.popState(); }
|
||||
<STATE_STRING>[^"]* { /* console.log('Long description:', yytext); */ return "STATE_DESCR"; }
|
||||
<STATE>[^\n\s\{]+ { /* console.log('COMPOSIT_STATE', yytext); */ return 'COMPOSIT_STATE'; }
|
||||
<STATE>\n { this.popState(); }
|
||||
<INITIAL,STATE>\{ { this.popState(); this.pushState('struct'); /* console.log('begin struct', yytext); */ return 'STRUCT_START'; }
|
||||
<struct>\%\%(?!\{)[^\n]* /* skip comments inside state*/
|
||||
<struct>\} { /*console.log('Ending struct');*/ this.popState(); return 'STRUCT_STOP';} }
|
||||
<struct>[\n] /* nothing */
|
||||
|
||||
<INITIAL,struct>"note"\s+ { this.begin('NOTE'); return 'note'; }
|
||||
<NOTE>"left of" { this.popState();this.pushState('NOTE_ID');return 'left_of';}
|
||||
<NOTE>"right of" { this.popState();this.pushState('NOTE_ID');return 'right_of';}
|
||||
<NOTE>\" { this.popState();this.pushState('FLOATING_NOTE');}
|
||||
<FLOATING_NOTE>\s*"as"\s* {this.popState();this.pushState('FLOATING_NOTE_ID');return "AS";}
|
||||
<FLOATING_NOTE>["] /**/
|
||||
<FLOATING_NOTE>[^"]* { /*console.log('Floating note text: ', yytext);*/return "NOTE_TEXT";}
|
||||
<FLOATING_NOTE_ID>[^\n]* {this.popState();/*console.log('Floating note ID', yytext);*/return "ID";}
|
||||
<NOTE_ID>\s*[^:\n\s\-]+ { this.popState();this.pushState('NOTE_TEXT');/*console.log('Got ID for note', yytext);*/return 'ID';}
|
||||
<NOTE_TEXT>\s*":"[^:\n;]+ { this.popState();/*console.log('Got NOTE_TEXT for note',yytext);*/yytext = yytext.substr(2).trim();return 'NOTE_TEXT';}
|
||||
<NOTE_TEXT>[\s\S]*?"end note" { this.popState();/*console.log('Got NOTE_TEXT for note',yytext);*/yytext = yytext.slice(0,-8).trim();return 'NOTE_TEXT';}
|
||||
<NOTE>"left of" { this.popState(); this.pushState('NOTE_ID'); return 'left_of'; }
|
||||
<NOTE>"right of" { this.popState(); this.pushState('NOTE_ID'); return 'right_of'; }
|
||||
<NOTE>\" { this.popState(); this.pushState('FLOATING_NOTE'); }
|
||||
<FLOATING_NOTE>\s*"as"\s* { this.popState(); this.pushState('FLOATING_NOTE_ID'); return "AS"; }
|
||||
<FLOATING_NOTE>["] /**/
|
||||
<FLOATING_NOTE>[^"]* { /* console.log('Floating note text: ', yytext); */ return "NOTE_TEXT"; }
|
||||
<FLOATING_NOTE_ID>[^\n]* { this.popState(); /* console.log('Floating note ID', yytext);*/ return "ID"; }
|
||||
<NOTE_ID>\s*[^:\n\s\-]+ { this.popState(); this.pushState('NOTE_TEXT'); /*console.log('Got ID for note', yytext);*/ return 'ID'; }
|
||||
<NOTE_TEXT>\s*":"[^:\n;]+ { this.popState(); /* console.log('Got NOTE_TEXT for note',yytext);*/yytext = yytext.substr(2).trim(); return 'NOTE_TEXT'; }
|
||||
<NOTE_TEXT>[\s\S]*?"end note" { this.popState(); /* console.log('Got NOTE_TEXT for note',yytext);*/yytext = yytext.slice(0,-8).trim(); return 'NOTE_TEXT'; }
|
||||
|
||||
"stateDiagram"\s+ { /*console.log('Got state diagram', yytext,'#');*/return 'SD'; }
|
||||
"stateDiagram-v2"\s+ { /*console.log('Got state diagram', yytext,'#');*/return 'SD'; }
|
||||
"hide empty description" { /*console.log('HIDE_EMPTY', yytext,'#');*/return 'HIDE_EMPTY'; }
|
||||
<INITIAL,struct>"[*]" { /*console.log('EDGE_STATE=',yytext);*/ return 'EDGE_STATE';}
|
||||
<INITIAL,struct>[^:\n\s\-\{]+ { /*console.log('=>ID=',yytext);*/ return 'ID';}
|
||||
// <INITIAL,struct>\s*":"[^\+\->:\n;]+ { yytext = yytext.trim(); /*console.log('Descr = ', yytext);*/ return 'DESCR'; }
|
||||
<INITIAL,struct>\s*":"[^:\n;]+ { yytext = yytext.trim(); /*console.log('Descr = ', yytext);*/ return 'DESCR'; }
|
||||
"stateDiagram"\s+ { /* console.log('Got state diagram', yytext,'#'); */ return 'SD'; }
|
||||
"stateDiagram-v2"\s+ { /* console.log('Got state diagram', yytext,'#'); */ return 'SD'; }
|
||||
|
||||
"hide empty description" { /* console.log('HIDE_EMPTY', yytext,'#'); */ return 'HIDE_EMPTY'; }
|
||||
|
||||
<INITIAL,struct>"[*]" { /* console.log('EDGE_STATE=',yytext); */ return 'EDGE_STATE'; }
|
||||
<INITIAL,struct>[^:\n\s\-\{]+ { /* console.log('=>ID=',yytext); */ return 'ID'; }
|
||||
// <INITIAL,struct>\s*":"[^\+\->:\n;]+ { yytext = yytext.trim(); /* console.log('Descr = ', yytext); */ return 'DESCR'; }
|
||||
<INITIAL,struct>\s*":"[^:\n;]+ { yytext = yytext.trim(); /* console.log('Descr = ', yytext); */ return 'DESCR'; }
|
||||
|
||||
<INITIAL,struct>"-->" return '-->';
|
||||
<struct>"--" return 'CONCURRENT';
|
||||
@@ -201,7 +204,7 @@ statement
|
||||
| COMPOSIT_STATE
|
||||
| COMPOSIT_STATE STRUCT_START document STRUCT_STOP
|
||||
{
|
||||
/* console.log('Adding document for state without id ', $1); */
|
||||
// console.log('Adding document for state without id ', $1);
|
||||
$$={ stmt: 'state', id: $1, type: 'default', description: '', doc: $3 }
|
||||
}
|
||||
| STATE_DESCR AS ID {
|
||||
@@ -217,7 +220,7 @@ statement
|
||||
}
|
||||
| STATE_DESCR AS ID STRUCT_START document STRUCT_STOP
|
||||
{
|
||||
/* console.log('Adding document for state with id zxzx', $3, $4, yy.getDirection()); yy.addDocument($3);*/
|
||||
// console.log('state with id ', $3,' document = ', $5, );
|
||||
$$={ stmt: 'state', id: $3, type: 'default', description: $1, doc: $5 }
|
||||
}
|
||||
| FORK {
|
||||
|
||||
@@ -94,9 +94,14 @@ const docTranslator = (parent, node, first) => {
|
||||
docTranslator(parent, node.state1, true);
|
||||
docTranslator(parent, node.state2, false);
|
||||
} else {
|
||||
if (node.stmt === STMT_STATE && node.id === '[*]') {
|
||||
node.id = first ? parent.id + '_start' : parent.id + '_end';
|
||||
node.start = first;
|
||||
if (node.stmt === STMT_STATE) {
|
||||
if (node.id === '[*]') {
|
||||
node.id = first ? parent.id + '_start' : parent.id + '_end';
|
||||
node.start = first;
|
||||
} else {
|
||||
// This is just a plain state, not a start or end
|
||||
node.id = node.id.trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (node.doc) {
|
||||
@@ -170,7 +175,7 @@ const extract = (_doc) => {
|
||||
switch (item.stmt) {
|
||||
case STMT_STATE:
|
||||
addState(
|
||||
item.id,
|
||||
item.id.trim(),
|
||||
item.type,
|
||||
item.doc,
|
||||
item.description,
|
||||
@@ -184,10 +189,10 @@ const extract = (_doc) => {
|
||||
addRelation(item.state1, item.state2, item.description);
|
||||
break;
|
||||
case STMT_CLASSDEF:
|
||||
addStyleClass(item.id, item.classes);
|
||||
addStyleClass(item.id.trim(), item.classes);
|
||||
break;
|
||||
case STMT_APPLYCLASS:
|
||||
setCssClass(item.id, item.styleClass);
|
||||
setCssClass(item.id.trim(), item.styleClass);
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -215,11 +220,12 @@ export const addState = function (
|
||||
styles = null,
|
||||
textStyles = null
|
||||
) {
|
||||
const trimmedId = id?.trim();
|
||||
// add the state if needed
|
||||
if (currentDocument.states[id] === undefined) {
|
||||
log.info('Adding state ', id, descr);
|
||||
currentDocument.states[id] = {
|
||||
id: id,
|
||||
if (currentDocument.states[trimmedId] === undefined) {
|
||||
log.info('Adding state ', trimmedId, descr);
|
||||
currentDocument.states[trimmedId] = {
|
||||
id: trimmedId,
|
||||
descriptions: [],
|
||||
type,
|
||||
doc,
|
||||
@@ -229,49 +235,49 @@ export const addState = function (
|
||||
textStyles: [],
|
||||
};
|
||||
} else {
|
||||
if (!currentDocument.states[id].doc) {
|
||||
currentDocument.states[id].doc = doc;
|
||||
if (!currentDocument.states[trimmedId].doc) {
|
||||
currentDocument.states[trimmedId].doc = doc;
|
||||
}
|
||||
if (!currentDocument.states[id].type) {
|
||||
currentDocument.states[id].type = type;
|
||||
if (!currentDocument.states[trimmedId].type) {
|
||||
currentDocument.states[trimmedId].type = type;
|
||||
}
|
||||
}
|
||||
|
||||
if (descr) {
|
||||
log.info('Setting state description', id, descr);
|
||||
log.info('Setting state description', trimmedId, descr);
|
||||
if (typeof descr === 'string') {
|
||||
addDescription(id, descr.trim());
|
||||
addDescription(trimmedId, descr.trim());
|
||||
}
|
||||
|
||||
if (typeof descr === 'object') {
|
||||
descr.forEach((des) => addDescription(id, des.trim()));
|
||||
descr.forEach((des) => addDescription(trimmedId, des.trim()));
|
||||
}
|
||||
}
|
||||
|
||||
if (note) {
|
||||
currentDocument.states[id].note = note;
|
||||
currentDocument.states[id].note.text = common.sanitizeText(
|
||||
currentDocument.states[id].note.text,
|
||||
currentDocument.states[trimmedId].note = note;
|
||||
currentDocument.states[trimmedId].note.text = common.sanitizeText(
|
||||
currentDocument.states[trimmedId].note.text,
|
||||
configApi.getConfig()
|
||||
);
|
||||
}
|
||||
|
||||
if (classes) {
|
||||
log.info('Setting state classes', id, classes);
|
||||
log.info('Setting state classes', trimmedId, classes);
|
||||
const classesList = typeof classes === 'string' ? [classes] : classes;
|
||||
classesList.forEach((klass) => setCssClass(id, klass.trim()));
|
||||
classesList.forEach((klass) => setCssClass(trimmedId, klass.trim()));
|
||||
}
|
||||
|
||||
if (styles) {
|
||||
log.info('Setting state styles', id, styles);
|
||||
log.info('Setting state styles', trimmedId, styles);
|
||||
const stylesList = typeof styles === 'string' ? [styles] : styles;
|
||||
stylesList.forEach((style) => setStyle(id, style.trim()));
|
||||
stylesList.forEach((style) => setStyle(trimmedId, style.trim()));
|
||||
}
|
||||
|
||||
if (textStyles) {
|
||||
log.info('Setting state styles', id, styles);
|
||||
log.info('Setting state styles', trimmedId, styles);
|
||||
const textStylesList = typeof textStyles === 'string' ? [textStyles] : textStyles;
|
||||
textStylesList.forEach((textStyle) => setTextStyle(id, textStyle.trim()));
|
||||
textStylesList.forEach((textStyle) => setTextStyle(trimmedId, textStyle.trim()));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -368,10 +374,10 @@ function endTypeIfNeeded(id = '', type = DEFAULT_STATE_TYPE) {
|
||||
* @param relationTitle
|
||||
*/
|
||||
export function addRelationObjs(item1, item2, relationTitle) {
|
||||
let id1 = startIdIfNeeded(item1.id);
|
||||
let type1 = startTypeIfNeeded(item1.id, item1.type);
|
||||
let id2 = startIdIfNeeded(item2.id);
|
||||
let type2 = startTypeIfNeeded(item2.id, item2.type);
|
||||
let id1 = startIdIfNeeded(item1.id.trim());
|
||||
let type1 = startTypeIfNeeded(item1.id.trim(), item1.type);
|
||||
let id2 = startIdIfNeeded(item2.id.trim());
|
||||
let type2 = startTypeIfNeeded(item2.id.trim(), item2.type);
|
||||
|
||||
addState(
|
||||
id1,
|
||||
@@ -412,9 +418,9 @@ export const addRelation = function (item1, item2, title) {
|
||||
if (typeof item1 === 'object') {
|
||||
addRelationObjs(item1, item2, title);
|
||||
} else {
|
||||
const id1 = startIdIfNeeded(item1);
|
||||
const id1 = startIdIfNeeded(item1.trim());
|
||||
const type1 = startTypeIfNeeded(item1);
|
||||
const id2 = endIdIfNeeded(item2);
|
||||
const id2 = endIdIfNeeded(item2.trim());
|
||||
const type2 = endTypeIfNeeded(item2);
|
||||
|
||||
addState(id1, type1);
|
||||
|
||||
@@ -307,8 +307,8 @@ const setupNode = (g, parent, parsedItem, diagramStates, diagramDb, altFlag) =>
|
||||
*
|
||||
* @param g
|
||||
* @param parentParsedItem - parsed Item that is the parent of this document (doc)
|
||||
* @param doc - the document to set up
|
||||
* @param {object} diagramStates - the list of all known states for the diagram
|
||||
* @param doc - the document to set up; it is a list of parsed statements
|
||||
* @param {object[]} diagramStates - the list of all known states for the diagram
|
||||
* @param diagramDb
|
||||
* @param {boolean} altFlag
|
||||
* @todo This duplicates some of what is done in stateDb.js extract method
|
||||
|
||||
20
packages/mermaid/src/diagrams/timeline/detector.ts
Normal file
20
packages/mermaid/src/diagrams/timeline/detector.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { ExternalDiagramDefinition } from '../../diagram-api/types';
|
||||
|
||||
const id = 'timeline';
|
||||
|
||||
const detector = (txt: string) => {
|
||||
return txt.match(/^\s*timeline/) !== null;
|
||||
};
|
||||
|
||||
const loader = async () => {
|
||||
const { diagram } = await import('./diagram-definition');
|
||||
return { id, diagram };
|
||||
};
|
||||
|
||||
const plugin: ExternalDiagramDefinition = {
|
||||
id,
|
||||
detector,
|
||||
loader,
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
12
packages/mermaid/src/diagrams/timeline/diagram-definition.ts
Normal file
12
packages/mermaid/src/diagrams/timeline/diagram-definition.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
import parser from './parser/timeline.jison';
|
||||
import * as db from './timelineDb';
|
||||
import renderer from './timelineRenderer';
|
||||
import styles from './styles';
|
||||
|
||||
export const diagram = {
|
||||
db,
|
||||
renderer,
|
||||
parser,
|
||||
styles,
|
||||
};
|
||||
112
packages/mermaid/src/diagrams/timeline/parser/timeline.jison
Normal file
112
packages/mermaid/src/diagrams/timeline/parser/timeline.jison
Normal file
@@ -0,0 +1,112 @@
|
||||
/** mermaid
|
||||
* https://mermaidjs.github.io/
|
||||
* (c) 2023 Knut Sveidqvist
|
||||
* MIT license.
|
||||
*/
|
||||
%lex
|
||||
%options case-insensitive
|
||||
%x acc_title
|
||||
%x acc_descr
|
||||
%x acc_descr_multiline
|
||||
|
||||
// Directive states
|
||||
%x open_directive type_directive arg_directive
|
||||
|
||||
|
||||
%%
|
||||
|
||||
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
|
||||
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
|
||||
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
|
||||
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
|
||||
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
|
||||
\%%(?!\{)[^\n]* /* skip comments */
|
||||
[^\}]\%\%[^\n]* /* skip comments */
|
||||
[\n]+ return 'NEWLINE';
|
||||
\s+ /* skip whitespace */
|
||||
\#[^\n]* /* skip comments */
|
||||
|
||||
"timeline" return 'timeline';
|
||||
"title"\s[^#\n;]+ return 'title';
|
||||
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
|
||||
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
|
||||
accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; }
|
||||
<acc_descr>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_descr_value"; }
|
||||
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
|
||||
<acc_descr_multiline>[\}] { this.popState(); }
|
||||
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
|
||||
"section"\s[^#:\n;]+ return 'section';
|
||||
|
||||
// event starting with "==>" keyword
|
||||
":"\s[^#:\n;]+ return 'event';
|
||||
[^#:\n;]+ return 'period';
|
||||
|
||||
|
||||
<<EOF>> return 'EOF';
|
||||
. return 'INVALID';
|
||||
|
||||
/lex
|
||||
|
||||
%left '^'
|
||||
|
||||
%start start
|
||||
|
||||
%% /* language grammar */
|
||||
|
||||
start
|
||||
: timeline document 'EOF' { return $2; }
|
||||
| directive start
|
||||
;
|
||||
|
||||
document
|
||||
: /* empty */ { $$ = [] }
|
||||
| document line {$1.push($2);$$ = $1}
|
||||
;
|
||||
|
||||
line
|
||||
: SPACE statement { $$ = $2 }
|
||||
| statement { $$ = $1 }
|
||||
| NEWLINE { $$=[];}
|
||||
| EOF { $$=[];}
|
||||
;
|
||||
|
||||
directive
|
||||
: openDirective typeDirective closeDirective 'NEWLINE'
|
||||
| openDirective typeDirective ':' argDirective closeDirective 'NEWLINE'
|
||||
;
|
||||
|
||||
statement
|
||||
: title {yy.getCommonDb().setDiagramTitle($1.substr(6));$$=$1.substr(6);}
|
||||
| acc_title acc_title_value { $$=$2.trim();yy.getCommonDb().setAccTitle($$); }
|
||||
| acc_descr acc_descr_value { $$=$2.trim();yy.getCommonDb().setAccDescription($$); }
|
||||
| acc_descr_multiline_value { $$=$1.trim();yy.getCommonDb().setAccDescription($$); }
|
||||
| section {yy.addSection($1.substr(8));$$=$1.substr(8);}
|
||||
| period_statement
|
||||
| event_statement
|
||||
| directive
|
||||
;
|
||||
period_statement
|
||||
: period {yy.addTask($1,0,'');$$=$1;}
|
||||
;
|
||||
event_statement
|
||||
: event {yy.addEvent($1.substr(2));$$=$1;}
|
||||
;
|
||||
|
||||
|
||||
openDirective
|
||||
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
|
||||
;
|
||||
|
||||
typeDirective
|
||||
: type_directive { yy.parseDirective($1, 'type_directive'); }
|
||||
;
|
||||
|
||||
argDirective
|
||||
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
|
||||
;
|
||||
|
||||
closeDirective
|
||||
: close_directive { yy.parseDirective('}%%', 'close_directive', 'timeline'); }
|
||||
;
|
||||
|
||||
%%
|
||||
81
packages/mermaid/src/diagrams/timeline/styles.js
Normal file
81
packages/mermaid/src/diagrams/timeline/styles.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import { darken, lighten, isDark } from 'khroma';
|
||||
|
||||
const genSections = (options) => {
|
||||
let sections = '';
|
||||
|
||||
for (let i = 0; i < options.THEME_COLOR_LIMIT; i++) {
|
||||
options['lineColor' + i] = options['lineColor' + i] || options['cScaleInv' + i];
|
||||
if (isDark(options['lineColor' + i])) {
|
||||
options['lineColor' + i] = lighten(options['lineColor' + i], 20);
|
||||
} else {
|
||||
options['lineColor' + i] = darken(options['lineColor' + i], 20);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < options.THEME_COLOR_LIMIT; i++) {
|
||||
const sw = '' + (17 - 3 * i);
|
||||
sections += `
|
||||
.section-${i - 1} rect, .section-${i - 1} path, .section-${i - 1} circle, .section-${
|
||||
i - 1
|
||||
} path {
|
||||
fill: ${options['cScale' + i]};
|
||||
}
|
||||
.section-${i - 1} text {
|
||||
fill: ${options['cScaleLabel' + i]};
|
||||
}
|
||||
.node-icon-${i - 1} {
|
||||
font-size: 40px;
|
||||
color: ${options['cScaleLabel' + i]};
|
||||
}
|
||||
.section-edge-${i - 1}{
|
||||
stroke: ${options['cScale' + i]};
|
||||
}
|
||||
.edge-depth-${i - 1}{
|
||||
stroke-width: ${sw};
|
||||
}
|
||||
.section-${i - 1} line {
|
||||
stroke: ${options['cScaleInv' + i]} ;
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
.lineWrapper line{
|
||||
stroke: ${options['cScaleLabel' + i]} ;
|
||||
}
|
||||
|
||||
.disabled, .disabled circle, .disabled text {
|
||||
fill: lightgray;
|
||||
}
|
||||
.disabled text {
|
||||
fill: #efefef;
|
||||
}
|
||||
`;
|
||||
}
|
||||
return sections;
|
||||
};
|
||||
|
||||
const getStyles = (options) =>
|
||||
`
|
||||
.edge {
|
||||
stroke-width: 3;
|
||||
}
|
||||
${genSections(options)}
|
||||
.section-root rect, .section-root path, .section-root circle {
|
||||
fill: ${options.git0};
|
||||
}
|
||||
.section-root text {
|
||||
fill: ${options.gitBranchLabel0};
|
||||
}
|
||||
.icon-container {
|
||||
height:100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.edge {
|
||||
fill: none;
|
||||
}
|
||||
.eventWrapper {
|
||||
filter: brightness(120%);
|
||||
}
|
||||
`;
|
||||
export default getStyles;
|
||||
602
packages/mermaid/src/diagrams/timeline/svgDraw.js
Normal file
602
packages/mermaid/src/diagrams/timeline/svgDraw.js
Normal file
@@ -0,0 +1,602 @@
|
||||
import { arc as d3arc, select } from 'd3';
|
||||
const MAX_SECTIONS = 12;
|
||||
|
||||
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.class !== undefined) {
|
||||
rectElem.attr('class', rectData.class);
|
||||
}
|
||||
|
||||
return rectElem;
|
||||
};
|
||||
|
||||
export const drawFace = function (element, faceData) {
|
||||
const radius = 15;
|
||||
const circleElement = element
|
||||
.append('circle')
|
||||
.attr('cx', faceData.cx)
|
||||
.attr('cy', faceData.cy)
|
||||
.attr('class', 'face')
|
||||
.attr('r', radius)
|
||||
.attr('stroke-width', 2)
|
||||
.attr('overflow', 'visible');
|
||||
|
||||
const face = element.append('g');
|
||||
|
||||
//left eye
|
||||
face
|
||||
.append('circle')
|
||||
.attr('cx', faceData.cx - radius / 3)
|
||||
.attr('cy', faceData.cy - radius / 3)
|
||||
.attr('r', 1.5)
|
||||
.attr('stroke-width', 2)
|
||||
.attr('fill', '#666')
|
||||
.attr('stroke', '#666');
|
||||
|
||||
//right eye
|
||||
face
|
||||
.append('circle')
|
||||
.attr('cx', faceData.cx + radius / 3)
|
||||
.attr('cy', faceData.cy - radius / 3)
|
||||
.attr('r', 1.5)
|
||||
.attr('stroke-width', 2)
|
||||
.attr('fill', '#666')
|
||||
.attr('stroke', '#666');
|
||||
|
||||
/** @param {any} face */
|
||||
function smile(face) {
|
||||
const arc = d3arc()
|
||||
.startAngle(Math.PI / 2)
|
||||
.endAngle(3 * (Math.PI / 2))
|
||||
.innerRadius(radius / 2)
|
||||
.outerRadius(radius / 2.2);
|
||||
//mouth
|
||||
face
|
||||
.append('path')
|
||||
.attr('class', 'mouth')
|
||||
.attr('d', arc)
|
||||
.attr('transform', 'translate(' + faceData.cx + ',' + (faceData.cy + 2) + ')');
|
||||
}
|
||||
|
||||
/** @param {any} face */
|
||||
function sad(face) {
|
||||
const arc = d3arc()
|
||||
.startAngle((3 * Math.PI) / 2)
|
||||
.endAngle(5 * (Math.PI / 2))
|
||||
.innerRadius(radius / 2)
|
||||
.outerRadius(radius / 2.2);
|
||||
//mouth
|
||||
face
|
||||
.append('path')
|
||||
.attr('class', 'mouth')
|
||||
.attr('d', arc)
|
||||
.attr('transform', 'translate(' + faceData.cx + ',' + (faceData.cy + 7) + ')');
|
||||
}
|
||||
|
||||
/** @param {any} face */
|
||||
function ambivalent(face) {
|
||||
face
|
||||
.append('line')
|
||||
.attr('class', 'mouth')
|
||||
.attr('stroke', 2)
|
||||
.attr('x1', faceData.cx - 5)
|
||||
.attr('y1', faceData.cy + 7)
|
||||
.attr('x2', faceData.cx + 5)
|
||||
.attr('y2', faceData.cy + 7)
|
||||
.attr('class', 'mouth')
|
||||
.attr('stroke-width', '1px')
|
||||
.attr('stroke', '#666');
|
||||
}
|
||||
|
||||
if (faceData.score > 3) {
|
||||
smile(face);
|
||||
} else if (faceData.score < 3) {
|
||||
sad(face);
|
||||
} else {
|
||||
ambivalent(face);
|
||||
}
|
||||
|
||||
return circleElement;
|
||||
};
|
||||
|
||||
export const drawCircle = function (element, circleData) {
|
||||
const circleElement = element.append('circle');
|
||||
circleElement.attr('cx', circleData.cx);
|
||||
circleElement.attr('cy', circleData.cy);
|
||||
circleElement.attr('class', 'actor-' + circleData.pos);
|
||||
circleElement.attr('fill', circleData.fill);
|
||||
circleElement.attr('stroke', circleData.stroke);
|
||||
circleElement.attr('r', circleData.r);
|
||||
|
||||
if (circleElement.class !== undefined) {
|
||||
circleElement.attr('class', circleElement.class);
|
||||
}
|
||||
|
||||
if (circleData.title !== undefined) {
|
||||
circleElement.append('title').text(circleData.title);
|
||||
}
|
||||
|
||||
return circleElement;
|
||||
};
|
||||
|
||||
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 drawLabel = function (elem, txtObject) {
|
||||
/**
|
||||
* @param {any} x
|
||||
* @param {any} y
|
||||
* @param {any} width
|
||||
* @param {any} height
|
||||
* @param {any} cut
|
||||
*/
|
||||
function genPoints(x, y, width, height, cut) {
|
||||
return (
|
||||
x +
|
||||
',' +
|
||||
y +
|
||||
' ' +
|
||||
(x + width) +
|
||||
',' +
|
||||
y +
|
||||
' ' +
|
||||
(x + width) +
|
||||
',' +
|
||||
(y + height - cut) +
|
||||
' ' +
|
||||
(x + width - cut * 1.2) +
|
||||
',' +
|
||||
(y + height) +
|
||||
' ' +
|
||||
x +
|
||||
',' +
|
||||
(y + height)
|
||||
);
|
||||
}
|
||||
const polygon = elem.append('polygon');
|
||||
polygon.attr('points', genPoints(txtObject.x, txtObject.y, 50, 20, 7));
|
||||
polygon.attr('class', 'labelBox');
|
||||
|
||||
txtObject.y = txtObject.y + txtObject.labelMargin;
|
||||
txtObject.x = txtObject.x + 0.5 * txtObject.labelMargin;
|
||||
drawText(elem, txtObject);
|
||||
};
|
||||
|
||||
export const drawSection = function (elem, section, conf) {
|
||||
const g = elem.append('g');
|
||||
|
||||
const rect = getNoteRect();
|
||||
rect.x = section.x;
|
||||
rect.y = section.y;
|
||||
rect.fill = section.fill;
|
||||
rect.width = conf.width;
|
||||
rect.height = conf.height;
|
||||
rect.class = 'journey-section section-type-' + section.num;
|
||||
rect.rx = 3;
|
||||
rect.ry = 3;
|
||||
drawRect(g, rect);
|
||||
|
||||
_drawTextCandidateFunc(conf)(
|
||||
section.text,
|
||||
g,
|
||||
rect.x,
|
||||
rect.y,
|
||||
rect.width,
|
||||
rect.height,
|
||||
{ class: 'journey-section section-type-' + section.num },
|
||||
conf,
|
||||
section.colour
|
||||
);
|
||||
};
|
||||
|
||||
let taskCount = -1;
|
||||
/**
|
||||
* Draws an actor in the diagram with the attached line
|
||||
*
|
||||
* @param {any} elem The HTML element
|
||||
* @param {any} task The task to render
|
||||
* @param {any} conf The global configuration
|
||||
*/
|
||||
export const drawTask = function (elem, task, conf) {
|
||||
const center = task.x + conf.width / 2;
|
||||
const g = elem.append('g');
|
||||
taskCount++;
|
||||
const maxHeight = 300 + 5 * 30;
|
||||
g.append('line')
|
||||
.attr('id', 'task' + taskCount)
|
||||
.attr('x1', center)
|
||||
.attr('y1', task.y)
|
||||
.attr('x2', center)
|
||||
.attr('y2', maxHeight)
|
||||
.attr('class', 'task-line')
|
||||
.attr('stroke-width', '1px')
|
||||
.attr('stroke-dasharray', '4 2')
|
||||
.attr('stroke', '#666');
|
||||
|
||||
drawFace(g, {
|
||||
cx: center,
|
||||
cy: 300 + (5 - task.score) * 30,
|
||||
score: task.score,
|
||||
});
|
||||
|
||||
const rect = getNoteRect();
|
||||
rect.x = task.x;
|
||||
rect.y = task.y;
|
||||
rect.fill = task.fill;
|
||||
rect.width = conf.width;
|
||||
rect.height = conf.height;
|
||||
rect.class = 'task task-type-' + task.num;
|
||||
rect.rx = 3;
|
||||
rect.ry = 3;
|
||||
drawRect(g, rect);
|
||||
|
||||
let xPos = task.x + 14;
|
||||
// task.people.forEach((person) => {
|
||||
// const colour = task.actors[person].color;
|
||||
|
||||
// const circle = {
|
||||
// cx: xPos,
|
||||
// cy: task.y,
|
||||
// r: 7,
|
||||
// fill: colour,
|
||||
// stroke: '#000',
|
||||
// title: person,
|
||||
// pos: task.actors[person].position,
|
||||
// };
|
||||
|
||||
// drawCircle(g, circle);
|
||||
// xPos += 10;
|
||||
// });
|
||||
|
||||
_drawTextCandidateFunc(conf)(
|
||||
task.task,
|
||||
g,
|
||||
rect.x,
|
||||
rect.y,
|
||||
rect.width,
|
||||
rect.height,
|
||||
{ class: 'task' },
|
||||
conf,
|
||||
task.colour
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a background rectangle
|
||||
*
|
||||
* @param {any} elem The html element
|
||||
* @param {any} bounds The bounds of the drawing
|
||||
*/
|
||||
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,
|
||||
class: 'rect',
|
||||
});
|
||||
rectElem.lower();
|
||||
};
|
||||
|
||||
export const getTextObj = function () {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
fill: undefined,
|
||||
'text-anchor': 'start',
|
||||
width: 100,
|
||||
height: 100,
|
||||
textMargin: 0,
|
||||
rx: 0,
|
||||
ry: 0,
|
||||
};
|
||||
};
|
||||
|
||||
export const getNoteRect = function () {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
anchor: 'start',
|
||||
height: 100,
|
||||
rx: 0,
|
||||
ry: 0,
|
||||
};
|
||||
};
|
||||
|
||||
const _drawTextCandidateFunc = (function () {
|
||||
/**
|
||||
* @param {any} content
|
||||
* @param {any} g
|
||||
* @param {any} x
|
||||
* @param {any} y
|
||||
* @param {any} width
|
||||
* @param {any} height
|
||||
* @param {any} textAttrs
|
||||
* @param {any} colour
|
||||
*/
|
||||
function byText(content, g, x, y, width, height, textAttrs, colour) {
|
||||
const text = g
|
||||
.append('text')
|
||||
.attr('x', x + width / 2)
|
||||
.attr('y', y + height / 2 + 5)
|
||||
.style('font-color', colour)
|
||||
.style('text-anchor', 'middle')
|
||||
.text(content);
|
||||
_setTextAttrs(text, textAttrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} content
|
||||
* @param {any} g
|
||||
* @param {any} x
|
||||
* @param {any} y
|
||||
* @param {any} width
|
||||
* @param {any} height
|
||||
* @param {any} textAttrs
|
||||
* @param {any} conf
|
||||
* @param {any} colour
|
||||
*/
|
||||
function byTspan(content, g, x, y, width, height, textAttrs, conf, colour) {
|
||||
const { taskFontSize, taskFontFamily } = conf;
|
||||
|
||||
const lines = content.split(/<br\s*\/?>/gi);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const dy = i * taskFontSize - (taskFontSize * (lines.length - 1)) / 2;
|
||||
const text = g
|
||||
.append('text')
|
||||
.attr('x', x + width / 2)
|
||||
.attr('y', y)
|
||||
.attr('fill', colour)
|
||||
.style('text-anchor', 'middle')
|
||||
.style('font-size', taskFontSize)
|
||||
.style('font-family', taskFontFamily);
|
||||
text
|
||||
.append('tspan')
|
||||
.attr('x', x + width / 2)
|
||||
.attr('dy', dy)
|
||||
.text(lines[i]);
|
||||
|
||||
text
|
||||
.attr('y', y + height / 2.0)
|
||||
.attr('dominant-baseline', 'central')
|
||||
.attr('alignment-baseline', 'central');
|
||||
|
||||
_setTextAttrs(text, textAttrs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} content
|
||||
* @param {any} g
|
||||
* @param {any} x
|
||||
* @param {any} y
|
||||
* @param {any} width
|
||||
* @param {any} height
|
||||
* @param {any} textAttrs
|
||||
* @param {any} conf
|
||||
*/
|
||||
function byFo(content, g, x, y, width, height, textAttrs, conf) {
|
||||
const body = g.append('switch');
|
||||
const f = body
|
||||
.append('foreignObject')
|
||||
.attr('x', x)
|
||||
.attr('y', y)
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.attr('position', 'fixed');
|
||||
|
||||
const text = f
|
||||
.append('xhtml:div')
|
||||
.style('display', 'table')
|
||||
.style('height', '100%')
|
||||
.style('width', '100%');
|
||||
|
||||
text
|
||||
.append('div')
|
||||
.attr('class', 'label')
|
||||
.style('display', 'table-cell')
|
||||
.style('text-align', 'center')
|
||||
.style('vertical-align', 'middle')
|
||||
.text(content);
|
||||
|
||||
byTspan(content, body, x, y, width, height, textAttrs, conf);
|
||||
_setTextAttrs(text, textAttrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} toText
|
||||
* @param {any} fromTextAttrsDict
|
||||
*/
|
||||
function _setTextAttrs(toText, fromTextAttrsDict) {
|
||||
for (const key in fromTextAttrsDict) {
|
||||
if (key in fromTextAttrsDict) {
|
||||
// noinspection JSUnfilteredForInLoop
|
||||
toText.attr(key, fromTextAttrsDict[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return function (conf) {
|
||||
return conf.textPlacement === 'fo' ? byFo : conf.textPlacement === 'old' ? byText : byTspan;
|
||||
};
|
||||
})();
|
||||
|
||||
const initGraphics = function (graphics) {
|
||||
graphics
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', 'arrowhead')
|
||||
.attr('refX', 5)
|
||||
.attr('refY', 2)
|
||||
.attr('markerWidth', 6)
|
||||
.attr('markerHeight', 4)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M 0,0 V 4 L6,2 Z'); // this is actual shape for arrowhead
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} text The text to be wrapped
|
||||
* @param {number} width The max width of the text
|
||||
*/
|
||||
function wrap(text, width) {
|
||||
text.each(function () {
|
||||
var text = select(this),
|
||||
words = text
|
||||
.text()
|
||||
.split(/(\s+|<br>)/)
|
||||
.reverse(),
|
||||
word,
|
||||
line = [],
|
||||
lineHeight = 1.1, // ems
|
||||
y = text.attr('y'),
|
||||
dy = parseFloat(text.attr('dy')),
|
||||
tspan = text
|
||||
.text(null)
|
||||
.append('tspan')
|
||||
.attr('x', 0)
|
||||
.attr('y', y)
|
||||
.attr('dy', dy + 'em');
|
||||
for (let j = 0; j < words.length; j++) {
|
||||
word = words[words.length - 1 - j];
|
||||
line.push(word);
|
||||
tspan.text(line.join(' ').trim());
|
||||
if (tspan.node().getComputedTextLength() > width || word === '<br>') {
|
||||
line.pop();
|
||||
tspan.text(line.join(' ').trim());
|
||||
if (word === '<br>') {
|
||||
line = [''];
|
||||
} else {
|
||||
line = [word];
|
||||
}
|
||||
|
||||
tspan = text
|
||||
.append('tspan')
|
||||
.attr('x', 0)
|
||||
.attr('y', y)
|
||||
.attr('dy', lineHeight + 'em')
|
||||
.text(word);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const drawNode = function (elem, node, fullSection, conf) {
|
||||
const section = (fullSection % MAX_SECTIONS) - 1;
|
||||
const nodeElem = elem.append('g');
|
||||
node.section = section;
|
||||
nodeElem.attr(
|
||||
'class',
|
||||
(node.class ? node.class + ' ' : '') + 'timeline-node ' + ('section-' + section)
|
||||
);
|
||||
const bkgElem = nodeElem.append('g');
|
||||
|
||||
// Create the wrapped text element
|
||||
const textElem = nodeElem.append('g');
|
||||
|
||||
const txt = textElem
|
||||
.append('text')
|
||||
.text(node.descr)
|
||||
.attr('dy', '1em')
|
||||
.attr('alignment-baseline', 'middle')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('text-anchor', 'middle')
|
||||
.call(wrap, node.width);
|
||||
const bbox = txt.node().getBBox();
|
||||
const fontSize =
|
||||
conf.fontSize && conf.fontSize.replace ? conf.fontSize.replace('px', '') : conf.fontSize;
|
||||
node.height = bbox.height + fontSize * 1.1 * 0.5 + node.padding;
|
||||
node.height = Math.max(node.height, node.maxHeight);
|
||||
node.width = node.width + 2 * node.padding;
|
||||
|
||||
textElem.attr('transform', 'translate(' + node.width / 2 + ', ' + node.padding / 2 + ')');
|
||||
|
||||
// Create the background element
|
||||
defaultBkg(bkgElem, node, section, conf);
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
export const getVirtualNodeHeight = function (elem, node, conf) {
|
||||
const textElem = elem.append('g');
|
||||
const txt = textElem
|
||||
.append('text')
|
||||
.text(node.descr)
|
||||
.attr('dy', '1em')
|
||||
.attr('alignment-baseline', 'middle')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('text-anchor', 'middle')
|
||||
.call(wrap, node.width);
|
||||
const bbox = txt.node().getBBox();
|
||||
const fontSize =
|
||||
conf.fontSize && conf.fontSize.replace ? conf.fontSize.replace('px', '') : conf.fontSize;
|
||||
textElem.remove();
|
||||
return bbox.height + fontSize * 1.1 * 0.5 + node.padding;
|
||||
};
|
||||
|
||||
const defaultBkg = function (elem, node, section) {
|
||||
const rd = 5;
|
||||
elem
|
||||
.append('path')
|
||||
.attr('id', 'node-' + node.id)
|
||||
.attr('class', 'node-bkg node-' + node.type)
|
||||
.attr(
|
||||
'd',
|
||||
`M0 ${node.height - rd} v${-node.height + 2 * rd} q0,-5 5,-5 h${
|
||||
node.width - 2 * rd
|
||||
} q5,0 5,5 v${node.height - rd} H0 Z`
|
||||
);
|
||||
|
||||
elem
|
||||
.append('line')
|
||||
.attr('class', 'node-line-' + section)
|
||||
.attr('x1', 0)
|
||||
.attr('y1', node.height)
|
||||
.attr('x2', node.width)
|
||||
.attr('y2', node.height);
|
||||
};
|
||||
|
||||
export default {
|
||||
drawRect,
|
||||
drawCircle,
|
||||
drawSection,
|
||||
drawText,
|
||||
drawLabel,
|
||||
drawTask,
|
||||
drawBackgroundRect,
|
||||
getTextObj,
|
||||
getNoteRect,
|
||||
initGraphics,
|
||||
drawNode,
|
||||
getVirtualNodeHeight,
|
||||
};
|
||||
122
packages/mermaid/src/diagrams/timeline/timeline.spec.js
Normal file
122
packages/mermaid/src/diagrams/timeline/timeline.spec.js
Normal file
@@ -0,0 +1,122 @@
|
||||
import { parser as timeline } from './parser/timeline';
|
||||
import * as timelineDB from './timelineDb';
|
||||
// import { injectUtils } from './mermaidUtils';
|
||||
import * as _commonDb from '../../commonDb';
|
||||
import { parseDirective as _parseDirective } from '../../directiveUtils';
|
||||
|
||||
import {
|
||||
log,
|
||||
setLogLevel,
|
||||
getConfig,
|
||||
sanitizeText,
|
||||
setupGraphViewBox,
|
||||
} from '../../diagram-api/diagramAPI';
|
||||
|
||||
// injectUtils(
|
||||
// log,
|
||||
// setLogLevel,
|
||||
// getConfig,
|
||||
// sanitizeText,
|
||||
// setupGraphViewBox,
|
||||
// _commonDb,
|
||||
// _parseDirective
|
||||
// );
|
||||
|
||||
describe('when parsing a timeline ', function () {
|
||||
beforeEach(function () {
|
||||
timeline.yy = timelineDB;
|
||||
timelineDB.clear();
|
||||
setLogLevel('trace');
|
||||
});
|
||||
describe('Timeline', function () {
|
||||
it('TL-1 should handle a simple section definition abc-123', function () {
|
||||
let str = `timeline
|
||||
section abc-123`;
|
||||
|
||||
timeline.parse(str);
|
||||
expect(timelineDB.getSections()).to.deep.equal(['abc-123']);
|
||||
});
|
||||
|
||||
it('TL-2 should handle a simple section and only two tasks', function () {
|
||||
let str = `timeline
|
||||
section abc-123
|
||||
task1
|
||||
task2`;
|
||||
timeline.parse(str);
|
||||
timelineDB.getTasks().forEach((task) => {
|
||||
expect(task.section).to.equal('abc-123');
|
||||
expect(task.task).to.be.oneOf(['task1', 'task2']);
|
||||
});
|
||||
});
|
||||
|
||||
it('TL-3 should handle a two section and two coressponding tasks', function () {
|
||||
let str = `timeline
|
||||
section abc-123
|
||||
task1
|
||||
task2
|
||||
section abc-456
|
||||
task3
|
||||
task4`;
|
||||
timeline.parse(str);
|
||||
expect(timelineDB.getSections()).to.deep.equal(['abc-123', 'abc-456']);
|
||||
timelineDB.getTasks().forEach((task) => {
|
||||
expect(task.section).to.be.oneOf(['abc-123', 'abc-456']);
|
||||
expect(task.task).to.be.oneOf(['task1', 'task2', 'task3', 'task4']);
|
||||
if (task.section === 'abc-123') {
|
||||
expect(task.task).to.be.oneOf(['task1', 'task2']);
|
||||
} else {
|
||||
expect(task.task).to.be.oneOf(['task3', 'task4']);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('TL-4 should handle a section, and task and its events', function () {
|
||||
let str = `timeline
|
||||
section abc-123
|
||||
task1: event1
|
||||
task2: event2: event3
|
||||
`;
|
||||
timeline.parse(str);
|
||||
expect(timelineDB.getSections()[0]).to.deep.equal('abc-123');
|
||||
timelineDB.getTasks().forEach((t) => {
|
||||
switch (t.task.trim()) {
|
||||
case 'task1':
|
||||
expect(t.events).to.deep.equal(['event1']);
|
||||
break;
|
||||
|
||||
case 'task2':
|
||||
expect(t.events).to.deep.equal(['event2', 'event3']);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('TL-5 should handle a section, and task and its multi line events', function () {
|
||||
let str = `timeline
|
||||
section abc-123
|
||||
task1: event1
|
||||
task2: event2: event3
|
||||
: event4: event5
|
||||
`;
|
||||
timeline.parse(str);
|
||||
expect(timelineDB.getSections()[0]).to.deep.equal('abc-123');
|
||||
timelineDB.getTasks().forEach((t) => {
|
||||
switch (t.task.trim()) {
|
||||
case 'task1':
|
||||
expect(t.events).to.deep.equal(['event1']);
|
||||
break;
|
||||
|
||||
case 'task2':
|
||||
expect(t.events).to.deep.equal(['event2', 'event3', 'event4', 'event5']);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
108
packages/mermaid/src/diagrams/timeline/timelineDb.js
Normal file
108
packages/mermaid/src/diagrams/timeline/timelineDb.js
Normal file
@@ -0,0 +1,108 @@
|
||||
import { parseDirective as _parseDirective } from '../../directiveUtils';
|
||||
import * as commonDb from '../../commonDb';
|
||||
let currentSection = '';
|
||||
let currentTaskId = 0;
|
||||
|
||||
const sections = [];
|
||||
const tasks = [];
|
||||
const rawTasks = [];
|
||||
|
||||
export const getCommonDb = () => commonDb;
|
||||
|
||||
export const parseDirective = (statement, context, type) => {
|
||||
_parseDirective(this, statement, context, type);
|
||||
};
|
||||
|
||||
export const clear = function () {
|
||||
sections.length = 0;
|
||||
tasks.length = 0;
|
||||
currentSection = '';
|
||||
rawTasks.length = 0;
|
||||
commonDb.clear();
|
||||
};
|
||||
|
||||
export const addSection = function (txt) {
|
||||
currentSection = txt;
|
||||
sections.push(txt);
|
||||
};
|
||||
|
||||
export const getSections = function () {
|
||||
return sections;
|
||||
};
|
||||
|
||||
export const getTasks = function () {
|
||||
let allItemsProcessed = compileTasks();
|
||||
const maxDepth = 100;
|
||||
let iterationCount = 0;
|
||||
while (!allItemsProcessed && iterationCount < maxDepth) {
|
||||
allItemsProcessed = compileTasks();
|
||||
iterationCount++;
|
||||
}
|
||||
|
||||
tasks.push(...rawTasks);
|
||||
|
||||
return tasks;
|
||||
};
|
||||
|
||||
export const addTask = function (period, length, event) {
|
||||
const rawTask = {
|
||||
id: currentTaskId++,
|
||||
section: currentSection,
|
||||
type: currentSection,
|
||||
task: period,
|
||||
score: length ? length : 0,
|
||||
//if event is defined, then add it the events array
|
||||
events: event ? [event] : [],
|
||||
};
|
||||
rawTasks.push(rawTask);
|
||||
};
|
||||
|
||||
export const addEvent = function (event) {
|
||||
// fetch current task with currnetTaskId
|
||||
const currentTask = rawTasks.find((task) => task.id === currentTaskId - 1);
|
||||
//add event to the events array
|
||||
currentTask.events.push(event);
|
||||
};
|
||||
|
||||
export const addTaskOrg = function (descr) {
|
||||
const newTask = {
|
||||
section: currentSection,
|
||||
type: currentSection,
|
||||
description: descr,
|
||||
task: descr,
|
||||
classes: [],
|
||||
};
|
||||
tasks.push(newTask);
|
||||
};
|
||||
|
||||
/**
|
||||
* Compiles the raw tasks into a list of tasks with events
|
||||
* @returns {boolean} true if all items are processed
|
||||
* @private
|
||||
* @memberof timelineDb
|
||||
*/
|
||||
const compileTasks = function () {
|
||||
const compileTask = function (pos) {
|
||||
return rawTasks[pos].processed;
|
||||
};
|
||||
|
||||
let allProcessed = true;
|
||||
for (const [i, rawTask] of rawTasks.entries()) {
|
||||
compileTask(i);
|
||||
|
||||
allProcessed = allProcessed && rawTask.processed;
|
||||
}
|
||||
return allProcessed;
|
||||
};
|
||||
|
||||
export default {
|
||||
clear,
|
||||
getCommonDb,
|
||||
addSection,
|
||||
getSections,
|
||||
getTasks,
|
||||
addTask,
|
||||
addTaskOrg,
|
||||
addEvent,
|
||||
parseDirective,
|
||||
};
|
||||
336
packages/mermaid/src/diagrams/timeline/timelineRenderer.ts
Normal file
336
packages/mermaid/src/diagrams/timeline/timelineRenderer.ts
Normal file
@@ -0,0 +1,336 @@
|
||||
// @ts-nocheck TODO: fix file
|
||||
import { select } from 'd3';
|
||||
import svgDraw from './svgDraw';
|
||||
import { log } from '../../logger';
|
||||
import { getConfig } from '../../config';
|
||||
import { setupGraphViewbox } from '../../setupGraphViewbox';
|
||||
|
||||
export const setConf = function (cnf) {
|
||||
const keys = Object.keys(cnf);
|
||||
|
||||
keys.forEach(function (key) {
|
||||
conf[key] = cnf[key];
|
||||
});
|
||||
};
|
||||
|
||||
export const draw = function (text, id, version, diagObj) {
|
||||
//1. Fetch the configuration
|
||||
const conf = getConfig();
|
||||
const LEFT_MARGIN = conf.leftMargin ? conf.leftMargin : 50;
|
||||
|
||||
//2. Clear the diagram db before parsing
|
||||
diagObj.db.clear();
|
||||
|
||||
//3. Parse the diagram text
|
||||
diagObj.parser.parse(text + '\n');
|
||||
|
||||
log.debug('timeline', diagObj.db);
|
||||
|
||||
const securityLevel = conf.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 svg = root.select('#' + id);
|
||||
|
||||
svg.append('g');
|
||||
|
||||
//4. Fetch the diagram data
|
||||
const tasks = diagObj.db.getTasks();
|
||||
const title = diagObj.db.getCommonDb().getDiagramTitle();
|
||||
log.debug('task', tasks);
|
||||
|
||||
//5. Initialize the diagram
|
||||
svgDraw.initGraphics(svg);
|
||||
|
||||
// fetch Sections
|
||||
const sections = diagObj.db.getSections();
|
||||
log.debug('sections', sections);
|
||||
|
||||
let maxSectionHeight = 0;
|
||||
let maxTaskHeight = 0;
|
||||
//let sectionBeginX = 0;
|
||||
let depthY = 0;
|
||||
let sectionBeginY = 0;
|
||||
let masterX = 50 + LEFT_MARGIN;
|
||||
//sectionBeginX = masterX;
|
||||
let masterY = 50;
|
||||
sectionBeginY = 50;
|
||||
//draw sections
|
||||
let sectionNumber = 0;
|
||||
let hasSections = true;
|
||||
|
||||
//Calculate the max height of the sections
|
||||
sections.forEach(function (section) {
|
||||
const sectionNode = {
|
||||
number: sectionNumber,
|
||||
descr: section,
|
||||
section: sectionNumber,
|
||||
width: 150,
|
||||
padding: 20,
|
||||
maxHeight: maxSectionHeight,
|
||||
};
|
||||
const sectionHeight = svgDraw.getVirtualNodeHeight(svg, sectionNode, conf);
|
||||
log.debug('sectionHeight before draw', sectionHeight);
|
||||
maxSectionHeight = Math.max(maxSectionHeight, sectionHeight + 20);
|
||||
});
|
||||
|
||||
//tasks length and maxEventCount
|
||||
let maxEventCount = 0;
|
||||
let maxEventLineLength = 0;
|
||||
log.debug('tasks.length', tasks.length);
|
||||
//calculate max task height
|
||||
// for loop till tasks.length
|
||||
for (const [i, task] of tasks.entries()) {
|
||||
const taskNode = {
|
||||
number: i,
|
||||
descr: task,
|
||||
section: task.section,
|
||||
width: 150,
|
||||
padding: 20,
|
||||
maxHeight: maxTaskHeight,
|
||||
};
|
||||
const taskHeight = svgDraw.getVirtualNodeHeight(svg, taskNode, conf);
|
||||
log.debug('taskHeight before draw', taskHeight);
|
||||
maxTaskHeight = Math.max(maxTaskHeight, taskHeight + 20);
|
||||
|
||||
//calculate maxEventCount
|
||||
maxEventCount = Math.max(maxEventCount, task.events.length);
|
||||
//calculate maxEventLineLength
|
||||
let maxEventLineLengthTemp = 0;
|
||||
for (let j = 0; j < task.events.length; j++) {
|
||||
const event = task.events[j];
|
||||
const eventNode = {
|
||||
descr: event,
|
||||
section: task.section,
|
||||
number: task.section,
|
||||
width: 150,
|
||||
padding: 20,
|
||||
maxHeight: 50,
|
||||
};
|
||||
maxEventLineLengthTemp += svgDraw.getVirtualNodeHeight(svg, eventNode, conf);
|
||||
}
|
||||
maxEventLineLength = Math.max(maxEventLineLength, maxEventLineLengthTemp);
|
||||
}
|
||||
|
||||
log.debug('maxSectionHeight before draw', maxSectionHeight);
|
||||
log.debug('maxTaskHeight before draw', maxTaskHeight);
|
||||
|
||||
if (sections && sections.length > 0) {
|
||||
sections.forEach((section) => {
|
||||
const sectionNode = {
|
||||
number: sectionNumber,
|
||||
descr: section,
|
||||
section: sectionNumber,
|
||||
width: 150,
|
||||
padding: 20,
|
||||
maxHeight: maxSectionHeight,
|
||||
};
|
||||
log.debug('sectionNode', sectionNode);
|
||||
const sectionNodeWrapper = svg.append('g');
|
||||
const node = svgDraw.drawNode(sectionNodeWrapper, sectionNode, sectionNumber, conf);
|
||||
log.debug('sectionNode output', node);
|
||||
|
||||
sectionNodeWrapper.attr('transform', `translate(${masterX}, ${sectionBeginY})`);
|
||||
|
||||
masterY += maxSectionHeight + 50;
|
||||
|
||||
//draw tasks for this section
|
||||
//filter task where tasks.section == section
|
||||
const tasksForSection = tasks.filter((task) => task.section === section);
|
||||
if (tasksForSection.length > 0) {
|
||||
drawTasks(
|
||||
svg,
|
||||
tasksForSection,
|
||||
sectionNumber,
|
||||
masterX,
|
||||
masterY,
|
||||
maxTaskHeight,
|
||||
conf,
|
||||
maxEventCount,
|
||||
maxEventLineLength,
|
||||
maxSectionHeight,
|
||||
false
|
||||
);
|
||||
}
|
||||
// todo replace with total width of section and its tasks
|
||||
masterX += 200 * Math.max(tasksForSection.length, 1);
|
||||
|
||||
masterY = sectionBeginY;
|
||||
sectionNumber++;
|
||||
});
|
||||
} else {
|
||||
//draw tasks
|
||||
hasSections = false;
|
||||
drawTasks(
|
||||
svg,
|
||||
tasks,
|
||||
sectionNumber,
|
||||
masterX,
|
||||
masterY,
|
||||
maxTaskHeight,
|
||||
conf,
|
||||
maxEventCount,
|
||||
maxEventLineLength,
|
||||
maxSectionHeight,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// Get BBox of the diagram
|
||||
const box = svg.node().getBBox();
|
||||
log.debug('bounds', box);
|
||||
|
||||
if (title) {
|
||||
svg
|
||||
.append('text')
|
||||
.text(title)
|
||||
.attr('x', box.width / 2 - LEFT_MARGIN)
|
||||
.attr('font-size', '4ex')
|
||||
.attr('font-weight', 'bold')
|
||||
.attr('y', 20);
|
||||
}
|
||||
//5. Draw the diagram
|
||||
depthY = hasSections ? maxSectionHeight + maxTaskHeight + 150 : maxTaskHeight + 100;
|
||||
|
||||
const lineWrapper = svg.append('g').attr('class', 'lineWrapper');
|
||||
// Draw activity line
|
||||
lineWrapper
|
||||
.append('line')
|
||||
.attr('x1', LEFT_MARGIN)
|
||||
.attr('y1', depthY) // One section head + one task + margins
|
||||
.attr('x2', box.width + 3 * LEFT_MARGIN) // Subtract stroke width so arrow point is retained
|
||||
.attr('y2', depthY)
|
||||
.attr('stroke-width', 4)
|
||||
.attr('stroke', 'black')
|
||||
.attr('marker-end', 'url(#arrowhead)');
|
||||
|
||||
// Setup the view box and size of the svg element
|
||||
setupGraphViewbox(
|
||||
undefined,
|
||||
svg,
|
||||
conf.timeline.padding ? conf.timeline.padding : 50,
|
||||
conf.timeline.useMaxWidth ? conf.timeline.useMaxWidth : false
|
||||
);
|
||||
|
||||
// addSVGAccessibilityFields(diagObj.db, diagram, id);
|
||||
};
|
||||
|
||||
export const drawTasks = function (
|
||||
diagram,
|
||||
tasks,
|
||||
sectionColor,
|
||||
masterX,
|
||||
masterY,
|
||||
maxTaskHeight,
|
||||
conf,
|
||||
maxEventCount,
|
||||
maxEventLineLength,
|
||||
maxSectionHeight,
|
||||
isWithoutSections
|
||||
) {
|
||||
// Draw the tasks
|
||||
for (const task of tasks) {
|
||||
// create node from task
|
||||
const taskNode = {
|
||||
descr: task.task,
|
||||
section: sectionColor,
|
||||
number: sectionColor,
|
||||
width: 150,
|
||||
padding: 20,
|
||||
maxHeight: maxTaskHeight,
|
||||
};
|
||||
|
||||
log.debug('taskNode', taskNode);
|
||||
// create task wrapper
|
||||
const taskWrapper = diagram.append('g').attr('class', 'taskWrapper');
|
||||
const node = svgDraw.drawNode(taskWrapper, taskNode, sectionColor, conf);
|
||||
const taskHeight = node.height;
|
||||
//log task height
|
||||
log.debug('taskHeight after draw', taskHeight);
|
||||
taskWrapper.attr('transform', `translate(${masterX}, ${masterY})`);
|
||||
|
||||
// update max task height
|
||||
maxTaskHeight = Math.max(maxTaskHeight, taskHeight);
|
||||
|
||||
// if task has events, draw them
|
||||
if (task.events) {
|
||||
// draw a line between the task and the events
|
||||
const lineWrapper = diagram.append('g').attr('class', 'lineWrapper');
|
||||
let linelength = maxTaskHeight;
|
||||
//add margin to task
|
||||
masterY += 100;
|
||||
linelength =
|
||||
linelength + drawEvents(diagram, task.events, sectionColor, masterX, masterY, conf);
|
||||
masterY -= 100;
|
||||
|
||||
lineWrapper
|
||||
.append('line')
|
||||
.attr('x1', masterX + 190 / 2)
|
||||
.attr('y1', masterY + maxTaskHeight) // One section head + one task + margins
|
||||
.attr('x2', masterX + 190 / 2) // Subtract stroke width so arrow point is retained
|
||||
.attr(
|
||||
'y2',
|
||||
masterY +
|
||||
maxTaskHeight +
|
||||
(isWithoutSections ? maxTaskHeight : maxSectionHeight) +
|
||||
maxEventLineLength +
|
||||
120
|
||||
)
|
||||
.attr('stroke-width', 2)
|
||||
.attr('stroke', 'black')
|
||||
.attr('marker-end', 'url(#arrowhead)')
|
||||
.attr('stroke-dasharray', '5,5');
|
||||
}
|
||||
|
||||
masterX = masterX + 200;
|
||||
if (isWithoutSections && !getConfig().timeline.disableMulticolor) {
|
||||
sectionColor++;
|
||||
}
|
||||
}
|
||||
|
||||
// reset Y coordinate for next section
|
||||
masterY = masterY - 10;
|
||||
};
|
||||
|
||||
export const drawEvents = function (diagram, events, sectionColor, masterX, masterY, conf) {
|
||||
let maxEventHeight = 0;
|
||||
const eventBeginY = masterY;
|
||||
masterY = masterY + 100;
|
||||
// Draw the events
|
||||
for (const event of events) {
|
||||
// create node from event
|
||||
const eventNode = {
|
||||
descr: event,
|
||||
section: sectionColor,
|
||||
number: sectionColor,
|
||||
width: 150,
|
||||
padding: 20,
|
||||
maxHeight: 50,
|
||||
};
|
||||
|
||||
//log task node
|
||||
log.debug('eventNode', eventNode);
|
||||
// create event wrapper
|
||||
const eventWrapper = diagram.append('g').attr('class', 'eventWrapper');
|
||||
const node = svgDraw.drawNode(eventWrapper, eventNode, sectionColor, conf);
|
||||
const eventHeight = node.height;
|
||||
maxEventHeight = maxEventHeight + eventHeight;
|
||||
eventWrapper.attr('transform', `translate(${masterX}, ${masterY})`);
|
||||
masterY = masterY + 10 + eventHeight;
|
||||
}
|
||||
// set masterY back to eventBeginY
|
||||
masterY = eventBeginY;
|
||||
return maxEventHeight;
|
||||
};
|
||||
|
||||
export default {
|
||||
setConf,
|
||||
draw,
|
||||
};
|
||||
87
packages/mermaid/src/directiveUtils.ts
Normal file
87
packages/mermaid/src/directiveUtils.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import * as configApi from './config';
|
||||
|
||||
import { log } from './logger';
|
||||
import { directiveSanitizer } from './utils';
|
||||
|
||||
let currentDirective: { type?: string; args?: any } | undefined = {};
|
||||
|
||||
export const parseDirective = function (
|
||||
p: any,
|
||||
statement: string,
|
||||
context: string,
|
||||
type: string
|
||||
): void {
|
||||
log.debug('parseDirective is being called', statement, context, type);
|
||||
try {
|
||||
if (statement !== undefined) {
|
||||
statement = statement.trim();
|
||||
switch (context) {
|
||||
case 'open_directive':
|
||||
currentDirective = {};
|
||||
break;
|
||||
case 'type_directive':
|
||||
if (!currentDirective) {
|
||||
throw new Error('currentDirective is undefined');
|
||||
}
|
||||
currentDirective.type = statement.toLowerCase();
|
||||
break;
|
||||
case 'arg_directive':
|
||||
if (!currentDirective) {
|
||||
throw new Error('currentDirective is undefined');
|
||||
}
|
||||
currentDirective.args = JSON.parse(statement);
|
||||
break;
|
||||
case 'close_directive':
|
||||
handleDirective(p, currentDirective, type);
|
||||
currentDirective = undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Error while rendering sequenceDiagram directive: ${statement} jison context: ${context}`
|
||||
);
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
log.error(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDirective = function (p: any, directive: any, type: string): void {
|
||||
log.info(`Directive type=${directive.type} with args:`, directive.args);
|
||||
switch (directive.type) {
|
||||
case 'init':
|
||||
case 'initialize': {
|
||||
['config'].forEach((prop) => {
|
||||
if (directive.args[prop] !== undefined) {
|
||||
if (type === 'flowchart-v2') {
|
||||
type = 'flowchart';
|
||||
}
|
||||
directive.args[type] = directive.args[prop];
|
||||
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;
|
||||
}
|
||||
case 'wrap':
|
||||
case 'nowrap':
|
||||
if (p && p['setWrap']) {
|
||||
p.setWrap(directive.type === 'wrap');
|
||||
}
|
||||
break;
|
||||
case 'themeCss':
|
||||
log.warn('themeCss encountered');
|
||||
break;
|
||||
default:
|
||||
log.warn(
|
||||
`Unhandled directive: source: '%%{${directive.type}: ${JSON.stringify(
|
||||
directive.args ? directive.args : {}
|
||||
)}}%%`,
|
||||
directive
|
||||
);
|
||||
break;
|
||||
}
|
||||
};
|
||||
@@ -37,16 +37,14 @@ import { JSDOM } from 'jsdom';
|
||||
import type { Code, Root } from 'mdast';
|
||||
import { posix, dirname, relative, join } from 'path';
|
||||
import prettier from 'prettier';
|
||||
import { remark as remarkBuilder } from 'remark';
|
||||
import { remark } from 'remark';
|
||||
import remarkFrontmatter from 'remark-frontmatter';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import chokidar from 'chokidar';
|
||||
import mm from 'micromatch';
|
||||
// @ts-ignore No typescript declaration file
|
||||
import flatmap from 'unist-util-flatmap';
|
||||
|
||||
// support tables and other GitHub Flavored Markdown syntax in markdown
|
||||
const remark = remarkBuilder().use(remarkGfm);
|
||||
|
||||
const MERMAID_MAJOR_VERSION = (
|
||||
JSON.parse(readFileSync('../mermaid/package.json', 'utf8')).version as string
|
||||
).split('.')[0];
|
||||
@@ -90,7 +88,7 @@ const filesTransformed: Set<string> = new Set();
|
||||
|
||||
const generateHeader = (file: string): string => {
|
||||
// path from file in docs/* to repo root, e.g ../ or ../../ */
|
||||
const relativePath = relative(file, SOURCE_DOCS_DIR);
|
||||
const relativePath = relative(file, SOURCE_DOCS_DIR).replaceAll('\\', '/');
|
||||
const filePathFromRoot = posix.join('/packages/mermaid', file);
|
||||
const sourcePathRelativeToGenerated = posix.join(relativePath, filePathFromRoot);
|
||||
return `
|
||||
@@ -191,7 +189,7 @@ const transformIncludeStatements = (file: string, text: string): string => {
|
||||
// resolve includes - src https://github.com/vuejs/vitepress/blob/428eec3750d6b5648a77ac52d88128df0554d4d1/src/node/markdownToVue.ts#L65-L76
|
||||
return text.replace(includesRE, (m, m1) => {
|
||||
try {
|
||||
const includePath = join(dirname(file), m1);
|
||||
const includePath = join(dirname(file), m1).replaceAll('\\', '/');
|
||||
const content = readSyncedUTF8file(includePath);
|
||||
includedFiles.add(changeToFinalDocDir(includePath));
|
||||
return content;
|
||||
@@ -201,48 +199,86 @@ const transformIncludeStatements = (file: string, text: string): string => {
|
||||
});
|
||||
};
|
||||
|
||||
/** Options for {@link transformMarkdownAst} */
|
||||
interface TransformMarkdownAstOptions {
|
||||
/**
|
||||
* Used to indicate the original/source file.
|
||||
*/
|
||||
originalFilename: string;
|
||||
/** If `true`, add a warning that the file is autogenerated */
|
||||
addAutogeneratedWarning?: boolean;
|
||||
/**
|
||||
* If `true`, remove the YAML metadata from the Markdown input.
|
||||
* Generally, YAML metadata is only used for Vitepress.
|
||||
*/
|
||||
removeYAML?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform code blocks in a Markdown file.
|
||||
* Use remark.parse() to turn the given content (a String) into an AST.
|
||||
* Remark plugin that transforms mermaid repo markdown to Vitepress/GFM markdown.
|
||||
*
|
||||
* For any AST node that is a code block: transform it as needed:
|
||||
* - blocks marked as MERMAID_DIAGRAM_ONLY will be set to a 'mermaid' code block so it will be rendered as (only) a diagram
|
||||
* - blocks marked as MERMAID_EXAMPLE_KEYWORDS will be copied and the original node will be a code only block and the copy with be rendered as the diagram
|
||||
* - blocks marked as BLOCK_QUOTE_KEYWORDS will be transformed into block quotes
|
||||
*
|
||||
* Convert the AST back to a string and return it.
|
||||
* If `addAutogeneratedWarning` is `true`, generates a header stating that this file is autogenerated.
|
||||
*
|
||||
* @param content - the contents of a Markdown file
|
||||
* @returns the contents with transformed code blocks
|
||||
* @returns plugin function for Remark
|
||||
*/
|
||||
export const transformBlocks = (content: string): string => {
|
||||
const ast: Root = remark.parse(content);
|
||||
const astWithTransformedBlocks = flatmap(ast, (node: Code) => {
|
||||
if (node.type !== 'code' || !node.lang) {
|
||||
return [node]; // no transformation if this is not a code block
|
||||
export function transformMarkdownAst({
|
||||
originalFilename,
|
||||
addAutogeneratedWarning,
|
||||
removeYAML,
|
||||
}: TransformMarkdownAstOptions) {
|
||||
return (tree: Root, _file?: any): Root => {
|
||||
const astWithTransformedBlocks = flatmap(tree, (node: Code) => {
|
||||
if (node.type !== 'code' || !node.lang) {
|
||||
return [node]; // no transformation if this is not a code block
|
||||
}
|
||||
|
||||
if (node.lang === MERMAID_DIAGRAM_ONLY) {
|
||||
// Set the lang to 'mermaid' so it will be rendered as a diagram.
|
||||
node.lang = MERMAID_KEYWORD;
|
||||
return [node];
|
||||
} else if (MERMAID_EXAMPLE_KEYWORDS.includes(node.lang)) {
|
||||
// Return 2 nodes:
|
||||
// 1. the original node with the language now set to 'mermaid-example' (will be rendered as code), and
|
||||
// 2. a copy of the original node with the language set to 'mermaid' (will be rendered as a diagram)
|
||||
node.lang = MERMAID_CODE_ONLY_KEYWORD;
|
||||
return [node, Object.assign({}, node, { lang: MERMAID_KEYWORD })];
|
||||
}
|
||||
|
||||
// Transform these blocks into block quotes.
|
||||
if (BLOCK_QUOTE_KEYWORDS.includes(node.lang)) {
|
||||
return [remark.parse(transformToBlockQuote(node.value, node.lang, node.meta))];
|
||||
}
|
||||
|
||||
return [node]; // default is to do nothing to the node
|
||||
}) as Root;
|
||||
|
||||
if (addAutogeneratedWarning) {
|
||||
// Add the header to the start of the file
|
||||
const headerNode = remark.parse(generateHeader(originalFilename)).children[0];
|
||||
if (astWithTransformedBlocks.children[0].type === 'yaml') {
|
||||
// insert header after the YAML frontmatter if it exists
|
||||
astWithTransformedBlocks.children.splice(1, 0, headerNode);
|
||||
} else {
|
||||
astWithTransformedBlocks.children.unshift(headerNode);
|
||||
}
|
||||
}
|
||||
|
||||
if (node.lang === MERMAID_DIAGRAM_ONLY) {
|
||||
// Set the lang to 'mermaid' so it will be rendered as a diagram.
|
||||
node.lang = MERMAID_KEYWORD;
|
||||
return [node];
|
||||
} else if (MERMAID_EXAMPLE_KEYWORDS.includes(node.lang)) {
|
||||
// Return 2 nodes:
|
||||
// 1. the original node with the language now set to 'mermaid-example' (will be rendered as code), and
|
||||
// 2. a copy of the original node with the language set to 'mermaid' (will be rendered as a diagram)
|
||||
node.lang = MERMAID_CODE_ONLY_KEYWORD;
|
||||
return [node, Object.assign({}, node, { lang: MERMAID_KEYWORD })];
|
||||
if (removeYAML) {
|
||||
const firstNode = astWithTransformedBlocks.children[0];
|
||||
if (firstNode.type == 'yaml') {
|
||||
// YAML is currently only used for Vitepress metadata, so we should remove it for GFM output
|
||||
astWithTransformedBlocks.children.shift();
|
||||
}
|
||||
}
|
||||
|
||||
// Transform these blocks into block quotes.
|
||||
if (BLOCK_QUOTE_KEYWORDS.includes(node.lang)) {
|
||||
return [remark.parse(transformToBlockQuote(node.value, node.lang, node.meta))];
|
||||
}
|
||||
|
||||
return [node]; // default is to do nothing to the node
|
||||
});
|
||||
|
||||
return remark.stringify(astWithTransformedBlocks);
|
||||
};
|
||||
return astWithTransformedBlocks;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a markdown file and write the transformed file to the directory for published
|
||||
@@ -260,11 +296,18 @@ export const transformBlocks = (content: string): string => {
|
||||
*/
|
||||
const transformMarkdown = (file: string) => {
|
||||
const doc = injectPlaceholders(transformIncludeStatements(file, readSyncedUTF8file(file)));
|
||||
let transformed = transformBlocks(doc);
|
||||
if (!noHeader) {
|
||||
// Add the header to the start of the file
|
||||
transformed = `${generateHeader(file)}\n${transformed}`;
|
||||
}
|
||||
|
||||
let transformed = remark()
|
||||
.use(remarkGfm)
|
||||
.use(remarkFrontmatter, ['yaml']) // support YAML front-matter in Markdown
|
||||
.use(transformMarkdownAst, {
|
||||
// mermaid project specific plugin
|
||||
originalFilename: file,
|
||||
addAutogeneratedWarning: !noHeader,
|
||||
removeYAML: !noHeader,
|
||||
})
|
||||
.processSync(doc)
|
||||
.toString();
|
||||
|
||||
if (vitepress && file === 'src/docs/index.md') {
|
||||
// Skip transforming index if vitepress is enabled
|
||||
@@ -331,7 +374,7 @@ const getFilesFromGlobs = async (globs: string[]): Promise<string[]> => {
|
||||
};
|
||||
|
||||
/** Main method (entry point) */
|
||||
(async () => {
|
||||
const main = async () => {
|
||||
if (verifyOnly) {
|
||||
console.log('Verifying that all files are in sync with the source files');
|
||||
}
|
||||
@@ -400,4 +443,6 @@ const getFilesFromGlobs = async (globs: string[]): Promise<string[]> => {
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
void main();
|
||||
|
||||
@@ -1,40 +1,24 @@
|
||||
import { transformBlocks, transformToBlockQuote } from './docs.mjs';
|
||||
import { transformMarkdownAst, transformToBlockQuote } from './docs.mjs';
|
||||
|
||||
import { remark as remarkBuilder } from 'remark'; // import it this way so we can mock it
|
||||
|
||||
const remark = remarkBuilder();
|
||||
|
||||
vi.mock('remark', async (importOriginal) => {
|
||||
const { remark: originalRemarkBuilder } = (await importOriginal()) as {
|
||||
remark: typeof remarkBuilder;
|
||||
};
|
||||
|
||||
// make sure that both `docs.mts` and this test file are using the same remark
|
||||
// object so that we can mock it
|
||||
const sharedRemark = originalRemarkBuilder();
|
||||
return {
|
||||
remark: () => sharedRemark,
|
||||
};
|
||||
});
|
||||
import { remark } from 'remark'; // import it this way so we can mock it
|
||||
import remarkFrontmatter from 'remark-frontmatter';
|
||||
import { vi, afterEach, describe, it, expect } from 'vitest';
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const originalFilename = 'example-input-filename.md';
|
||||
const remarkBuilder = remark().use(remarkFrontmatter, ['yaml']); // support YAML front-matter in Markdown
|
||||
|
||||
describe('docs.mts', () => {
|
||||
describe('transformBlocks', () => {
|
||||
it('uses remark.parse to create the AST for the file ', () => {
|
||||
const remarkParseSpy = vi
|
||||
.spyOn(remark, 'parse')
|
||||
.mockReturnValue({ type: 'root', children: [] });
|
||||
const contents = 'Markdown file contents';
|
||||
transformBlocks(contents);
|
||||
expect(remarkParseSpy).toHaveBeenCalledWith(contents);
|
||||
});
|
||||
describe('transformMarkdownAst', () => {
|
||||
describe('checks each AST node', () => {
|
||||
it('does no transformation if there are no code blocks', async () => {
|
||||
const contents = 'Markdown file contents\n';
|
||||
const result = transformBlocks(contents);
|
||||
const result = (
|
||||
await remarkBuilder().use(transformMarkdownAst, { originalFilename }).process(contents)
|
||||
).toString();
|
||||
expect(result).toEqual(contents);
|
||||
});
|
||||
|
||||
@@ -46,8 +30,12 @@ describe('docs.mts', () => {
|
||||
const lang_keyword = 'mermaid-nocode';
|
||||
const contents = beforeCodeLine + '```' + lang_keyword + '\n' + diagram_text + '\n```\n';
|
||||
|
||||
it('changes the language to "mermaid"', () => {
|
||||
const result = transformBlocks(contents);
|
||||
it('changes the language to "mermaid"', async () => {
|
||||
const result = (
|
||||
await remarkBuilder()
|
||||
.use(transformMarkdownAst, { originalFilename })
|
||||
.process(contents)
|
||||
).toString();
|
||||
expect(result).toEqual(
|
||||
beforeCodeLine + '\n' + '```' + 'mermaid' + '\n' + diagram_text + '\n```\n'
|
||||
);
|
||||
@@ -61,8 +49,12 @@ describe('docs.mts', () => {
|
||||
const contents =
|
||||
beforeCodeLine + '```' + lang_keyword + '\n' + diagram_text + '\n```\n';
|
||||
|
||||
it('changes the language to "mermaid-example" and adds a copy of the code block with language = "mermaid"', () => {
|
||||
const result = transformBlocks(contents);
|
||||
it('changes the language to "mermaid-example" and adds a copy of the code block with language = "mermaid"', async () => {
|
||||
const result = (
|
||||
await remarkBuilder()
|
||||
.use(transformMarkdownAst, { originalFilename })
|
||||
.process(contents)
|
||||
).toString();
|
||||
expect(result).toEqual(
|
||||
beforeCodeLine +
|
||||
'\n' +
|
||||
@@ -77,16 +69,40 @@ describe('docs.mts', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('calls transformToBlockQuote with the node information', () => {
|
||||
it('calls transformToBlockQuote with the node information', async () => {
|
||||
const lang_keyword = 'note';
|
||||
const contents =
|
||||
beforeCodeLine + '```' + lang_keyword + '\n' + 'This is the text\n' + '```\n';
|
||||
|
||||
const result = transformBlocks(contents);
|
||||
const result = (
|
||||
await remarkBuilder().use(transformMarkdownAst, { originalFilename }).process(contents)
|
||||
).toString();
|
||||
expect(result).toEqual(beforeCodeLine + '\n> **Note**\n' + '> This is the text\n');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove YAML if `removeYAML` is true', async () => {
|
||||
const contents = `---
|
||||
title: Flowcharts Syntax
|
||||
---
|
||||
|
||||
This Markdown should be kept.
|
||||
`;
|
||||
const withYaml = (
|
||||
await remarkBuilder().use(transformMarkdownAst, { originalFilename }).process(contents)
|
||||
).toString();
|
||||
// no change
|
||||
expect(withYaml).toEqual(contents);
|
||||
|
||||
const withoutYaml = (
|
||||
await remarkBuilder()
|
||||
.use(transformMarkdownAst, { originalFilename, removeYAML: true })
|
||||
.process(contents)
|
||||
).toString();
|
||||
// no change
|
||||
expect(withoutYaml).toEqual('This Markdown should be kept.\n');
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformToBlockQuote', () => {
|
||||
|
||||
@@ -77,8 +77,8 @@ function sidebarAll() {
|
||||
],
|
||||
},
|
||||
...sidebarSyntax(),
|
||||
...sidebarEcosystem(),
|
||||
...sidebarConfig(),
|
||||
...sidebarMisc(),
|
||||
...sidebarCommunity(),
|
||||
];
|
||||
}
|
||||
@@ -104,6 +104,7 @@ function sidebarSyntax() {
|
||||
{ text: 'Gitgraph (Git) Diagram 🔥', link: '/syntax/gitgraph' },
|
||||
{ text: 'C4C Diagram (Context) Diagram 🦺⚠️', link: '/syntax/c4c' },
|
||||
{ text: 'Mindmaps 🔥', link: '/syntax/mindmap' },
|
||||
{ text: 'Timeline 🔥', link: '/syntax/timeline' },
|
||||
{ text: 'Other Examples', link: '/syntax/examples' },
|
||||
],
|
||||
},
|
||||
@@ -125,19 +126,20 @@ function sidebarConfig() {
|
||||
{ text: 'Accessibility', link: '/config/accessibility' },
|
||||
{ text: 'Mermaid CLI', link: '/config/mermaidCLI' },
|
||||
{ text: 'Advanced usage', link: '/config/n00b-advanced' },
|
||||
{ text: 'FAQ', link: '/config/faq' },
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function sidebarMisc() {
|
||||
function sidebarEcosystem() {
|
||||
return [
|
||||
{
|
||||
text: '📚 Misc',
|
||||
text: '📚 Ecosystem',
|
||||
collapsible: true,
|
||||
items: [
|
||||
{ text: 'Use-Cases and Integrations', link: '/misc/integrations' },
|
||||
{ text: 'FAQ', link: '/misc/faq' },
|
||||
{ text: 'Showcases', link: '/ecosystem/showcases' },
|
||||
{ text: 'Use-Cases and Integrations', link: '/ecosystem/integrations' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -10,9 +10,6 @@ export default {
|
||||
// register global components
|
||||
app.component('Mermaid', Mermaid);
|
||||
router.onBeforeRouteChange = (to) => {
|
||||
if (router.route.path !== '/') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const newPath = getRedirect(to);
|
||||
if (newPath) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import mermaid, { type MermaidConfig } from 'mermaid';
|
||||
import mindmap from '@mermaid-js/mermaid-mindmap';
|
||||
// import timeline from '@mermaid-js/mermaid-timeline';
|
||||
|
||||
const init = (async () => {
|
||||
try {
|
||||
await mermaid.registerExternalDiagrams([mindmap]);
|
||||
await mermaid.registerExternalDiagrams([mindmap, timeline]);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user