diff --git a/.eslintrc.cjs b/.eslintrc.cjs index e6f99a8bf9..273aa47b9f 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -38,12 +38,17 @@ module.exports = { 'lodash', 'unicorn', ], + ignorePatterns: [ + // this file is automatically generated by `pnpm run --filter mermaid types:build-config` + 'packages/mermaid/src/config.type.ts', + ], rules: { curly: 'error', 'no-console': 'error', 'no-prototype-builtins': 'off', 'no-unused-vars': 'off', 'cypress/no-async-tests': 'off', + '@typescript-eslint/consistent-type-imports': 'error', '@typescript-eslint/no-floating-promises': 'error', '@typescript-eslint/no-misused-promises': 'error', '@typescript-eslint/ban-ts-comment': [ @@ -123,6 +128,14 @@ module.exports = { files: ['*.{ts,tsx}'], plugins: ['tsdoc'], rules: { + 'no-restricted-syntax': [ + 'error', + { + selector: 'TSEnumDeclaration', + message: + 'Prefer using TypeScript union types over TypeScript enum, since TypeScript enums have a bunch of issues, see https://dev.to/dvddpl/whats-the-problem-with-typescript-enums-2okj', + }, + ], 'tsdoc/syntax': 'error', }, }, diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 1b84bfd45e..2f87cd60c4 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -53,8 +53,17 @@ body: Please fill out the info below. Note that you only need to fill out the relevant section value: |- - - Mermaid version: + - Mermaid version: - Browser and Version: [Chrome, Edge, Firefox] + - type: textarea + attributes: + label: Suggested Solutions + description: > + If applicable, suggest solutions that could resolve the bug. + It would help maintainers/contributors to not waste time looking for the solution. Even pointing the line causing the bug would be great! + placeholder: |- + - Variable `parser` in file is not initialised ... + - Add a new type for ... - type: textarea attributes: label: Additional Context diff --git a/codecov.yaml b/.github/codecov.yaml similarity index 56% rename from codecov.yaml rename to .github/codecov.yaml index b268d66800..950edb6a9a 100644 --- a/codecov.yaml +++ b/.github/codecov.yaml @@ -1,6 +1,17 @@ +codecov: + branch: develop + comment: layout: 'reach, diff, flags, files' behavior: default require_changes: false # if true: only post the comment if coverage changes require_base: no # [yes :: must have a base report to post] require_head: yes # [yes :: must have a head report to post] + +coverage: + status: + project: + off + # Turing off for now as code coverage isn't stable and causes unnecessary build failures. + # default: + # threshold: 2% diff --git a/.github/lychee.toml b/.github/lychee.toml new file mode 100644 index 0000000000..b13e536161 --- /dev/null +++ b/.github/lychee.toml @@ -0,0 +1,44 @@ +############################# Display ############################# + +# Verbose program output +# Accepts log level: "error", "warn", "info", "debug", "trace" +verbose = "debug" + +# Don't show interactive progress bar while checking links. +no_progress = true + +############################# Cache ############################### + +# Enable link caching. This can be helpful to avoid checking the same links on +# multiple runs. +cache = true + +# Discard all cached requests older than this duration. +max_cache_age = "1d" + +############################# Requests ############################ + +# Comma-separated list of accepted status codes for valid links. +accept = [200, 429] + +############################# Exclusions ########################## + +# Exclude URLs and mail addresses from checking (supports regex). +exclude = [ +# Network error: Forbidden +"https://codepen.io", + +# Timeout error, maybe Twitter has anti-bot defenses against GitHub's CI servers? +"https://twitter.com/mermaidjs_", + +# Don't check files that are generated during the build via `pnpm docs:code` +'packages/mermaid/src/docs/config/setup/*', + +# Ignore slack invite +"https://join.slack.com/" +] + +# Exclude all private IPs from checking. +# Equivalent to setting `exclude_private`, `exclude_link_local`, and +# `exclude_loopback` to true. +exclude_all_private = true diff --git a/.github/pr-labeler.yml b/.github/pr-labeler.yml index 077cc568b4..0bbd8db66a 100644 --- a/.github/pr-labeler.yml +++ b/.github/pr-labeler.yml @@ -1,3 +1,4 @@ -'Type: Bug / Error': 'bug/*' -'Type: Enhancement': 'feature/*' -'Type: Other': 'other/*' +'Type: Bug / Error': ['bug/*', fix/*] +'Type: Enhancement': ['feature/*', 'feat/*'] +'Type: Other': ['other/*', 'chore/*', 'test/*', 'refactor/*'] +'Area: Documentation': ['docs/*'] diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 3574c35997..ff34d24fd0 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -13,6 +13,6 @@ Describe the way your implementation works or what design decisions you made if Make sure you - [ ] :book: have read the [contribution guidelines](https://github.com/mermaid-js/mermaid/blob/develop/CONTRIBUTING.md) -- [ ] :computer: have added unit/e2e tests (if appropriate) -- [ ] :notebook: have added documentation (if appropriate) +- [ ] :computer: have added necessary unit/e2e tests. +- [ ] :notebook: have added documentation. Make sure [`MERMAID_RELEASE_VERSION`](https://github.com/mermaid-js/mermaid/blob/develop/packages/mermaid/src/docs/community/development.md#3-update-documentation) is used for all new features. - [ ] :bookmark: targeted `develop` branch diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 278151bca4..e650f8dd11 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,14 +1,27 @@ name-template: '$NEXT_PATCH_VERSION' tag-template: '$NEXT_PATCH_VERSION' categories: + - title: '🚨 **Breaking Changes**' + labels: + - 'Breaking Change' - title: '🚀 Features' labels: - 'Type: Enhancement' + - 'feature' # deprecated, new PRs shouldn't have this - title: '🐛 Bug Fixes' labels: - 'Type: Bug / Error' + - 'fix' # deprecated, new PRs shouldn't have this - title: '🧰 Maintenance' - label: 'Type: Other' + labels: + - 'Type: Other' + - 'chore' # deprecated, new PRs shouldn't have this + - title: '⚡️ Performance' + labels: + - 'Type: Performance' + - title: '📚 Documentation' + labels: + - 'Area: Documentation' change-template: '- $TITLE (#$NUMBER) @$AUTHOR' sort-by: title sort-direction: ascending diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index f3e440fce6..152b177ae9 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -1,14 +1,18 @@ name: Build Vitepress docs on: + push: + branches: + - master + - release/* pull_request: + merge_group: permissions: contents: read jobs: - # Build job - build: + build-docs: runs-on: ubuntu-latest steps: - name: Checkout @@ -25,5 +29,9 @@ jobs: - name: Install Packages run: pnpm install --frozen-lockfile + - name: Verify release verion + if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/release')) }} + run: pnpm --filter mermaid run docs:verify-version + - name: Run Build run: pnpm --filter mermaid run docs:build:vitepress diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2a70b5901d..eeb557ebb9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,6 +2,7 @@ name: Build on: push: {} + merge_group: pull_request: types: - opened @@ -12,7 +13,7 @@ permissions: contents: read jobs: - build: + build-mermaid: runs-on: ubuntu-latest strategy: matrix: diff --git a/.github/workflows/check-readme-in-sync.yml b/.github/workflows/check-readme-in-sync.yml index 13912e5b9f..5a8ca319b2 100644 --- a/.github/workflows/check-readme-in-sync.yml +++ b/.github/workflows/check-readme-in-sync.yml @@ -14,7 +14,7 @@ permissions: contents: read jobs: - check: + check-readme: runs-on: ubuntu-latest steps: - name: Checkout repository diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 396ff4e6ed..9f9f316c40 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -1,15 +1,16 @@ on: - push: {} + push: + merge_group: pull_request: types: - opened - synchronize - ready_for_review -name: Static analysis +name: Static analysis on Test files jobs: - test: + check-tests: runs-on: ubuntu-latest name: check tests if: github.repository_owner == 'mermaid-js' diff --git a/.github/workflows/e2e-applitools.yml b/.github/workflows/e2e-applitools.yml index 92f2f80b1c..5b19431421 100644 --- a/.github/workflows/e2e-applitools.yml +++ b/.github/workflows/e2e-applitools.yml @@ -19,7 +19,7 @@ env: USE_APPLI: ${{ secrets.APPLITOOLS_API_KEY && 'true' || '' }} jobs: - test: + e2e-applitools: runs-on: ubuntu-latest strategy: matrix: diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 64637c5fbe..3e6966677b 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,12 +1,15 @@ name: E2E -on: [push, pull_request] +on: + push: + pull_request: + merge_group: permissions: contents: read jobs: - build: + e2e: runs-on: ubuntu-latest strategy: fail-fast: false @@ -42,15 +45,18 @@ jobs: env: CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} VITEST_COVERAGE: true + CYPRESS_COMMIT: ${{ github.sha }} - name: Upload Coverage to Codecov uses: codecov/codecov-action@v3 - if: steps.cypress.conclusion == 'success' + # Run step only pushes to develop and pull_requests + if: ${{ steps.cypress.conclusion == 'success' && (github.event_name == 'pull_request' || github.ref == 'refs/heads/develop')}} with: files: coverage/cypress/lcov.info flags: e2e name: mermaid-codecov - fail_ci_if_error: true + fail_ci_if_error: false verbose: true + token: 6845cc80-77ee-4e17-85a1-026cd95e0766 - name: Upload Artifacts uses: actions/upload-artifact@v3 if: ${{ failure() && steps.cypress.conclusion == 'failure' }} diff --git a/.github/workflows/link-checker.yml b/.github/workflows/link-checker.yml index a2dc989e71..70580bfff1 100644 --- a/.github/workflows/link-checker.yml +++ b/.github/workflows/link-checker.yml @@ -20,7 +20,7 @@ on: - cron: '30 8 * * *' jobs: - linkChecker: + link-checker: runs-on: ubuntu-latest permissions: # lychee only uses the GITHUB_TOKEN to avoid rate-limiting @@ -39,10 +39,7 @@ jobs: uses: lycheeverse/lychee-action@v1.8.0 with: args: >- - --verbose - --no-progress - --cache - --max-cache-age 1d + --config .github/lychee.toml packages/mermaid/src/docs/**/*.md README.md README.zh-CN.md diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d4bf4afe81..f59c8af31d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,7 +1,8 @@ name: Lint on: - push: {} + push: + merge_group: pull_request: types: - opened @@ -52,6 +53,33 @@ jobs: exit 1 fi + - name: Verify `./src/config.type.ts` is in sync with `./src/schemas/config.schema.yaml` + shell: bash + run: | + if ! pnpm run --filter mermaid types:verify-config; then + ERROR_MESSAGE='Running `pnpm run --filter mermaid types:verify-config` failed.' + ERROR_MESSAGE+=' This should be fixed by running' + ERROR_MESSAGE+=' `pnpm run --filter mermaid types:build-config`' + ERROR_MESSAGE+=' on your local machine.' + echo "::error title=Lint failure::${ERROR_MESSAGE}" + # make sure to return an error exitcode so that GitHub actions shows a red-cross + exit 1 + fi + + - name: Verify no circular dependencies + working-directory: ./packages/mermaid + shell: bash + run: | + if ! pnpm run --filter mermaid checkCircle; then + ERROR_MESSAGE='Circular dependency detected.' + ERROR_MESSAGE+=' This should be fixed by removing the circular dependency.' + ERROR_MESSAGE+=' Run `pnpm run --filter mermaid checkCircle` on your local machine' + ERROR_MESSAGE+=' to see the circular dependency.' + echo "::error title=Lint failure::${ERROR_MESSAGE}" + # make sure to return an error exitcode so that GitHub actions shows a red-cross + exit 1 + fi + - name: Verify Docs id: verifyDocs working-directory: ./packages/mermaid diff --git a/.github/workflows/pr-labeler-config-validator.yml b/.github/workflows/pr-labeler-config-validator.yml index af5c477d64..ff5d8d0a1f 100644 --- a/.github/workflows/pr-labeler-config-validator.yml +++ b/.github/workflows/pr-labeler-config-validator.yml @@ -1,11 +1,15 @@ name: Validate PR Labeler Configuration on: - push: {} + push: + paths: + - .github/workflows/pr-labeler-config-validator.yml + - .github/workflows/pr-labeler.yml + - .github/pr-labeler.yml pull_request: - types: - - opened - - synchronize - - ready_for_review + paths: + - .github/workflows/pr-labeler-config-validator.yml + - .github/workflows/pr-labeler.yml + - .github/pr-labeler.yml jobs: pr-labeler: diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index b556c1b1d7..f63e587502 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -19,7 +19,7 @@ concurrency: jobs: # Build job - build: + build-docs: runs-on: ubuntu-latest steps: - name: Checkout @@ -48,11 +48,11 @@ jobs: path: packages/mermaid/src/vitepress/.vitepress/dist # Deployment job - deploy: + deploy-docs: environment: name: github-pages runs-on: ubuntu-latest - needs: build + needs: build-docs steps: - name: Deploy to GitHub Pages id: deployment diff --git a/.github/workflows/release-preview-publish.yml b/.github/workflows/release-preview-publish.yml index 5f4936ab68..221e3836ee 100644 --- a/.github/workflows/release-preview-publish.yml +++ b/.github/workflows/release-preview-publish.yml @@ -6,7 +6,7 @@ on: - 'release/**' jobs: - publish: + publish-preview: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6bae6b71f8..7c32795e8d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,12 +1,12 @@ name: Unit Tests -on: [push, pull_request] +on: [push, pull_request, merge_group] permissions: contents: read jobs: - build: + unit-test: runs-on: ubuntu-latest strategy: matrix: @@ -43,15 +43,12 @@ jobs: - name: Upload Coverage to Codecov uses: codecov/codecov-action@v3 + # Run step only pushes to develop and pull_requests + if: ${{ github.event_name == 'pull_request' || github.ref == 'refs/heads/develop' }} with: files: ./coverage/vitest/lcov.info flags: unit name: mermaid-codecov - fail_ci_if_error: true + fail_ci_if_error: false verbose: true - # Coveralls is throwing 500. Disabled for now. - # - name: Upload Coverage to Coveralls - # uses: coverallsapp/github-action@v2 - # with: - # github-token: ${{ secrets.GITHUB_TOKEN }} - # flag-name: unit + token: 6845cc80-77ee-4e17-85a1-026cd95e0766 diff --git a/.github/workflows/update-browserlist.yml b/.github/workflows/update-browserlist.yml index 4155ec988e..813a400b36 100644 --- a/.github/workflows/update-browserlist.yml +++ b/.github/workflows/update-browserlist.yml @@ -5,7 +5,7 @@ on: workflow_dispatch: jobs: - build: + update-browser-list: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 diff --git a/.gitignore b/.gitignore index 009c6dfac2..6a1cc85e51 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ stats/ **/contributor-names.json .pnpm-store .nyc_output + +demos/dev/** +!/demos/dev/example.html diff --git a/.lycheeignore b/.lycheeignore deleted file mode 100644 index d22d3cb153..0000000000 --- a/.lycheeignore +++ /dev/null @@ -1,17 +0,0 @@ -# These links are ignored by our link checker https://github.com/lycheeverse/lychee -# The file allows you to list multiple regular expressions for exclusion (one pattern per line). - -# Network error: Forbidden -https://codepen.io - -# Timeout error, maybe Twitter has anti-bot defenses against GitHub's CI servers? -https://twitter.com/mermaidjs_ - -# Don't check files that are generated during the build via `pnpm docs:code` -packages/mermaid/src/docs/config/setup/* - -# Ignore localhost -http://localhost:3333/ - -# Ignore slack invite -https://join.slack.com/ diff --git a/.prettierignore b/.prettierignore index 2ab91f93e8..8a9086315e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,4 +5,8 @@ coverage # Autogenerated by PNPM pnpm-lock.yaml stats -packages/mermaid/src/docs/.vitepress/components.d.ts +**/.vitepress/components.d.ts +**/.vitepress/cache +.nyc_output +# Autogenerated by `pnpm run --filter mermaid types:build-config` +packages/mermaid/src/config.type.ts diff --git a/.vite/build.ts b/.vite/build.ts index 85c9b7fa0f..b89df9e310 100644 --- a/.vite/build.ts +++ b/.vite/build.ts @@ -2,6 +2,7 @@ import { build, InlineConfig, type PluginOption } from 'vite'; import { resolve } from 'path'; import { fileURLToPath } from 'url'; import jisonPlugin from './jisonPlugin.js'; +import jsonSchemaPlugin from './jsonSchemaPlugin.js'; import { readFileSync } from 'fs'; import typescript from '@rollup/plugin-typescript'; import { visualizer } from 'rollup-plugin-visualizer'; @@ -121,6 +122,7 @@ export const getBuildConfig = ({ minify, core, watch, entryName }: BuildOptions) }, plugins: [ jisonPlugin(), + jsonSchemaPlugin(), // handles `.schema.yaml` files // @ts-expect-error According to the type definitions, rollup plugins are incompatible with vite typescript({ compilerOptions: { declaration: false } }), istanbul({ diff --git a/.vite/jsonSchemaPlugin.ts b/.vite/jsonSchemaPlugin.ts new file mode 100644 index 0000000000..671a9612e8 --- /dev/null +++ b/.vite/jsonSchemaPlugin.ts @@ -0,0 +1,150 @@ +import { load, JSON_SCHEMA } from 'js-yaml'; +import assert from 'node:assert'; +import Ajv2019, { type JSONSchemaType } from 'ajv/dist/2019.js'; +import { PluginOption } from 'vite'; + +import type { MermaidConfig, BaseDiagramConfig } from '../packages/mermaid/src/config.type.js'; + +/** + * All of the keys in the mermaid config that have a mermaid diagram config. + */ +const MERMAID_CONFIG_DIAGRAM_KEYS = [ + 'flowchart', + 'sequence', + 'gantt', + 'journey', + 'class', + 'state', + 'er', + 'pie', + 'quadrantChart', + 'requirement', + 'mindmap', + 'timeline', + 'gitGraph', + 'c4', + 'sankey', +] as const; + +/** + * Generate default values from the JSON Schema. + * + * AJV does not support nested default values yet (or default values with $ref), + * so we need to manually find them (this may be fixed in ajv v9). + * + * @param mermaidConfigSchema - The Mermaid JSON Schema to use. + * @returns The default mermaid config object. + */ +function generateDefaults(mermaidConfigSchema: JSONSchemaType) { + const ajv = new Ajv2019({ + useDefaults: true, + allowUnionTypes: true, + strict: true, + }); + + ajv.addKeyword({ + keyword: 'meta:enum', // used by jsonschema2md + errors: false, + }); + ajv.addKeyword({ + keyword: 'tsType', // used by json-schema-to-typescript + errors: false, + }); + + // ajv currently doesn't support nested default values, see https://github.com/ajv-validator/ajv/issues/1718 + // (may be fixed in v9) so we need to manually use sub-schemas + const mermaidDefaultConfig = {}; + + assert.ok(mermaidConfigSchema.$defs); + const baseDiagramConfig = mermaidConfigSchema.$defs.BaseDiagramConfig; + + for (const key of MERMAID_CONFIG_DIAGRAM_KEYS) { + const subSchemaRef = mermaidConfigSchema.properties[key].$ref; + const [root, defs, defName] = subSchemaRef.split('/'); + assert.strictEqual(root, '#'); + assert.strictEqual(defs, '$defs'); + const subSchema = { + $schema: mermaidConfigSchema.$schema, + $defs: mermaidConfigSchema.$defs, + ...mermaidConfigSchema.$defs[defName], + } as JSONSchemaType; + + const validate = ajv.compile(subSchema); + + mermaidDefaultConfig[key] = {}; + + for (const required of subSchema.required ?? []) { + if (subSchema.properties[required] === undefined && baseDiagramConfig.properties[required]) { + mermaidDefaultConfig[key][required] = baseDiagramConfig.properties[required].default; + } + } + if (!validate(mermaidDefaultConfig[key])) { + throw new Error( + `schema for subconfig ${key} does not have valid defaults! Errors were ${JSON.stringify( + validate.errors, + undefined, + 2 + )}` + ); + } + } + + const validate = ajv.compile(mermaidConfigSchema); + + if (!validate(mermaidDefaultConfig)) { + throw new Error( + `Mermaid config JSON Schema does not have valid defaults! Errors were ${JSON.stringify( + validate.errors, + undefined, + 2 + )}` + ); + } + + return mermaidDefaultConfig; +} + +/** + * Vite plugin that handles JSON Schemas saved as a `.schema.yaml` file. + * + * Use `my-example.schema.yaml?only-defaults=true` to only load the default values. + */ +export default function jsonSchemaPlugin(): PluginOption { + return { + name: 'json-schema-plugin', + transform(src: string, id: string) { + const idAsUrl = new URL(id, 'file:///'); + + if (!idAsUrl.pathname.endsWith('schema.yaml')) { + return; + } + + if (idAsUrl.searchParams.get('only-defaults')) { + const jsonSchema = load(src, { + filename: idAsUrl.pathname, + // only allow JSON types in our YAML doc (will probably be default in YAML 1.3) + // e.g. `true` will be parsed a boolean `true`, `True` will be parsed as string `"True"`. + schema: JSON_SCHEMA, + }) as JSONSchemaType; + return { + code: `export default ${JSON.stringify(generateDefaults(jsonSchema), undefined, 2)};`, + map: null, // no source map + }; + } else { + return { + code: `export default ${JSON.stringify( + load(src, { + filename: idAsUrl.pathname, + // only allow JSON types in our YAML doc (will probably be default in YAML 1.3) + // e.g. `true` will be parsed a boolean `true`, `True` will be parsed as string `"True"`. + schema: JSON_SCHEMA, + }), + undefined, + 2 + )};`, + map: null, // provide source map if available + }; + } + }, + }; +} diff --git a/.vscode/launch.json b/.vscode/launch.json index 83b80fa403..dc5ec94a10 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,7 +17,7 @@ "name": "Docs generation", "type": "node", "request": "launch", - "args": ["src/docs.mts"], + "args": ["scripts/docs.cli.mts"], "runtimeArgs": ["--loader", "ts-node/esm"], "cwd": "${workspaceRoot}/packages/mermaid", "skipFiles": ["/**", "**/node_modules/**"], diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000000..2f14a4b3d2 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,16 @@ +cff-version: 1.2.0 +title: 'Mermaid: Generate diagrams from markdown-like text' +message: >- + If you use this software, please cite it using the metadata from this file. +type: software +authors: + - family-names: Sveidqvist + given-names: Knut + - name: 'Contributors to Mermaid' +repository-code: 'https://github.com/mermaid-js/mermaid' +date-released: 2014-12-02 +url: 'https://mermaid.js.org/' +abstract: >- + JavaScript based diagramming and charting tool that renders Markdown-inspired + text definitions to create and modify diagrams dynamically. +license: MIT diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e8ac09325e..3142c57605 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,9 +26,14 @@ Install required packages: ```bash # npx is required for first install as volta support for pnpm is not added yet. npx pnpm install -pnpm test +pnpm test # run unit tests +pnpm dev # starts a dev server ``` +Open in your browser after starting the dev server. +You can also duplicate the `example.html` file in `demos/dev`, rename it and add your own mermaid code to it. +That will be served at . + ### Docker If you are using docker and docker-compose, you have self-documented `run` bash script, which is a convenient alias for docker-compose commands: @@ -64,8 +69,10 @@ eg: `feature/2945_state-diagram-new-arrow-florbs`, `bug/1123_fix_random_ugly_red Documentation is necessary for all non bugfix/refactoring changes. -Only make changes to files are in [`/packages/mermaid/src/docs`](packages/mermaid/src/docs) +Only make changes to files that are in [`/packages/mermaid/src/docs`](packages/mermaid/src/docs) + +**_DO NOT CHANGE FILES IN `/docs` MANUALLY_** -**_DO NOT CHANGE FILES IN `/docs`_** +The `/docs` folder will be rebuilt and committed as part of a pre-commit hook. [Join our slack community if you want closer contact!](https://join.slack.com/t/mermaid-talk/shared_invite/enQtNzc4NDIyNzk4OTAyLWVhYjQxOTI2OTg4YmE1ZmJkY2Y4MTU3ODliYmIwOTY3NDJlYjA0YjIyZTdkMDMyZTUwOGI0NjEzYmEwODcwOTE) diff --git a/README.md b/README.md index f1df0966bd..98741a6894 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Use Mermaid with your favorite applications, check out the list of [Integrations You can also use Mermaid within [GitHub](https://github.blog/2022-02-14-include-diagrams-markdown-files-mermaid/) as well many of your other favorite applications—check out the list of [Integrations and Usages of Mermaid](./docs/ecosystem/integrations.md). -For a more detailed introduction to Mermaid and some of its more basic uses, look to the [Beginner's Guide](./docs/community/n00b-overview.md), [Usage](./docs/config/usage.md) and [Tutorials](./docs/config/Tutorials.md). +For a more detailed introduction to Mermaid and some of its more basic uses, look to the [Beginner's Guide](./docs/intro/getting-started.md), [Usage](./docs/config/usage.md) and [Tutorials](./docs/config/Tutorials.md). In our release process we rely heavily on visual regression tests using [applitools](https://applitools.com/). Applitools is a great service which has been easy to use and integrate with our tests. @@ -386,7 +386,7 @@ Update version number in `package.json`. npm publish ``` -The above command generates files into the `dist` folder and publishes them to npmjs.org. +The above command generates files into the `dist` folder and publishes them to . ## Related projects @@ -402,7 +402,7 @@ Detailed information about how to contribute can be found in the [contribution g ## Security and safe diagrams -For public sites, it can be precarious to retrieve text from users on the internet, storing that content for presentation in a browser at a later stage. The reason is that the user content can contain embedded malicious scripts that will run when the data is presented. For Mermaid this is a risk, specially as mermaid diagrams contain many characters that are used in html which makes the standard sanitation unusable as it also breaks the diagrams. We still make an effort to sanitise the incoming code and keep refining the process but it is hard to guarantee that there are no loop holes. +For public sites, it can be precarious to retrieve text from users on the internet, storing that content for presentation in a browser at a later stage. The reason is that the user content can contain embedded malicious scripts that will run when the data is presented. For Mermaid this is a risk, specially as mermaid diagrams contain many characters that are used in html which makes the standard sanitation unusable as it also breaks the diagrams. We still make an effort to sanitize the incoming code and keep refining the process but it is hard to guarantee that there are no loop holes. As an extra level of security for sites with external users we are happy to introduce a new security level in which the diagram is rendered in a sandboxed iframe preventing javascript in the code from being executed. This is a great step forward for better security. diff --git a/README.zh-CN.md b/README.zh-CN.md index 9af34998d5..80e7155090 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -55,7 +55,7 @@ Mermaid 通过允许用户创建便于修改的图表来解决这一难题,它 Mermaid 甚至能让非程序员也能通过 [Mermaid Live Editor](https://mermaid.live/) 轻松创建详细的图表。
你可以访问 [教程](./docs/config/Tutorials.md) 来查看 Live Editor 的视频教程,也可以查看 [Mermaid 的集成和使用](./docs/ecosystem/integrations.md) 这个清单来检查你的文档工具是否已经集成了 Mermaid 支持。 -如果想要查看关于 Mermaid 更详细的介绍及基础使用方式,可以查看 [入门指引](./docs/community/n00b-overview.md), [用法](./docs/config/usage.md) 和 [教程](./docs/config/Tutorials.md). +如果想要查看关于 Mermaid 更详细的介绍及基础使用方式,可以查看 [入门指引](./docs/intro/getting-started.md), [用法](./docs/config/usage.md) 和 [教程](./docs/config/Tutorials.md). @@ -322,7 +322,7 @@ Rel(SystemC, customerA, "Sends e-mails to") npm publish ``` -以上的命令会将文件打包到 `dist` 目录并发布至 npmjs.org. +以上的命令会将文件打包到 `dist` 目录并发布至 . ## 相关项目 diff --git a/__mocks__/pieRenderer.ts b/__mocks__/pieRenderer.ts new file mode 100644 index 0000000000..439800f8c5 --- /dev/null +++ b/__mocks__/pieRenderer.ts @@ -0,0 +1,8 @@ +/** + * Mocked pie (picChart) diagram renderer + */ +import { vi } from 'vitest'; + +const draw = vi.fn().mockImplementation(() => ''); + +export const renderer = { draw }; diff --git a/__mocks__/pieRenderer.js b/__mocks__/sankeyRenderer.js similarity index 76% rename from __mocks__/pieRenderer.js rename to __mocks__/sankeyRenderer.js index 317c69901d..76324c93f1 100644 --- a/__mocks__/pieRenderer.js +++ b/__mocks__/sankeyRenderer.js @@ -1,5 +1,5 @@ /** - * Mocked pie (picChart) diagram renderer + * Mocked Sankey diagram renderer */ import { vi } from 'vitest'; diff --git a/cSpell.json b/cSpell.json index ff9ef10743..e8d718316c 100644 --- a/cSpell.json +++ b/cSpell.json @@ -38,10 +38,16 @@ "docsy", "doku", "dompurify", + "dont", + "doublecircle", "edgechromium", + "elems", "elkjs", + "elle", "faber", "flatmap", + "foswiki", + "frontmatter", "ftplugin", "gantt", "gitea", @@ -51,6 +57,7 @@ "graphviz", "grav", "greywolf", + "gzipped", "huynh", "huynhicode", "inkdrop", @@ -84,7 +91,11 @@ "mkdocs", "mmorel", "mult", + "neurodiverse", "nextra", + "nikolay", + "nirname", + "npmjs", "orlandoni", "pathe", "pbrolin", @@ -98,10 +109,14 @@ "ranksep", "rect", "rects", + "reda", "redmine", + "regexes", "rehype", "roledescription", + "rozhkov", "sandboxed", + "sankey", "setupgraphviewbox", "shiki", "sidharth", @@ -116,6 +131,7 @@ "stylis", "subhash-halder", "substate", + "sulais", "sveidqvist", "swimm", "techn", @@ -126,6 +142,7 @@ "tsdoc", "tuleap", "tylerlong", + "typora", "ugge", "unist", "unocss", @@ -139,7 +156,9 @@ "vueuse", "xlink", "yash", - "zenuml" + "yokozuna", + "zenuml", + "zune" ], "patterns": [ { "name": "Markdown links", "pattern": "\\((.*)\\)", "description": "" }, diff --git a/cypress/helpers/util.js b/cypress/helpers/util.js deleted file mode 100644 index 4d13b35901..0000000000 --- a/cypress/helpers/util.js +++ /dev/null @@ -1,93 +0,0 @@ -const utf8ToB64 = (str) => { - return window.btoa(unescape(encodeURIComponent(str))); -}; - -const batchId = 'mermaid-batch' + new Date().getTime(); - -export const mermaidUrl = (graphStr, options, api) => { - const obj = { - code: graphStr, - mermaid: options, - }; - const objStr = JSON.stringify(obj); - let url = 'http://localhost:9000/e2e.html?graph=' + utf8ToB64(objStr); - if (api) { - url = 'http://localhost:9000/xss.html?graph=' + graphStr; - } - - if (options.listUrl) { - cy.log(options.listId, ' ', url); - } - - return url; -}; - -export const imgSnapshotTest = (graphStr, _options = {}, api = false, validation = undefined) => { - 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 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); -}; - -export const openURLAndVerifyRendering = (url, options, validation = undefined) => { - const useAppli = Cypress.env('useAppli'); - const name = (options.name || cy.state('runnable').fullTitle()).replace(/\s+/g, '-'); - - if (useAppli) { - cy.log('Opening eyes ' + Cypress.spec.name + ' --- ' + name); - cy.eyesOpen({ - appName: 'Mermaid', - testName: name, - batchName: Cypress.spec.name, - batchId: batchId + Cypress.spec.name, - }); - } - - cy.visit(url); - cy.window().should('have.property', 'rendered', true); - cy.get('svg').should('be.visible'); - - if (validation) { - cy.get('svg').should(validation); - } - - if (useAppli) { - cy.log('Check eyes' + Cypress.spec.name); - cy.eyesCheckWindow('Click!'); - cy.log('Closing eyes' + Cypress.spec.name); - cy.eyesClose(); - } else { - cy.matchImageSnapshot(name); - } -}; diff --git a/cypress/helpers/util.ts b/cypress/helpers/util.ts new file mode 100644 index 0000000000..4160f4cbdc --- /dev/null +++ b/cypress/helpers/util.ts @@ -0,0 +1,136 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Buffer } from 'buffer'; +import type { MermaidConfig } from '../../packages/mermaid/src/config.type.js'; + +interface CypressConfig { + listUrl?: boolean; + listId?: string; + name?: string; +} +type CypressMermaidConfig = MermaidConfig & CypressConfig; + +interface CodeObject { + code: string; + mermaid: CypressMermaidConfig; +} + +const utf8ToB64 = (str: string): string => { + return Buffer.from(decodeURIComponent(encodeURIComponent(str))).toString('base64'); +}; + +const batchId: string = + 'mermaid-batch-' + + (Cypress.env('useAppli') + ? Date.now().toString() + : Cypress.env('CYPRESS_COMMIT') || Date.now().toString()); + +export const mermaidUrl = ( + graphStr: string, + options: CypressMermaidConfig, + api: boolean +): string => { + const codeObject: CodeObject = { + code: graphStr, + mermaid: options, + }; + const objStr: string = JSON.stringify(codeObject); + let url = `http://localhost:9000/e2e.html?graph=${utf8ToB64(objStr)}`; + if (api) { + url = `http://localhost:9000/xss.html?graph=${graphStr}`; + } + + if (options.listUrl) { + cy.log(options.listId, ' ', url); + } + + return url; +}; + +export const imgSnapshotTest = ( + graphStr: string, + _options: CypressMermaidConfig = {}, + api = false, + validation?: any +): void => { + cy.log(JSON.stringify(_options)); + const options: CypressMermaidConfig = 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 = 16; + } + + const url: string = mermaidUrl(graphStr, options, api); + openURLAndVerifyRendering(url, options, validation); +}; + +export const urlSnapshotTest = ( + url: string, + _options: CypressMermaidConfig, + _api = false, + validation?: any +): void => { + const options: CypressMermaidConfig = Object.assign(_options); + openURLAndVerifyRendering(url, options, validation); +}; + +export const renderGraph = ( + graphStr: string, + options: CypressMermaidConfig = {}, + api = false +): void => { + const url: string = mermaidUrl(graphStr, options, api); + openURLAndVerifyRendering(url, options); +}; + +export const openURLAndVerifyRendering = ( + url: string, + options: CypressMermaidConfig, + validation?: any +): void => { + const useAppli: boolean = Cypress.env('useAppli'); + const name: string = (options.name || cy.state('runnable').fullTitle()).replace(/\s+/g, '-'); + + if (useAppli) { + cy.log(`Opening eyes ${Cypress.spec.name} --- ${name}`); + cy.eyesOpen({ + appName: 'Mermaid', + testName: name, + batchName: Cypress.spec.name, + batchId: batchId + Cypress.spec.name, + }); + } + + cy.visit(url); + cy.window().should('have.property', 'rendered', true); + cy.get('svg').should('be.visible'); + + if (validation) { + cy.get('svg').should(validation); + } + + if (useAppli) { + cy.log(`Check eyes ${Cypress.spec.name}`); + cy.eyesCheckWindow('Click!'); + cy.log(`Closing eyes ${Cypress.spec.name}`); + cy.eyesClose(); + } else { + cy.matchImageSnapshot(name); + } +}; diff --git a/cypress/integration/other/configuration.spec.js b/cypress/integration/other/configuration.spec.js index 6df7edd843..7cbc5d1059 100644 --- a/cypress/integration/other/configuration.spec.js +++ b/cypress/integration/other/configuration.spec.js @@ -1,4 +1,4 @@ -import { renderGraph } from '../../helpers/util.js'; +import { renderGraph } from '../../helpers/util.ts'; describe('Configuration', () => { describe('arrowMarkerAbsolute', () => { it('should handle default value false of arrowMarkerAbsolute', () => { diff --git a/cypress/integration/other/external-diagrams.spec.js b/cypress/integration/other/external-diagrams.spec.js index 4ade11e81a..704222f2fb 100644 --- a/cypress/integration/other/external-diagrams.spec.js +++ b/cypress/integration/other/external-diagrams.spec.js @@ -1,4 +1,4 @@ -import { urlSnapshotTest } from '../../helpers/util.js'; +import { urlSnapshotTest } from '../../helpers/util.ts'; describe('mermaid', () => { describe('registerDiagram', () => { diff --git a/cypress/integration/other/ghsa.spec.js b/cypress/integration/other/ghsa.spec.js index 912f357280..48eb57a916 100644 --- a/cypress/integration/other/ghsa.spec.js +++ b/cypress/integration/other/ghsa.spec.js @@ -1,4 +1,4 @@ -import { urlSnapshotTest, openURLAndVerifyRendering } from '../../helpers/util.js'; +import { urlSnapshotTest, openURLAndVerifyRendering } from '../../helpers/util.ts'; describe('CSS injections', () => { it('should not allow CSS injections outside of the diagram', () => { diff --git a/cypress/integration/other/xss.spec.js b/cypress/integration/other/xss.spec.js index 76b2c47f2c..fa4ca4fc80 100644 --- a/cypress/integration/other/xss.spec.js +++ b/cypress/integration/other/xss.spec.js @@ -1,4 +1,4 @@ -import { mermaidUrl } from '../../helpers/util.js'; +import { mermaidUrl } from '../../helpers/util.ts'; describe('XSS', () => { it('should handle xss in tags', () => { const str = diff --git a/cypress/integration/rendering/appli.spec.js b/cypress/integration/rendering/appli.spec.js index 462fe869cd..5def968157 100644 --- a/cypress/integration/rendering/appli.spec.js +++ b/cypress/integration/rendering/appli.spec.js @@ -1,4 +1,4 @@ -import { imgSnapshotTest } from '../../helpers/util.js'; +import { imgSnapshotTest } from '../../helpers/util.ts'; describe('Git Graph diagram', () => { it('1: should render a simple gitgraph with commit on main branch', () => { diff --git a/cypress/integration/rendering/c4.spec.js b/cypress/integration/rendering/c4.spec.js index 0cf128ff63..59af6504b9 100644 --- a/cypress/integration/rendering/c4.spec.js +++ b/cypress/integration/rendering/c4.spec.js @@ -1,4 +1,4 @@ -import { imgSnapshotTest, renderGraph } from '../../helpers/util.js'; +import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts'; describe('C4 diagram', () => { it('should render a simple C4Context diagram', () => { diff --git a/cypress/integration/rendering/classDiagram-v2.spec.js b/cypress/integration/rendering/classDiagram-v2.spec.js index 2e7a1cbd72..37e9cada02 100644 --- a/cypress/integration/rendering/classDiagram-v2.spec.js +++ b/cypress/integration/rendering/classDiagram-v2.spec.js @@ -1,4 +1,4 @@ -import { imgSnapshotTest } from '../../helpers/util.js'; +import { imgSnapshotTest } from '../../helpers/util.ts'; describe('Class diagram V2', () => { it('0: should render a simple class diagram', () => { imgSnapshotTest( @@ -386,12 +386,11 @@ describe('Class diagram V2', () => { { logLevel: 1, flowchart: { htmlLabels: false } } ); }); - - it('18: should handle the direction statement with LR', () => { + it('17a: should handle the direction statement with BT', () => { imgSnapshotTest( ` classDiagram - direction LR + direction BT class Student { -idCard : IdCard } @@ -410,11 +409,11 @@ describe('Class diagram V2', () => { { logLevel: 1, flowchart: { htmlLabels: false } } ); }); - it('17a: should handle the direction statement with BT', () => { + it('17b: should handle the direction statement with RL', () => { imgSnapshotTest( ` classDiagram - direction BT + direction RL class Student { -idCard : IdCard } @@ -433,11 +432,12 @@ describe('Class diagram V2', () => { { logLevel: 1, flowchart: { htmlLabels: false } } ); }); - it('17b: should handle the direction statement with RL', () => { + + it('18a: should handle the direction statement with LR', () => { imgSnapshotTest( ` classDiagram - direction RL + direction LR class Student { -idCard : IdCard } @@ -457,7 +457,7 @@ describe('Class diagram V2', () => { ); }); - it('18: should render a simple class diagram with notes', () => { + it('18b: should render a simple class diagram with notes', () => { imgSnapshotTest( ` classDiagram-v2 @@ -562,4 +562,13 @@ class C13["With Città foreign language"] ` ); }); + it('should render a simple class diagram with no members', () => { + imgSnapshotTest( + ` + classDiagram-v2 + class Class10 + `, + { logLevel: 1, flowchart: { htmlLabels: false } } + ); + }); }); diff --git a/cypress/integration/rendering/classDiagram.spec.js b/cypress/integration/rendering/classDiagram.spec.js index 427b4cf0b6..a23430b083 100644 --- a/cypress/integration/rendering/classDiagram.spec.js +++ b/cypress/integration/rendering/classDiagram.spec.js @@ -1,4 +1,4 @@ -import { imgSnapshotTest, renderGraph } from '../../helpers/util.js'; +import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts'; describe('Class diagram', () => { it('1: should render a simple class diagram', () => { @@ -423,4 +423,82 @@ describe('Class diagram', () => { ); cy.get('svg'); }); + + it('should render class diagram with newlines in title', () => { + imgSnapshotTest(` + classDiagram + Animal <|-- \`Du\nck\` + Animal : +int age + Animal : +String gender + Animal: +isMammal() + Animal: +mate() + class \`Du\nck\` { + +String beakColor + +String featherColor + +swim() + +quack() + } + `); + cy.get('svg'); + }); + + it('should render class diagram with many newlines in title', () => { + imgSnapshotTest(` + classDiagram + class \`This\nTitle\nHas\nMany\nNewlines\` { + +String Also + -Stirng Many + #int Members + +And() + -Many() + #Methods() + } + `); + }); + + it('should render with newlines in title and an annotation', () => { + imgSnapshotTest(` + classDiagram + class \`This\nTitle\nHas\nMany\nNewlines\` { + +String Also + -Stirng Many + #int Members + +And() + -Many() + #Methods() + } + <<Interface>> \`This\nTitle\nHas\nMany\nNewlines\` + `); + }); + + it('should handle newline title in namespace', () => { + imgSnapshotTest(` + classDiagram + namespace testingNamespace { + class \`This\nTitle\nHas\nMany\nNewlines\` { + +String Also + -Stirng Many + #int Members + +And() + -Many() + #Methods() + } + } + `); + }); + + it('should handle newline in string label', () => { + imgSnapshotTest(` + classDiagram + class A["This has\na newline!"] { + +String boop + -Int beep + #double bop + } + + class B["This title also has\na newline"] + B : +with(more) + B : -methods() + `); + }); }); diff --git a/cypress/integration/rendering/conf-and-directives.spec.js b/cypress/integration/rendering/conf-and-directives.spec.js index 3fc0f7f02f..401a24894b 100644 --- a/cypress/integration/rendering/conf-and-directives.spec.js +++ b/cypress/integration/rendering/conf-and-directives.spec.js @@ -1,4 +1,4 @@ -import { imgSnapshotTest } from '../../helpers/util.js'; +import { imgSnapshotTest } from '../../helpers/util.ts'; describe('Configuration and directives - nodes should be light blue', () => { it('No config - use default', () => { @@ -14,7 +14,6 @@ describe('Configuration and directives - nodes should be light blue', () => { `, {} ); - cy.get('svg'); }); it('Settings from initialize - nodes should be green', () => { imgSnapshotTest( @@ -28,7 +27,6 @@ graph TD end `, { theme: 'forest' } ); - cy.get('svg'); }); it('Settings from initialize overriding themeVariable - nodes should be red', () => { imgSnapshotTest( @@ -46,7 +44,6 @@ graph TD `, { theme: 'base', themeVariables: { primaryColor: '#ff0000' }, logLevel: 0 } ); - cy.get('svg'); }); it('Settings from directive - nodes should be grey', () => { imgSnapshotTest( @@ -62,7 +59,24 @@ graph TD `, {} ); - cy.get('svg'); + }); + it('Settings from frontmatter - nodes should be grey', () => { + imgSnapshotTest( + ` +--- +config: + theme: neutral +--- +graph TD + A(Start) --> B[/Another/] + A[/Another/] --> C[End] + subgraph section + B + C + end + `, + {} + ); }); it('Settings from directive overriding theme variable - nodes should be red', () => { @@ -79,7 +93,6 @@ graph TD `, {} ); - cy.get('svg'); }); it('Settings from initialize and directive - nodes should be grey', () => { imgSnapshotTest( @@ -95,7 +108,6 @@ graph TD `, { theme: 'forest' } ); - cy.get('svg'); }); it('Theme from initialize, directive overriding theme variable - nodes should be red', () => { imgSnapshotTest( @@ -111,8 +123,71 @@ graph TD `, { theme: 'base' } ); - cy.get('svg'); }); + it('Theme from initialize, frontmatter overriding theme variable - nodes should be red', () => { + imgSnapshotTest( + ` +--- +config: + theme: base + themeVariables: + primaryColor: '#ff0000' +--- +graph TD + A(Start) --> B[/Another/] + A[/Another/] --> C[End] + subgraph section + B + C + end + `, + { theme: 'forest' } + ); + }); + it('Theme from initialize, frontmatter overriding theme variable, directive overriding primaryColor - nodes should be red', () => { + imgSnapshotTest( + ` +--- +config: + theme: base + themeVariables: + primaryColor: '#00ff00' +--- +%%{init: {'theme': 'base', 'themeVariables':{ 'primaryColor': '#ff0000'}}}%% +graph TD + A(Start) --> B[/Another/] + A[/Another/] --> C[End] + subgraph section + B + C + end + `, + { theme: 'forest' } + ); + }); + + it('should render if values are not quoted properly', () => { + // #ff0000 is not quoted properly, and will evaluate to null. + // This test ensures that the rendering still works. + imgSnapshotTest( + `--- +config: + theme: base + themeVariables: + primaryColor: #ff0000 +--- +graph TD + A(Start) --> B[/Another/] + A[/Another/] --> C[End] + subgraph section + B + C + end + `, + { theme: 'forest' } + ); + }); + it('Theme variable from initialize, theme from directive - nodes should be red', () => { imgSnapshotTest( ` @@ -127,13 +202,11 @@ graph TD `, { themeVariables: { primaryColor: '#ff0000' } } ); - cy.get('svg'); }); describe('when rendering several diagrams', () => { it('diagrams should not taint later diagrams', () => { const url = 'http://localhost:9000/theme-directives.html'; cy.visit(url); - cy.get('svg'); cy.matchImageSnapshot('conf-and-directives.spec-when-rendering-several-diagrams-diagram-1'); }); }); diff --git a/cypress/integration/rendering/current.spec.js b/cypress/integration/rendering/current.spec.js index e0b36d53a2..d7bc08105e 100644 --- a/cypress/integration/rendering/current.spec.js +++ b/cypress/integration/rendering/current.spec.js @@ -1,4 +1,4 @@ -import { imgSnapshotTest } from '../../helpers/util.js'; +import { imgSnapshotTest } from '../../helpers/util.ts'; describe('Current diagram', () => { it('should render a state with states in it', () => { diff --git a/cypress/integration/rendering/debug.spec.js b/cypress/integration/rendering/debug.spec.js index afde4af3e3..56ad0f15f8 100644 --- a/cypress/integration/rendering/debug.spec.js +++ b/cypress/integration/rendering/debug.spec.js @@ -1,4 +1,4 @@ -import { imgSnapshotTest } from '../../helpers/util.js'; +import { imgSnapshotTest } from '../../helpers/util.ts'; describe('Flowchart', () => { it('34: testing the label width in percy', () => { diff --git a/cypress/integration/rendering/erDiagram.spec.js b/cypress/integration/rendering/erDiagram.spec.js index 0c6eaa8386..578f5a3984 100644 --- a/cypress/integration/rendering/erDiagram.spec.js +++ b/cypress/integration/rendering/erDiagram.spec.js @@ -1,4 +1,4 @@ -import { imgSnapshotTest, renderGraph } from '../../helpers/util.js'; +import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts'; describe('Entity Relationship Diagram', () => { it('should render a simple ER diagram', () => { @@ -200,6 +200,27 @@ describe('Entity Relationship Diagram', () => { ); }); + it('should render entities with attributes that begin with asterisk', () => { + imgSnapshotTest( + ` + erDiagram + BOOK { + int *id + string name + varchar(99) summary + } + BOOK }o..o{ STORE : soldBy + STORE { + int *id + string name + varchar(50) address + } + `, + { loglevel: 1 } + ); + cy.get('svg'); + }); + it('should render entities with keys', () => { renderGraph( ` @@ -284,4 +305,21 @@ ORDER ||--|{ LINE-ITEM : contains {} ); }); + + it('should render entities with entity name aliases', () => { + imgSnapshotTest( + ` + erDiagram + p[Person] { + varchar(64) firstName + varchar(64) lastName + } + c["Customer Account"] { + varchar(128) email + } + p ||--o| c : has + `, + { logLevel: 1 } + ); + }); }); diff --git a/cypress/integration/rendering/flowchart-elk.spec.js b/cypress/integration/rendering/flowchart-elk.spec.js index 4f90646a28..221806b073 100644 --- a/cypress/integration/rendering/flowchart-elk.spec.js +++ b/cypress/integration/rendering/flowchart-elk.spec.js @@ -1,4 +1,4 @@ -import { imgSnapshotTest, renderGraph } from '../../helpers/util.js'; +import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts'; describe.skip('Flowchart ELK', () => { it('1-elk: should render a simple flowchart', () => { @@ -681,7 +681,7 @@ title: Simple flowchart flowchart-elk TD A --> B `, - { titleTopMargin: 0 } + { flowchart: { titleTopMargin: 0 } } ); }); it('elk: should include classes on the edges', () => { @@ -710,7 +710,7 @@ flowchart-elk LR style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5 classDef someclass fill:#f96 `, - { titleTopMargin: 0 } + { flowchart: { titleTopMargin: 0 } } ); }); it('With formatting in a node', () => { @@ -726,7 +726,7 @@ flowchart-elk LR b --> d(The dog in the hog) c --> d `, - { titleTopMargin: 0 } + { flowchart: { titleTopMargin: 0 } } ); }); it('New line in node and formatted edge label', () => { @@ -736,7 +736,7 @@ flowchart-elk LR b("\`The dog in **the** hog.(1) NL\`") --"\`1o **bold**\`"--> c `, - { titleTopMargin: 0 } + { flowchart: { titleTopMargin: 0 } } ); }); it('Wrapping long text with a new line', () => { @@ -749,7 +749,7 @@ Word! Another line with many, many words. Another line with many, many words. Another line with many, many words. Another line with many, many words. Another line with many, many words. Another line with many, many words. Another line with many, many words. Another line with many, many words. \`) --> c `, - { titleTopMargin: 0 } + { flowchart: { titleTopMargin: 0 } } ); }); it('Sub graphs and markdown strings', () => { @@ -766,7 +766,7 @@ subgraph "\`**Two**\`" end `, - { titleTopMargin: 0 } + { flowchart: { titleTopMargin: 0 } } ); }); }); @@ -782,7 +782,7 @@ flowchart-elk LR style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5 classDef someclass fill:#f96 `, - { titleTopMargin: 0 } + { flowchart: { titleTopMargin: 0 } } ); }); it('With formatting in a node', () => { @@ -798,7 +798,7 @@ flowchart-elk LR b --> d(The dog in the hog) c --> d `, - { titleTopMargin: 0 } + { flowchart: { titleTopMargin: 0 } } ); }); it('New line in node and formatted edge label', () => { @@ -808,7 +808,7 @@ flowchart-elk LR b("\`The dog in **the** hog.(1) NL\`") --"\`1o **bold**\`"--> c `, - { titleTopMargin: 0 } + { flowchart: { titleTopMargin: 0 } } ); }); it('Wrapping long text with a new line', () => { @@ -821,7 +821,7 @@ Word! Another line with many, many words. Another line with many, many words. Another line with many, many words. Another line with many, many words. Another line with many, many words. Another line with many, many words. Another line with many, many words. Another line with many, many words. \`") --> c `, - { titleTopMargin: 0 } + { flowchart: { titleTopMargin: 0 } } ); }); it('Sub graphs and markdown strings', () => { @@ -838,7 +838,7 @@ subgraph "\`**Two**\`" end `, - { titleTopMargin: 0 } + { flowchart: { titleTopMargin: 0 } } ); }); }); diff --git a/cypress/integration/rendering/flowchart-v2.spec.js b/cypress/integration/rendering/flowchart-v2.spec.js index 305c55b214..aac4a31b17 100644 --- a/cypress/integration/rendering/flowchart-v2.spec.js +++ b/cypress/integration/rendering/flowchart-v2.spec.js @@ -1,4 +1,4 @@ -import { imgSnapshotTest, renderGraph } from '../../helpers/util.js'; +import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts'; describe('Flowchart v2', () => { it('1: should render a simple flowchart', () => { @@ -449,7 +449,7 @@ flowchart TD { htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' } ); }); - it('65: text-color from classes', () => { + it('65-1: text-color from classes', () => { imgSnapshotTest( ` flowchart LR @@ -460,6 +460,31 @@ flowchart TD { htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' } ); }); + it('65-2: bold text from classes', () => { + imgSnapshotTest( + ` + flowchart + classDef cat fill:#f9d5e5, stroke:#233d4d,stroke-width:2px, font-weight:bold; + CS(A long bold text to be viewed):::cat + `, + { htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' } + ); + }); + it('65-3: bigger font from classes', () => { + imgSnapshotTest( + ` +flowchart + Node1:::class1 --> Node2:::class2 + Node1:::class1 --> Node3:::class2 + Node3 --> Node4((I am a circle)):::larger + + classDef class1 fill:lightblue + classDef class2 fill:pink + classDef larger font-size:30px,fill:yellow + `, + { htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' } + ); + }); it('66: More nested subgraph cases (TB)', () => { imgSnapshotTest( ` @@ -671,7 +696,7 @@ title: Simple flowchart flowchart TD A --> B `, - { titleTopMargin: 0 } + { flowchart: { titleTopMargin: 10 } } ); }); it('3192: It should be possieble to render flowcharts with invisible edges', () => { @@ -682,7 +707,7 @@ title: Simple flowchart with invisible edges flowchart TD A ~~~ B `, - { titleTopMargin: 0 } + { flowchart: { titleTopMargin: 10 } } ); }); it('4023: Should render html labels with images and-or text correctly', () => { @@ -695,6 +720,15 @@ A ~~~ B {} ); }); + + it('4439: Should render the graph even if some images are missing', () => { + imgSnapshotTest( + `flowchart TD + B[] + B-->C[]`, + {} + ); + }); describe('Markdown strings flowchart (#4220)', () => { describe('html labels', () => { it('With styling and classes', () => { @@ -707,7 +741,7 @@ flowchart LR style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5 classDef someclass fill:#f96 `, - { titleTopMargin: 0 } + { flowchart: { titleTopMargin: 0 } } ); }); it('With formatting in a node', () => { @@ -723,7 +757,7 @@ flowchart LR b --> d(The dog in the hog) c --> d `, - { titleTopMargin: 0 } + { flowchart: { titleTopMargin: 0 } } ); }); it('New line in node and formatted edge label', () => { @@ -733,7 +767,7 @@ flowchart LR b("\`The dog in **the** hog.(1) NL\`") --"\`1o **bold**\`"--> c `, - { titleTopMargin: 0 } + { flowchart: { titleTopMargin: 0 } } ); }); it('Wrapping long text with a new line', () => { @@ -746,7 +780,7 @@ Word! Another line with many, many words. Another line with many, many words. Another line with many, many words. Another line with many, many words. Another line with many, many words. Another line with many, many words. Another line with many, many words. Another line with many, many words. \`") --> c `, - { titleTopMargin: 0 } + { flowchart: { titleTopMargin: 0 } } ); }); it('Sub graphs and markdown strings', () => { @@ -763,7 +797,7 @@ subgraph "\`**Two**\`" end `, - { titleTopMargin: 0 } + { flowchart: { titleTopMargin: 0 } } ); }); }); @@ -779,7 +813,7 @@ flowchart LR style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5 classDef someclass fill:#f96 `, - { titleTopMargin: 0 } + { flowchart: { titleTopMargin: 0 } } ); }); it('With formatting in a node', () => { @@ -795,7 +829,7 @@ flowchart LR b --> d(The dog in the hog) c --> d `, - { titleTopMargin: 0 } + { flowchart: { titleTopMargin: 0 } } ); }); it('New line in node and formatted edge label', () => { @@ -805,7 +839,7 @@ flowchart LR b("\`The dog in **the** hog.(1) NL\`") --"\`1o **bold**\`"--> c `, - { titleTopMargin: 0 } + { flowchart: { titleTopMargin: 0 } } ); }); it('Wrapping long text with a new line', () => { @@ -818,7 +852,7 @@ Word! Another line with many, many words. Another line with many, many words. Another line with many, many words. Another line with many, many words. Another line with many, many words. Another line with many, many words. Another line with many, many words. Another line with many, many words. \`") --> c `, - { titleTopMargin: 0 } + { flowchart: { titleTopMargin: 0 } } ); }); it('Sub graphs and markdown strings', () => { @@ -835,7 +869,7 @@ subgraph "\`**Two**\`" end `, - { titleTopMargin: 0 } + { flowchart: { titleTopMargin: 0 } } ); }); }); diff --git a/cypress/integration/rendering/flowchart.spec.js b/cypress/integration/rendering/flowchart.spec.js index d25043d289..e4766e7923 100644 --- a/cypress/integration/rendering/flowchart.spec.js +++ b/cypress/integration/rendering/flowchart.spec.js @@ -1,4 +1,4 @@ -import { imgSnapshotTest, renderGraph } from '../../helpers/util.js'; +import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts'; describe('Graph', () => { it('1: should render a simple flowchart no htmlLabels', () => { @@ -891,4 +891,27 @@ graph TD { htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' } ); }); + it('66: apply class called default on node called default', () => { + imgSnapshotTest( + ` + graph TD + classDef default fill:#a34,stroke:#000,stroke-width:4px,color:#fff + hello --> default + `, + { htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' } + ); + }); + + it('67: should be able to style default node independently', () => { + imgSnapshotTest( + ` + flowchart TD + classDef default fill:#a34 + hello --> default + + style default stroke:#000,stroke-width:4px + `, + { htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' } + ); + }); }); diff --git a/cypress/integration/rendering/gantt.spec.js b/cypress/integration/rendering/gantt.spec.js index cb65f73b0b..33d3ac9e14 100644 --- a/cypress/integration/rendering/gantt.spec.js +++ b/cypress/integration/rendering/gantt.spec.js @@ -1,4 +1,4 @@ -import { imgSnapshotTest, renderGraph } from '../../helpers/util.js'; +import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts'; describe('Gantt diagram', () => { beforeEach(() => { @@ -414,6 +414,28 @@ describe('Gantt diagram', () => { ); }); + it('should render a gantt diagram with tick is 1 week, with the day starting on monday', () => { + imgSnapshotTest( + ` + gantt + title A Gantt Diagram + dateFormat YYYY-MM-DD + axisFormat %m-%d + tickInterval 1week + weekday monday + excludes weekends + + section Section + A task : a1, 2022-10-01, 30d + Another task : after a1, 20d + section Another + Task in sec : 2022-10-20, 12d + another task : 24d + `, + {} + ); + }); + it('should render a gantt diagram with tick is 1 month', () => { imgSnapshotTest( ` diff --git a/cypress/integration/rendering/gitGraph.spec.js b/cypress/integration/rendering/gitGraph.spec.js index 43f91a983e..c01a557961 100644 --- a/cypress/integration/rendering/gitGraph.spec.js +++ b/cypress/integration/rendering/gitGraph.spec.js @@ -1,4 +1,4 @@ -import { imgSnapshotTest } from '../../helpers/util.js'; +import { imgSnapshotTest } from '../../helpers/util.ts'; describe('Git Graph diagram', () => { it('1: should render a simple gitgraph with commit on main branch', () => { @@ -333,4 +333,372 @@ gitGraph {} ); }); + it('15: should render a simple gitgraph with commit on main branch | Vertical Branch', () => { + imgSnapshotTest( + `gitGraph TB: + commit id: "1" + commit id: "2" + commit id: "3" + `, + {} + ); + }); + it('16: should render a simple gitgraph with commit on main branch with Id | Vertical Branch', () => { + imgSnapshotTest( + `gitGraph TB: + commit id: "One" + commit id: "Two" + commit id: "Three" + `, + {} + ); + }); + it('17: should render a simple gitgraph with different commitTypes on main branch | Vertical Branch', () => { + imgSnapshotTest( + `gitGraph TB: + commit id: "Normal Commit" + commit id: "Reverse Commit" type: REVERSE + commit id: "Hightlight Commit" type: HIGHLIGHT + `, + {} + ); + }); + it('18: should render a simple gitgraph with tags commitTypes on main branch | Vertical Branch', () => { + imgSnapshotTest( + `gitGraph TB: + commit id: "Normal Commit with tag" tag: "v1.0.0" + commit id: "Reverse Commit with tag" type: REVERSE tag: "RC_1" + commit id: "Hightlight Commit" type: HIGHLIGHT tag: "8.8.4" + `, + {} + ); + }); + it('19: should render a simple gitgraph with two branches | Vertical Branch', () => { + imgSnapshotTest( + `gitGraph TB: + commit id: "1" + commit id: "2" + branch develop + checkout develop + commit id: "3" + commit id: "4" + checkout main + commit id: "5" + commit id: "6" + `, + {} + ); + }); + it('20: should render a simple gitgraph with two branches and merge commit | Vertical Branch', () => { + imgSnapshotTest( + `gitGraph TB: + commit id: "1" + commit id: "2" + branch develop + checkout develop + commit id: "3" + commit id: "4" + checkout main + merge develop + commit id: "5" + commit id: "6" + `, + {} + ); + }); + it('21: should render a simple gitgraph with three branches and tagged merge commit | Vertical Branch', () => { + imgSnapshotTest( + `gitGraph TB: + commit id: "1" + commit id: "2" + branch nice_feature + checkout nice_feature + commit id: "3" + checkout main + commit id: "4" + checkout nice_feature + branch very_nice_feature + checkout very_nice_feature + commit id: "5" + checkout main + commit id: "6" + checkout nice_feature + commit id: "7" + checkout main + merge nice_feature id: "12345" tag: "my merge commit" + checkout very_nice_feature + commit id: "8" + checkout main + commit id: "9" + `, + {} + ); + }); + it('22: should render a simple gitgraph with more than 8 branchs & overriding variables | Vertical Branch', () => { + imgSnapshotTest( + `%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': { + 'gitBranchLabel0': '#ffffff', + 'gitBranchLabel1': '#ffffff', + 'gitBranchLabel2': '#ffffff', + 'gitBranchLabel3': '#ffffff', + 'gitBranchLabel4': '#ffffff', + 'gitBranchLabel5': '#ffffff', + 'gitBranchLabel6': '#ffffff', + 'gitBranchLabel7': '#ffffff', + } } }%% + gitGraph TB: + checkout main + branch branch1 + branch branch2 + branch branch3 + branch branch4 + branch branch5 + branch branch6 + branch branch7 + branch branch8 + branch branch9 + checkout branch1 + commit id: "1" + `, + {} + ); + }); + it('23: should render a simple gitgraph with rotated labels | Vertical Branch', () => { + imgSnapshotTest( + `%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'gitGraph': { + 'rotateCommitLabel': true + } } }%% + gitGraph TB: + commit id: "75f7219e83b321cd3fdde7dcf83bc7c1000a6828" + commit id: "0db4784daf82736dec4569e0dc92980d328c1f2e" + commit id: "7067e9973f9eaa6cd4a4b723c506d1eab598e83e" + commit id: "66972321ad6c199013b5b31f03b3a86fa3f9817d" + `, + {} + ); + }); + it('24: should render a simple gitgraph with horizontal labels | Vertical Branch', () => { + imgSnapshotTest( + `%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'gitGraph': { + 'rotateCommitLabel': false + } } }%% + gitGraph TB: + commit id: "Alpha" + commit id: "Beta" + commit id: "Gamma" + commit id: "Delta" + `, + {} + ); + }); + it('25: should render a simple gitgraph with cherry pick commit | Vertical Branch', () => { + imgSnapshotTest( + ` + gitGraph TB: + commit id: "ZERO" + branch develop + commit id:"A" + checkout main + commit id:"ONE" + checkout develop + commit id:"B" + checkout main + commit id:"TWO" + cherry-pick id:"A" + commit id:"THREE" + checkout develop + commit id:"C" + `, + {} + ); + }); + it('26: should render a gitgraph with cherry pick commit with custom tag | Vertical Branch', () => { + imgSnapshotTest( + ` + gitGraph TB: + commit id: "ZERO" + branch develop + commit id:"A" + checkout main + commit id:"ONE" + checkout develop + commit id:"B" + checkout main + commit id:"TWO" + cherry-pick id:"A" tag: "snapshot" + commit id:"THREE" + checkout develop + commit id:"C" + `, + {} + ); + }); + it('27: should render a gitgraph with cherry pick commit with no tag | Vertical Branch', () => { + imgSnapshotTest( + ` + gitGraph TB: + commit id: "ZERO" + branch develop + commit id:"A" + checkout main + commit id:"ONE" + checkout develop + commit id:"B" + checkout main + commit id:"TWO" + cherry-pick id:"A" tag: "" + commit id:"THREE" + checkout develop + commit id:"C" + `, + {} + ); + }); + it('28: should render a simple gitgraph with two cherry pick commit | Vertical Branch', () => { + imgSnapshotTest( + ` + gitGraph TB: + commit id: "ZERO" + branch develop + commit id:"A" + checkout main + commit id:"ONE" + checkout develop + commit id:"B" + branch featureA + commit id:"FIX" + commit id: "FIX-2" + checkout main + commit id:"TWO" + cherry-pick id:"A" + commit id:"THREE" + cherry-pick id:"FIX" + checkout develop + commit id:"C" + merge featureA + `, + {} + ); + }); + it('29: should render commits for more than 8 branches | Vertical Branch', () => { + imgSnapshotTest( + ` + gitGraph TB: + checkout main + %% Make sure to manually set the ID of all commits, for consistent visual tests + commit id: "1-abcdefg" + checkout main + branch branch1 + commit id: "2-abcdefg" + checkout main + merge branch1 + branch branch2 + commit id: "3-abcdefg" + checkout main + merge branch2 + branch branch3 + commit id: "4-abcdefg" + checkout main + merge branch3 + branch branch4 + commit id: "5-abcdefg" + checkout main + merge branch4 + branch branch5 + commit id: "6-abcdefg" + checkout main + merge branch5 + branch branch6 + commit id: "7-abcdefg" + checkout main + merge branch6 + branch branch7 + commit id: "8-abcdefg" + checkout main + merge branch7 + branch branch8 + commit id: "9-abcdefg" + checkout main + merge branch8 + branch branch9 + commit id: "10-abcdefg" + `, + {} + ); + }); + it('30: should render a simple gitgraph with three branches,custom merge commit id,tag,type | Vertical Branch', () => { + imgSnapshotTest( + `gitGraph TB: + commit id: "1" + commit id: "2" + branch nice_feature + checkout nice_feature + commit id: "3" + checkout main + commit id: "4" + checkout nice_feature + branch very_nice_feature + checkout very_nice_feature + commit id: "5" + checkout main + commit id: "6" + checkout nice_feature + commit id: "7" + checkout main + merge nice_feature id: "customID" tag: "customTag" type: REVERSE + checkout very_nice_feature + commit id: "8" + checkout main + commit id: "9" + `, + {} + ); + }); + it('31: should render a simple gitgraph with a title | Vertical Branch', () => { + imgSnapshotTest( + `--- +title: simple gitGraph +--- +gitGraph TB: + commit id: "1-abcdefg" +`, + {} + ); + }); + it('32: should render a simple gitgraph overlapping commits | Vertical Branch', () => { + imgSnapshotTest( + `gitGraph TB: + commit id:"s1" + commit id:"s2" + branch branch1 + commit id:"s3" + commit id:"s4" + checkout main + commit id:"s5" + checkout branch1 + commit id:"s6" + commit id:"s7" + merge main + `, + {} + ); + }); + it('33: should render a simple gitgraph overlapping commits', () => { + imgSnapshotTest( + `gitGraph + commit id:"s1" + commit id:"s2" + branch branch1 + commit id:"s3" + commit id:"s4" + checkout main + commit id:"s5" + checkout branch1 + commit id:"s6" + commit id:"s7" + merge main + `, + {} + ); + }); }); diff --git a/cypress/integration/rendering/info.spec.ts b/cypress/integration/rendering/info.spec.ts index 3db74c9802..97b384eb5e 100644 --- a/cypress/integration/rendering/info.spec.ts +++ b/cypress/integration/rendering/info.spec.ts @@ -1,4 +1,4 @@ -import { imgSnapshotTest } from '../../helpers/util.js'; +import { imgSnapshotTest } from '../../helpers/util.ts'; describe('info diagram', () => { it('should handle an info definition', () => { diff --git a/cypress/integration/rendering/journey.spec.js b/cypress/integration/rendering/journey.spec.js index 6f9d9bb609..d8bef6d1b9 100644 --- a/cypress/integration/rendering/journey.spec.js +++ b/cypress/integration/rendering/journey.spec.js @@ -1,4 +1,4 @@ -import { imgSnapshotTest, renderGraph } from '../../helpers/util.js'; +import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts'; describe('User journey diagram', () => { it('Simple test', () => { diff --git a/cypress/integration/rendering/mindmap.spec.ts b/cypress/integration/rendering/mindmap.spec.ts index e390beaee5..a77459f589 100644 --- a/cypress/integration/rendering/mindmap.spec.ts +++ b/cypress/integration/rendering/mindmap.spec.ts @@ -1,4 +1,4 @@ -import { imgSnapshotTest } from '../../helpers/util.js'; +import { imgSnapshotTest } from '../../helpers/util.ts'; /** * Check whether the SVG Element has a Mindmap root @@ -242,8 +242,7 @@ mindmap a second line 😎\`] id2[\`The dog in **the** hog... a *very long text* about it Word!\`] -`, - { titleTopMargin: 0 } +` ); }); }); diff --git a/cypress/integration/rendering/pie.spec.js b/cypress/integration/rendering/pie.spec.ts similarity index 54% rename from cypress/integration/rendering/pie.spec.js rename to cypress/integration/rendering/pie.spec.ts index 8a89a0cde5..269efafb26 100644 --- a/cypress/integration/rendering/pie.spec.js +++ b/cypress/integration/rendering/pie.spec.ts @@ -1,89 +1,85 @@ -import { imgSnapshotTest, renderGraph } from '../../helpers/util.js'; +import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts'; -describe('Pie Chart', () => { +describe('pie chart', () => { it('should render a simple pie diagram', () => { imgSnapshotTest( + `pie title Sports in Sweden + "Bandy": 40 + "Ice-Hockey": 80 + "Football": 90 ` - pie title Sports in Sweden - "Bandy" : 40 - "Ice-Hockey" : 80 - "Football" : 90 - `, - {} ); - cy.get('svg'); }); + it('should render a simple pie diagram with long labels', () => { imgSnapshotTest( + `pie title NETFLIX + "Time spent looking for movie": 90 + "Time spent watching it": 10 ` - pie title NETFLIX - "Time spent looking for movie" : 90 - "Time spent watching it" : 10 - `, - {} ); - cy.get('svg'); }); + it('should render a simple pie diagram with capital letters for labels', () => { imgSnapshotTest( + `pie title What Voldemort doesn't have? + "FRIENDS": 2 + "FAMILY": 3 + "NOSE": 45 ` - pie title What Voldemort doesn't have? - "FRIENDS" : 2 - "FAMILY" : 3 - "NOSE" : 45 - `, - {} ); - cy.get('svg'); }); + it('should render a pie diagram when useMaxWidth is true (default)', () => { renderGraph( - ` - pie title Sports in Sweden - "Bandy" : 40 - "Ice-Hockey" : 80 - "Football" : 90 + `pie title Sports in Sweden + "Bandy": 40 + "Ice-Hockey": 80 + "Football": 90 `, { pie: { useMaxWidth: true } } ); cy.get('svg').should((svg) => { expect(svg).to.have.attr('width', '100%'); - // expect(svg).to.have.attr('height'); - // const height = parseFloat(svg.attr('height')); - // expect(height).to.eq(450); const style = svg.attr('style'); expect(style).to.match(/^max-width: [\d.]+px;$/); const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join('')); expect(maxWidthValue).to.eq(984); }); }); + it('should render a pie diagram when useMaxWidth is false', () => { renderGraph( - ` - pie title Sports in Sweden - "Bandy" : 40 - "Ice-Hockey" : 80 - "Football" : 90 + `pie title Sports in Sweden + "Bandy": 40 + "Ice-Hockey": 80 + "Football": 90 `, { pie: { useMaxWidth: false } } ); cy.get('svg').should((svg) => { - // const height = parseFloat(svg.attr('height')); const width = parseFloat(svg.attr('width')); - // expect(height).to.eq(450); expect(width).to.eq(984); expect(svg).to.not.have.attr('style'); }); }); - it('should render a pie diagram when textPosition is set', () => { + + it('should render a pie diagram when textPosition is setted', () => { imgSnapshotTest( - ` - pie - "Dogs": 50 - "Cats": 25 - `, + `pie + "Dogs": 50 + "Cats": 25 + `, { logLevel: 1, pie: { textPosition: 0.9 } } ); - cy.get('svg'); + }); + + it('should render a pie diagram with showData', () => { + imgSnapshotTest( + `pie showData + "Dogs": 50 + "Cats": 25 + ` + ); }); }); diff --git a/cypress/integration/rendering/quadrantChart.spec.js b/cypress/integration/rendering/quadrantChart.spec.js index 4bcf58b601..50520eb1a5 100644 --- a/cypress/integration/rendering/quadrantChart.spec.js +++ b/cypress/integration/rendering/quadrantChart.spec.js @@ -1,4 +1,4 @@ -import { imgSnapshotTest, renderGraph } from '../../helpers/util.js'; +import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts'; describe('Quadrant Chart', () => { it('should render if only chart type is provided', () => { diff --git a/cypress/integration/rendering/requirement.spec.js b/cypress/integration/rendering/requirement.spec.js index 0bf9014bf9..f33ae7a0cb 100644 --- a/cypress/integration/rendering/requirement.spec.js +++ b/cypress/integration/rendering/requirement.spec.js @@ -1,4 +1,4 @@ -import { imgSnapshotTest, renderGraph } from '../../helpers/util.js'; +import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts'; describe('Requirement diagram', () => { it('sample', () => { diff --git a/cypress/integration/rendering/sankey.spec.ts b/cypress/integration/rendering/sankey.spec.ts new file mode 100644 index 0000000000..ad0d75f187 --- /dev/null +++ b/cypress/integration/rendering/sankey.spec.ts @@ -0,0 +1,144 @@ +import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts'; + +describe('Sankey Diagram', () => { + it('should render a simple example', () => { + imgSnapshotTest( + ` + sankey-beta + + sourceNode,targetNode,10 + `, + {} + ); + }); + + describe('when given a linkColor', function () { + this.beforeAll(() => { + cy.wrap( + `sankey-beta + a,b,10 + ` + ).as('graph'); + }); + + it('links should use hex color', function () { + renderGraph(this.graph, { sankey: { linkColor: '#636465' } }); + + cy.get('.link path').should((link) => { + expect(link.attr('stroke')).to.equal('#636465'); + }); + }); + + it('links should be the same color as source node', function () { + renderGraph(this.graph, { sankey: { linkColor: 'source' } }); + + cy.get('.link path').then((link) => { + cy.get('.node[id="node-1"] rect').should((node) => + expect(link.attr('stroke')).to.equal(node.attr('fill')) + ); + }); + }); + + it('links should be the same color as target node', function () { + renderGraph(this.graph, { sankey: { linkColor: 'target' } }); + + cy.get('.link path').then((link) => { + cy.get('.node[id="node-2"] rect').should((node) => + expect(link.attr('stroke')).to.equal(node.attr('fill')) + ); + }); + }); + + it('links must be gradient', function () { + renderGraph(this.graph, { sankey: { linkColor: 'gradient' } }); + + cy.get('.link path').should((link) => { + expect(link.attr('stroke')).to.equal('url(#linearGradient-3)'); + }); + }); + }); + + describe('when given a nodeAlignment', function () { + this.beforeAll(() => { + cy.wrap( + ` + sankey-beta + + a,b,8 + b,c,8 + c,d,8 + d,e,8 + + x,c,4 + c,y,4 + ` + ).as('graph'); + }); + + this.afterEach(() => { + cy.get('.node[id="node-1"]').should((node) => { + expect(node.attr('x')).to.equal('0'); + }); + cy.get('.node[id="node-2"]').should((node) => { + expect(node.attr('x')).to.equal('100'); + }); + cy.get('.node[id="node-3"]').should((node) => { + expect(node.attr('x')).to.equal('200'); + }); + cy.get('.node[id="node-4"]').should((node) => { + expect(node.attr('x')).to.equal('300'); + }); + cy.get('.node[id="node-5"]').should((node) => { + expect(node.attr('x')).to.equal('400'); + }); + }); + + it('should justify nodes', function () { + renderGraph(this.graph, { + sankey: { nodeAlignment: 'justify', width: 410, useMaxWidth: false }, + }); + cy.get('.node[id="node-6"]').should((node) => { + expect(node.attr('x')).to.equal('0'); + }); + cy.get('.node[id="node-7"]').should((node) => { + expect(node.attr('x')).to.equal('400'); + }); + }); + + it('should align nodes left', function () { + renderGraph(this.graph, { + sankey: { nodeAlignment: 'left', width: 410, useMaxWidth: false }, + }); + cy.get('.node[id="node-6"]').should((node) => { + expect(node.attr('x')).to.equal('0'); + }); + cy.get('.node[id="node-7"]').should((node) => { + expect(node.attr('x')).to.equal('300'); + }); + }); + + it('should align nodes right', function () { + renderGraph(this.graph, { + sankey: { nodeAlignment: 'right', width: 410, useMaxWidth: false }, + }); + cy.get('.node[id="node-6"]').should((node) => { + expect(node.attr('x')).to.equal('100'); + }); + cy.get('.node[id="node-7"]').should((node) => { + expect(node.attr('x')).to.equal('400'); + }); + }); + + it('should center nodes', function () { + renderGraph(this.graph, { + sankey: { nodeAlignment: 'center', width: 410, useMaxWidth: false }, + }); + cy.get('.node[id="node-6"]').should((node) => { + expect(node.attr('x')).to.equal('100'); + }); + cy.get('.node[id="node-7"]').should((node) => { + expect(node.attr('x')).to.equal('300'); + }); + }); + }); +}); diff --git a/cypress/integration/rendering/sequencediagram.spec.js b/cypress/integration/rendering/sequencediagram.spec.js index 185cc4133a..7659138241 100644 --- a/cypress/integration/rendering/sequencediagram.spec.js +++ b/cypress/integration/rendering/sequencediagram.spec.js @@ -1,6 +1,6 @@ /// -import { imgSnapshotTest, renderGraph } from '../../helpers/util.js'; +import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts'; context('Sequence diagram', () => { it('should render a sequence diagram with boxes', () => { @@ -156,6 +156,81 @@ context('Sequence diagram', () => { ` ); }); + it('should render a sequence diagram with basic actor creation and destruction', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice ->> Bob: Hello Bob, how are you ? + Bob ->> Alice: Fine, thank you. And you? + create participant Polo + Alice ->> Polo: Hi Polo! + create actor Ola1 as Ola + Polo ->> Ola1: Hiii + Ola1 ->> Alice: Hi too + destroy Ola1 + Alice --x Ola1: Bye! + Alice ->> Bob: And now? + create participant Ola2 as Ola + Alice ->> Ola2: Hello again + destroy Alice + Alice --x Ola2: Bye for me! + destroy Bob + Ola2 --> Bob: The end + ` + ); + }); + it('should render a sequence diagram with actor creation and destruction coupled with backgrounds, loops and notes', () => { + imgSnapshotTest( + ` + sequenceDiagram + accTitle: test the accTitle + accDescr: Test a description + + participant Alice + participant Bob + autonumber 10 10 + rect rgb(200, 220, 100) + rect rgb(200, 255, 200) + + Alice ->> Bob: Hello Bob, how are you? + create participant John as John
Second Line + Bob-->>John: How about you John? + end + + Bob--x Alice: I am good thanks! + Bob-x John: I am good thanks! + Note right of John: John thinks a long
long time, so long
that the text does
not fit on a row. + + Bob-->Alice: Checking with John... + Note over John:wrap: John looks like he's still thinking, so Bob prods him a bit. + Bob-x John: Hey John - we're still waiting to know
how you're doing + Note over John:nowrap: John's trying hard not to break his train of thought. + destroy John + Bob-x John: John! Cmon! + Note over John: After a few more moments, John
finally snaps out of it. + end + + autonumber off + alt either this + create actor Lola + Alice->>+Lola: Yes + Lola-->>-Alice: OK + else or this + autonumber + Alice->>Lola: No + else or this will happen + Alice->Lola: Maybe + end + autonumber 200 + par this happens in parallel + destroy Bob + Alice -->> Bob: Parallel message 1 + and + Alice -->> Lola: Parallel message 2 + end + ` + ); + }); context('font settings', () => { it('should render different note fonts when configured', () => { imgSnapshotTest( diff --git a/cypress/integration/rendering/stateDiagram-v2.spec.js b/cypress/integration/rendering/stateDiagram-v2.spec.js index 700791621f..9a1a27abe5 100644 --- a/cypress/integration/rendering/stateDiagram-v2.spec.js +++ b/cypress/integration/rendering/stateDiagram-v2.spec.js @@ -1,4 +1,4 @@ -import { imgSnapshotTest, renderGraph } from '../../helpers/util.js'; +import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts'; describe('State diagram', () => { it('v2 should render a simple info', () => { diff --git a/cypress/integration/rendering/stateDiagram.spec.js b/cypress/integration/rendering/stateDiagram.spec.js index 28c24d3985..01e7a2b44e 100644 --- a/cypress/integration/rendering/stateDiagram.spec.js +++ b/cypress/integration/rendering/stateDiagram.spec.js @@ -1,4 +1,4 @@ -import { imgSnapshotTest, renderGraph } from '../../helpers/util.js'; +import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts'; describe('State diagram', () => { it('should render a simple state diagrams', () => { diff --git a/cypress/integration/rendering/theme.spec.js b/cypress/integration/rendering/theme.spec.js index ef3bd9a4b0..c84ad0c4b7 100644 --- a/cypress/integration/rendering/theme.spec.js +++ b/cypress/integration/rendering/theme.spec.js @@ -1,4 +1,4 @@ -import { imgSnapshotTest } from '../../helpers/util.js'; +import { imgSnapshotTest } from '../../helpers/util.ts'; describe('themeCSS balancing, it', () => { it('should not allow unbalanced CSS definitions', () => { diff --git a/cypress/integration/rendering/timeline.spec.ts b/cypress/integration/rendering/timeline.spec.ts index 6fae82fb4e..68da01d503 100644 --- a/cypress/integration/rendering/timeline.spec.ts +++ b/cypress/integration/rendering/timeline.spec.ts @@ -1,4 +1,4 @@ -import { imgSnapshotTest } from '../../helpers/util.js'; +import { imgSnapshotTest } from '../../helpers/util.ts'; describe('Timeline diagram', () => { it('1: should render a simple timeline with no specific sections', () => { diff --git a/cypress/integration/rendering/zenuml.spec.js b/cypress/integration/rendering/zenuml.spec.js index f317fbe824..53d8ae3469 100644 --- a/cypress/integration/rendering/zenuml.spec.js +++ b/cypress/integration/rendering/zenuml.spec.js @@ -1,4 +1,4 @@ -import { imgSnapshotTest } from '../../helpers/util.js'; +import { imgSnapshotTest } from '../../helpers/util.ts'; describe('Zen UML', () => { it('Basic Zen UML diagram', () => { diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html index 1b1ccd6856..f9a9f3756d 100644 --- a/cypress/platform/knsv2.html +++ b/cypress/platform/knsv2.html @@ -58,6 +58,23 @@
+      flowchart
+        classDef mainCategories fill:#f9d5e5, stroke:#233d4d,stroke-width:2px, font-weight:bold;
+        CS(Customer Awareness Journey):::mainCategories
+      
+
+flowchart
+Node1:::class1 --> Node2:::class2
+Node1:::class1 --> Node3:::class2
+Node3 --> Node4((I am a circle)):::larger
+
+classDef class1 fill:lightblue
+classDef class2 fill:pink
+classDef larger font-size:30px,fill:yellow
+      
+
 stateDiagram-v2
     [*] --> Still
     Still --> [*]
@@ -73,7 +90,7 @@
       a1 -- l2 --> a2
     end
     
-
+    
 flowchart RL
     subgraph "`one`"
       a1 -- l1 --> a2
@@ -98,11 +115,11 @@
         way`"]
   
-
+    
       classDiagram-v2
         note "I love this diagram!\nDo you love it?"
     
-
+    
     stateDiagram-v2
     State1: The state with a note with minus - and plus + in it
     note left of State1
@@ -147,7 +164,7 @@
       शान्तिः سلام  和平 `"]
 
     
-
+    
 %%{init: {"flowchart": {"defaultRenderer": "elk"}} }%%
 flowchart TB
   %% I could not figure out how to use double quotes in labels in Mermaid
@@ -399,21 +416,31 @@
       mermaid.parseError = function (err, hash) {
         // console.error('Mermaid error: ', err);
       };
+      // mermaid.initialize({
+      //   // theme: 'forest',
+      //   startOnLoad: true,
+      //   logLevel: 0,
+      //   flowchart: {
+      //     // defaultRenderer: 'elk',
+      //     useMaxWidth: false,
+      //     // htmlLabels: false,
+      //     htmlLabels: true,
+      //   },
+      //   // htmlLabels: false,
+      //   gantt: {
+      //     useMaxWidth: false,
+      //   },
+      //   useMaxWidth: false,
+      // });
       mermaid.initialize({
-        // theme: 'forest',
-        startOnLoad: true,
-        logLevel: 0,
-        flowchart: {
-          // defaultRenderer: 'elk',
-          useMaxWidth: false,
-          // htmlLabels: false,
-          htmlLabels: true,
-        },
-        // htmlLabels: false,
-        gantt: {
-          useMaxWidth: false,
+        flowchart: { titleTopMargin: 10 },
+        fontFamily: 'courier',
+        sequence: {
+          actorFontFamily: 'courier',
+          noteFontFamily: 'courier',
+          messageFontFamily: 'courier',
         },
-        useMaxWidth: false,
+        fontSize: 16,
       });
       function callback() {
         alert('It worked');
diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json
index e3351cebeb..baa9a72172 100644
--- a/cypress/tsconfig.json
+++ b/cypress/tsconfig.json
@@ -2,7 +2,9 @@
   "compilerOptions": {
     "target": "es2020",
     "lib": ["es2020", "dom"],
-    "types": ["cypress", "node"]
+    "types": ["cypress", "node"],
+    "allowImportingTsExtensions": true,
+    "noEmit": true
   },
   "include": ["**/*.ts"]
 }
diff --git a/demos/classchart.html b/demos/classchart.html
index b20dda2a33..3ad8fb100b 100644
--- a/demos/classchart.html
+++ b/demos/classchart.html
@@ -38,12 +38,14 @@ 

Class diagram demos

+quack() } class Fish{ - -int sizeInFeet + -Listint sizeInFeet -canEat() } class Zebra{ +bool is_wild - +run() + +run(List~T~, List~OT~) + %% +run-composite(List~T, K~) + +run-nested(List~List~OT~~) }
@@ -80,6 +82,7 @@

Class diagram demos

Class01 : #size() Class01 : -int chimp Class01 : +int gorilla + Class01 : +abstractAttribute string* class Class10~T~ { <<service>> int id @@ -122,6 +125,8 @@

Class diagram demos

classDiagram direction LR Animal ()-- Dog + Animal ()-- Cat + note for Cat "should have no members area" Dog : bark() Dog : species()
@@ -151,6 +156,30 @@

Class diagram demos

~InternalProperty : string ~AnotherInternalProperty : List~List~string~~ } + class People List~List~Person~~ +
+
+ +
+    classDiagram
+      A1 --> B1
+      namespace A {
+        class A1 {
+          +foo : string
+        }
+        class A2 {
+          +bar : int
+        }
+      }
+      namespace B {
+        class B1 {
+          +foo : bool
+        }
+        class B2 {
+          +bar : float
+        }
+      }
+      A2 --> B2
     

diff --git a/demos/dev/example.html b/demos/dev/example.html new file mode 100644 index 0000000000..482343014e --- /dev/null +++ b/demos/dev/example.html @@ -0,0 +1,34 @@ + + + + + Mermaid development page + + +
+graph TB
+      a --> b
+      a --> c
+      b --> d
+      c --> d
+    
+ +
+ + + + diff --git a/demos/er.html b/demos/er.html index 49f0a683f0..027c2e2772 100644 --- a/demos/er.html +++ b/demos/er.html @@ -21,6 +21,8 @@
       ---
       title: This is a title
+      config:
+        theme: forest
       ---
       erDiagram
         %% title This is a title
@@ -108,6 +110,35 @@
         }
         MANUFACTURER only one to zero or more CAR : makes
     
+
+ +
+    erDiagram
+      p[Person] {
+          string firstName
+          string lastName
+      }
+      a["Customer Account"] {
+          string email
+      }
+      p ||--o| a : has
+    
+
+ +
+    erDiagram
+      _customer_order {
+          bigint id PK
+          bigint customer_id FK
+          text shipping_address 
+          text delivery_method 
+          timestamp_with_time_zone ordered_at 
+          numeric total_tax_amount 
+          numeric total_price 
+          text payment_method 
+      }
+    
+
diff --git a/demos/sankey.html b/demos/sankey.html new file mode 100644 index 0000000000..5a76f476ab --- /dev/null +++ b/demos/sankey.html @@ -0,0 +1,124 @@ + + + + + + Sankey Mermaid Quick Test Page + + + + +

Sankey diagram demos

+

FY20-21 Performance

+
+      ---
+      config:
+        sankey:
+          showValues: true
+          prefix: $
+          suffix: B
+          width: 800
+          nodeAlignment: left
+      ---
+      sankey-beta
+        Revenue,Expenses,10
+        Revenue,Profit,10
+        Expenses,Manufacturing,5
+        Expenses,Tax,3
+        Expenses,Research,2
+    
+ +

Energy flow

+
+      ---
+      config:
+        sankey:
+          showValues: false
+          width: 1200
+          height: 600
+          linkColor: gradient
+          nodeAlignment: justify
+      ---
+      sankey-beta
+
+      Agricultural 'waste',Bio-conversion,124.729
+      Bio-conversion,Liquid,0.597
+      Bio-conversion,Losses,26.862
+      Bio-conversion,Solid,280.322
+      Bio-conversion,Gas,81.144
+      Biofuel imports,Liquid,35
+      Biomass imports,Solid,35
+      Coal imports,Coal,11.606
+      Coal reserves,Coal,63.965
+      Coal,Solid,75.571
+      District heating,Industry,10.639
+      District heating,Heating and cooling - commercial,22.505
+      District heating,Heating and cooling - homes,46.184
+      Electricity grid,Over generation / exports,104.453
+      Electricity grid,Heating and cooling - homes,113.726
+      Electricity grid,H2 conversion,27.14
+      Electricity grid,Industry,342.165
+      Electricity grid,Road transport,37.797
+      Electricity grid,Agriculture,4.412
+      Electricity grid,Heating and cooling - commercial,40.858
+      Electricity grid,Losses,56.691
+      Electricity grid,Rail transport,7.863
+      Electricity grid,Lighting & appliances - commercial,90.008
+      Electricity grid,Lighting & appliances - homes,93.494
+      Gas imports,Ngas,40.719
+      Gas reserves,Ngas,82.233
+      Gas,Heating and cooling - commercial,0.129
+      Gas,Losses,1.401
+      Gas,Thermal generation,151.891
+      Gas,Agriculture,2.096
+      Gas,Industry,48.58
+      Geothermal,Electricity grid,7.013
+      H2 conversion,H2,20.897
+      H2 conversion,Losses,6.242
+      H2,Road transport,20.897
+      Hydro,Electricity grid,6.995
+      Liquid,Industry,121.066
+      Liquid,International shipping,128.69
+      Liquid,Road transport,135.835
+      Liquid,Domestic aviation,14.458
+      Liquid,International aviation,206.267
+      Liquid,Agriculture,3.64
+      Liquid,National navigation,33.218
+      Liquid,Rail transport,4.413
+      Marine algae,Bio-conversion,4.375
+      Ngas,Gas,122.952
+      Nuclear,Thermal generation,839.978
+      Oil imports,Oil,504.287
+      Oil reserves,Oil,107.703
+      Oil,Liquid,611.99
+      Other waste,Solid,56.587
+      Other waste,Bio-conversion,77.81
+      Pumped heat,Heating and cooling - homes,193.026
+      Pumped heat,Heating and cooling - commercial,70.672
+      Solar PV,Electricity grid,59.901
+      Solar Thermal,Heating and cooling - homes,19.263
+      Solar,Solar Thermal,19.263
+      Solar,Solar PV,59.901
+      Solid,Agriculture,0.882
+      Solid,Thermal generation,400.12
+      Solid,Industry,46.477
+      Thermal generation,Electricity grid,525.531
+      Thermal generation,Losses,787.129
+      Thermal generation,District heating,79.329
+      Tidal,Electricity grid,9.452
+      UK land based bioenergy,Bio-conversion,182.01
+      Wave,Electricity grid,19.013
+      Wind,Electricity grid,289.366
+    
+ + + + diff --git a/docker-compose.yml b/docker-compose.yml index 2762f3b99f..5827a5cc64 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,36 @@ version: '3.9' services: mermaid: - image: node:18.16.0-alpine3.18 + image: node:18.17.1-alpine3.18 stdin_open: true tty: true working_dir: /mermaid + mem_limit: '4G' + environment: + - NODE_OPTIONS=--max_old_space_size=4096 volumes: - ./:/mermaid + - root_cache:/root/.cache + - root_local:/root/.local + - root_npm:/root/.npm + ports: + - 9000:9000 + - 3333:3333 + cypress: + image: cypress/included:12.17.4 + stdin_open: true + tty: true + working_dir: /mermaid + mem_limit: '2G' + entrypoint: cypress + environment: + - DISPLAY + volumes: + - ./:/mermaid + - /tmp/.X11-unix:/tmp/.X11-unix + network_mode: host + +volumes: + root_cache: + root_local: + root_npm: diff --git a/docs/community/code.md b/docs/community/code.md new file mode 100644 index 0000000000..e77a25467e --- /dev/null +++ b/docs/community/code.md @@ -0,0 +1,176 @@ +> **Warning** +> +> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT. +> +> ## Please edit the corresponding file in [/packages/mermaid/src/docs/community/code.md](../../packages/mermaid/src/docs/community/code.md). + +# Contributing Code + +The basic steps for contributing code are: + +```mermaid-example +graph LR + git[1. Checkout a git branch] --> codeTest[2. Write tests and code] --> doc[3. Update documentation] --> submit[4. Submit a PR] --> review[5. Review and merge] +``` + +```mermaid +graph LR + git[1. Checkout a git branch] --> codeTest[2. Write tests and code] --> doc[3. Update documentation] --> submit[4. Submit a PR] --> review[5. Review and merge] +``` + +1. **Create** and checkout a git branch and work on your code in the branch +2. Write and update **tests** (unit and perhaps even integration (e2e) tests) (If you do TDD/BDD, the order might be different.) +3. **Let users know** that things have changed or been added in the documents! This is often overlooked, but _critical_ +4. **Submit** your code as a _pull request_. +5. Maintainers will **review** your code. If there are no changes necessary, the PR will be merged. Otherwise, make the requested changes and repeat. + +## 1. Checkout a git branch + +Mermaid uses a [Git Flow](https://guides.github.com/introduction/flow/)–inspired approach to branching. + +Development is done in the `develop` branch. + +Once development is done we create a `release/vX.X.X` branch from `develop` for testing. + +Once the release happens we add a tag to the `release` branch and merge it with `master`. The live product and on-line documentation are what is in the `master` branch. + +**All new work should be based on the `develop` branch.** + +**When you are ready to do work, always, ALWAYS:** + +1. Make sure you have the most up-to-date version of the `develop` branch. (fetch or pull to update it) +2. Check out the `develop` branch +3. Create a new branch for your work. Please name the branch following our naming convention below. + +We use the following naming convention for branches: + +```txt + [feature | bug | chore | docs]/[issue number]_[short description using dashes ('-') or underscores ('_') instead of spaces] +``` + +You can always check current [configuration of labelling and branch prefixes](https://github.com/mermaid-js/mermaid/blob/develop/.github/pr-labeler.yml) + +- The first part is the **type** of change: a feature, bug, chore, or documentation change ('docs') +- followed by a _slash_ (which helps to group like types together in many git tools) +- followed by the **issue number** +- followed by an _underscore_ ('\_') +- followed by a short text description (but use dashes ('-') or underscores ('\_') instead of spaces) + +If your work is specific to a single diagram type, it is a good idea to put the diagram type at the start of the description. This will help us keep release notes organized: it will help us keep changes for a diagram type together. + +**Ex: A new feature described in issue 2945 that adds a new arrow type called 'florbs' to state diagrams** + +`feature/2945_state-diagram-new-arrow-florbs` + +**Ex: A bug described in issue 1123 that causes random ugly red text in multiple diagram types** +`bug/1123_fix_random_ugly_red_text` + +## 2. Write Tests + +Tests ensure that each function, module, or part of code does what it says it will do. This is critically +important when other changes are made to ensure that existing code is not broken (no regression). + +Just as important, the tests act as _specifications:_ they specify what the code does (or should do). +Whenever someone is new to a section of code, they should be able to read the tests to get a thorough understanding of what it does and why. + +If you are fixing a bug, you should add tests to ensure that your code has actually fixed the bug, to specify/describe what the code is doing, and to ensure the bug doesn't happen again. +(If there had been a test for the situation, the bug never would have happened in the first place.) +You may need to change existing tests if they were inaccurate. + +If you are adding a feature, you will definitely need to add tests. Depending on the size of your feature, you may need to add integration tests. + +### Unit Tests + +Unit tests are tests that test a single function or module. They are the easiest to write and the fastest to run. + +Unit tests are mandatory all code except the renderers. (The renderers are tested with integration tests.) + +We use [Vitest](https://vitest.dev) to run unit tests. + +You can use the following command to run the unit tests: + +```sh +pnpm test +``` + +When writing new tests, it's easier to have the tests automatically run as you make changes. You can do this by running the following command: + +```sh +pnpm test:watch +``` + +### Integration/End-to-End (e2e) tests + +These test the rendering and visual appearance of the diagrams. +This ensures that the rendering of that feature in the e2e will be reviewed in the release process going forward. Less chance that it breaks! + +To start working with the e2e tests: + +1. Run `pnpm dev` to start the dev server +2. Start **Cypress** by running `pnpm cypress:open`. + +The rendering tests are very straightforward to create. There is a function `imgSnapshotTest`, which takes a diagram in text form and the mermaid options, and it renders that diagram in Cypress. + +When running in CI it will take a snapshot of the rendered diagram and compare it with the snapshot from last build and flag it for review if it differs. + +This is what a rendering test looks like: + +```js +it('should render forks and joins', () => { + imgSnapshotTest( + ` + stateDiagram + state fork_state <<fork>> + [*] --> fork_state + fork_state --> State2 + fork_state --> State3 + + state join_state <<join>> + State2 --> join_state + State3 --> join_state + join_state --> State4 + State4 --> [*] + `, + { logLevel: 0 } + ); + cy.get('svg'); +}); +``` + +**_\[TODO - running the tests against what is expected in development. ]_** + +**_\[TODO - how to generate new screenshots]_** +.... + +## 3. Update Documentation + +If the users have no way to know that things have changed, then you haven't really _fixed_ anything for the users; you've just added to making Mermaid feel broken. +Likewise, if users don't know that there is a new feature that you've implemented, it will forever remain unknown and unused. + +The documentation has to be updated to users know that things have changed and added! +If you are adding a new feature, add `(v+)` in the title or description. It will be replaced automatically with the current version number when the release happens. + +eg: `# Feature Name (v+)` + +We know it can sometimes be hard to code _and_ write user documentation. + +Our documentation is managed in `packages/mermaid/src/docs`. Details on how to edit is in the [Contributing Documentation](#contributing-documentation) section. + +Create another issue specifically for the documentation.\ +You will need to help with the PR, but definitely ask for help if you feel stuck. +When it feels hard to write stuff out, explaining it to someone and having that person ask you clarifying questions can often be 80% of the work! + +When in doubt, write up and submit what you can. It can be clarified and refined later. (With documentation, something is better than nothing!) + +## 4. Submit your pull request + +**\[TODO - PR titles should start with (fix | feat | ....)]** + +We make all changes via Pull Requests (PRs). As we have many Pull Requests from developers new to Mermaid, we have put in place a process wherein _knsv, Knut Sveidqvist_ is in charge of the final release process and the active maintainers are in charge of reviewing and merging most PRs. + +- PRs will be reviewed by active maintainers, who will provide feedback and request changes as needed. +- The maintainers will request a review from knsv, if necessary. +- Once the PR is approved, the maintainers will merge the PR into the `develop` branch. +- When a release is ready, the `release/x.x.x` branch will be created, extensively tested and knsv will be in charge of the release process. + +**Reminder: Pull Requests should be submitted to the develop branch.** diff --git a/docs/community/development.md b/docs/community/development.md index 90f728e152..c9d8dab979 100644 --- a/docs/community/development.md +++ b/docs/community/development.md @@ -6,15 +6,9 @@ # Contributing to Mermaid -## Contents - -- [Technical Requirements and Setup](#technical-requirements-and-setup) -- [Contributing Code](#contributing-code) -- [Contributing Documentation](#contributing-documentation) -- [Questions or Suggestions?](#questions-or-suggestions) -- [Last Words](#last-words) - ---- +> The following documentation describes how to work with Mermaid in your host environment. +> There's also a [Docker installation guide](../community/docker-development.md) +> if you prefer to work in a Docker environment. So you want to help? That's great! @@ -22,352 +16,76 @@ So you want to help? That's great! Here are a few things to get you started on the right path. -## Technical Requirements and Setup +## Get the Source Code -### Technical Requirements +In GitHub, you first **fork** a repository when you are going to make changes and submit pull requests. -These are the tools we use for working with the code and documentation. - -- [volta](https://volta.sh/) to manage node versions. -- [Node.js](https://nodejs.org/en/). `volta install node` -- [pnpm](https://pnpm.io/) package manager. `volta install pnpm` -- [npx](https://docs.npmjs.com/cli/v8/commands/npx) the packaged executor in npm. This is needed [to install pnpm.](#2-install-pnpm) +Then you **clone** a copy to your local development machine (e.g. where you code) to make a copy with all the files to work with. -Follow [the setup steps below](#setup) to install them and verify they are working +[Fork mermaid](https://github.com/mermaid-js/mermaid/fork) to start contributing to the main project and its documentation, or [search for other repositories](https://github.com/orgs/mermaid-js/repositories). -### Setup +[Here is a GitHub document that gives an overview of the process.](https://docs.github.com/en/get-started/quickstart/fork-a-repo) -Follow these steps to set up the environment you need to work on code and/or documentation. +## Technical Requirements -#### 1. Fork and clone the repository +> The following documentation describes how to work with Mermaid in your host environment. +> There's also a [Docker installation guide](../community/docker-development.md) +> if you prefer to work in a Docker environment. -In GitHub, you first _fork_ a repository when you are going to make changes and submit pull requests. +These are the tools we use for working with the code and documentation: -Then you _clone_ a copy to your local development machine (e.g. where you code) to make a copy with all the files to work with. +- [volta](https://volta.sh/) to manage node versions. +- [Node.js](https://nodejs.org/en/). `volta install node` +- [pnpm](https://pnpm.io/) package manager. `volta install pnpm` +- [npx](https://docs.npmjs.com/cli/v8/commands/npx) the packaged executor in npm. This is needed [to install pnpm.](#install-packages) -[Here is a GitHub document that gives an overview of the process.](https://docs.github.com/en/get-started/quickstart/fork-a-repo) +Follow the setup steps below to install them and start the development. -#### 2. Install pnpm +## Setup and Launch -Once you have cloned the repository onto your development machine, change into the `mermaid` project folder so that you can install `pnpm`. You will need `npx` to install pnpm because volta doesn't support it yet. +### Switch to project -Ex: +Once you have cloned the repository onto your development machine, change into the `mermaid` project folder (the top level directory of the mermaid project repository) ```bash -# Change into the mermaid directory (the top level director of the mermaid project repository) cd mermaid -# npx is required for first install because volta does not support pnpm yet -npx pnpm install ``` -#### 3. Verify Everything Is Working +### Install packages -Once you have installed pnpm, you can run the `test` script to verify that pnpm is working _and_ that the repository has been cloned correctly: +Run `npx pnpm install`. You will need `npx` for this because volta doesn't support it yet. ```bash -pnpm test +npx pnpm install # npx is required for first install ``` -The `test` script and others are in the top-level `package.json` file. - -All tests should run successfully without any errors or failures. (You might see _lint_ or _formatting_ warnings; those are ok during this step.) - -### Docker - -If you are using docker and docker-compose, you have self-documented `run` bash script, which is a convenient alias for docker-compose commands: +### Launch ```bash -./run install # npx pnpm install -./run test # pnpm test -``` - -## Contributing Code - -The basic steps for contributing code are: - -```mermaid-example -graph LR - git[1. Checkout a git branch] --> codeTest[2. Write tests and code] --> doc[3. Update documentation] --> submit[4. Submit a PR] --> review[5. Review and merge] -``` - -```mermaid -graph LR - git[1. Checkout a git branch] --> codeTest[2. Write tests and code] --> doc[3. Update documentation] --> submit[4. Submit a PR] --> review[5. Review and merge] -``` - -1. **Create** and checkout a git branch and work on your code in the branch -2. Write and update **tests** (unit and perhaps even integration (e2e) tests) (If you do TDD/BDD, the order might be different.) -3. **Let users know** that things have changed or been added in the documents! This is often overlooked, but _critical_ -4. **Submit** your code as a _pull request_. -5. Maintainers will **review** your code. If there are no changes necessary, the PR will be merged. Otherwise, make the requested changes and repeat. - -### 1. Checkout a git branch - -Mermaid uses a [Git Flow](https://guides.github.com/introduction/flow/)–inspired approach to branching. - -Development is done in the `develop` branch. - -Once development is done we create a `release/vX.X.X` branch from `develop` for testing. - -Once the release happens we add a tag to the `release` branch and merge it with `master`. The live product and on-line documentation are what is in the `master` branch. - -**All new work should be based on the `develop` branch.** - -**When you are ready to do work, always, ALWAYS:** - -1. Make sure you have the most up-to-date version of the `develop` branch. (fetch or pull to update it) -2. Check out the `develop` branch -3. Create a new branch for your work. Please name the branch following our naming convention below. - -We use the follow naming convention for branches: - -```text - [feature | bug | chore | docs]/[issue number]_[short description using dashes ('-') or underscores ('_') instead of spaces] +npx pnpm run dev ``` -- The first part is the **type** of change: a feature, bug, chore, or documentation change ('docs') -- followed by a _slash_ (which helps to group like types together in many git tools) -- followed by the **issue number** -- followed by an _underscore_ ('\_') -- followed by a short text description (but use dashes ('-') or underscores ('\_') instead of spaces) - -If your work is specific to a single diagram type, it is a good idea to put the diagram type at the start of the description. This will help us keep release notes organized: it will help us keep changes for a diagram type together. - -**Ex: A new feature described in issue 2945 that adds a new arrow type called 'florbs' to state diagrams** - -`feature/2945_state-diagram-new-arrow-florbs` - -**Ex: A bug described in issue 1123 that causes random ugly red text in multiple diagram types** -`bug/1123_fix_random_ugly_red_text` +Now you are ready to make your changes! Edit whichever files in `src` as required. -### 2. Write Tests +Open in your browser, after starting the dev server. +There is a list of demos that can be used to see and test your changes. -Tests ensure that each function, module, or part of code does what it says it will do. This is critically -important when other changes are made to ensure that existing code is not broken (no regression). +If you need a specific diagram, you can duplicate the `example.html` file in `/demos/dev` and add your own mermaid code to your copy. -Just as important, the tests act as _specifications:_ they specify what the code does (or should do). -Whenever someone is new to a section of code, they should be able to read the tests to get a thorough understanding of what it does and why. +That will be served at . +After making code changes, the dev server will rebuild the mermaid library. You will need to reload the browser page yourself to see the changes. (PRs for auto reload are welcome!) -If you are fixing a bug, you should add tests to ensure that your code has actually fixed the bug, to specify/describe what the code is doing, and to ensure the bug doesn't happen again. -(If there had been a test for the situation, the bug never would have happened in the first place.) -You may need to change existing tests if they were inaccurate. +## Verify Everything is Working -If you are adding a feature, you will definitely need to add tests. Depending on the size of your feature, you may need to add integration tests. +You can run the `test` script to verify that pnpm is working _and_ that the repository has been cloned correctly: -#### Unit Tests - -Unit tests are tests that test a single function or module. They are the easiest to write and the fastest to run. - -Unit tests are mandatory all code except the renderers. (The renderers are tested with integration tests.) - -We use [Vitest](https://vitest.dev) to run unit tests. - -You can use the following command to run the unit tests: - -```sh +```bash pnpm test ``` -When writing new tests, it's easier to have the tests automatically run as you make changes. You can do this by running the following command: - -```sh -pnpm test:watch -``` - -#### Integration/End-to-End (e2e) tests - -These test the rendering and visual appearance of the diagrams. -This ensures that the rendering of that feature in the e2e will be reviewed in the release process going forward. Less chance that it breaks! - -To start working with the e2e tests: - -1. Run `pnpm dev` to start the dev server -2. Start **Cypress** by running `pnpm cypress:open`. - -The rendering tests are very straightforward to create. There is a function `imgSnapshotTest`, which takes a diagram in text form and the mermaid options, and it renders that diagram in Cypress. - -When running in CI it will take a snapshot of the rendered diagram and compare it with the snapshot from last build and flag it for review if it differs. - -This is what a rendering test looks like: - -```js -it('should render forks and joins', () => { - imgSnapshotTest( - ` - stateDiagram - state fork_state <<fork>> - [*] --> fork_state - fork_state --> State2 - fork_state --> State3 - - state join_state <<join>> - State2 --> join_state - State3 --> join_state - join_state --> State4 - State4 --> [*] - `, - { logLevel: 0 } - ); - cy.get('svg'); -}); -``` - -**_\[TODO - running the tests against what is expected in development. ]_** - -**_\[TODO - how to generate new screenshots]_** -.... - -### 3. Update Documentation - -If the users have no way to know that things have changed, then you haven't really _fixed_ anything for the users; you've just added to making Mermaid feel broken. -Likewise, if users don't know that there is a new feature that you've implemented, it will forever remain unknown and unused. - -The documentation has to be updated to users know that things have changed and added! - -We know it can sometimes be hard to code _and_ write user documentation. - -Our documentation is managed in `packages/mermaid/src/docs`. Details on how to edit is in the [Contributing Documentation](#contributing-documentation) section. - -Create another issue specifically for the documentation.\ -You will need to help with the PR, but definitely ask for help if you feel stuck. -When it feels hard to write stuff out, explaining it to someone and having that person ask you clarifying questions can often be 80% of the work! - -When in doubt, write up and submit what you can. It can be clarified and refined later. (With documentation, something is better than nothing!) - -### 4. Submit your pull request - -**\[TODO - PR titles should start with (fix | feat | ....)]** - -We make all changes via Pull Requests (PRs). As we have many Pull Requests from developers new to Mermaid, we have put in place a process wherein _knsv, Knut Sveidqvist_ is in charge of the final release process and the active maintainers are in charge of reviewing and merging most PRs. - -- PRs will be reviewed by active maintainers, who will provide feedback and request changes as needed. -- The maintainers will request a review from knsv, if necessary. -- Once the PR is approved, the maintainers will merge the PR into the `develop` branch. -- When a release is ready, the `release/x.x.x` branch will be created, extensively tested and knsv will be in charge of the release process. - -**Reminder: Pull Requests should be submitted to the develop branch.** - -## Contributing Documentation - -**_\[TODO: This section is still a WIP. It still needs MAJOR revision.]_** - -If it is not in the documentation, it's like it never happened. Wouldn't that be sad? With all the effort that was put into the feature? - -The docs are located in the `packages/mermaid/src/docs` folder and are written in Markdown. Just pick the right section and start typing. - -The contents of [mermaid.js.org](https://mermaid.js.org/) are based on the docs from the `master` branch. -Updates committed to the `master` branch are reflected in the [Mermaid Docs](https://mermaid.js.org/) once published. - -### How to Contribute to Documentation - -We are a little less strict here, it is OK to commit directly in the `develop` branch if you are a collaborator. - -The documentation is located in the `packages/mermaid/src/docs` directory and organized according to relevant subfolder. - -The `docs` folder will be automatically generated when committing to `packages/mermaid/src/docs` and **should not** be edited manually. - -```mermaid-example -flowchart LR - classDef default fill:#fff,color:black,stroke:black - - source["files in /packages/mermaid/src/docs\n(changes should be done here)"] -- automatic processing\nto generate the final documentation--> published["files in /docs\ndisplayed on the official documentation site"] - -``` - -```mermaid -flowchart LR - classDef default fill:#fff,color:black,stroke:black - - source["files in /packages/mermaid/src/docs\n(changes should be done here)"] -- automatic processing\nto generate the final documentation--> published["files in /docs\ndisplayed on the official documentation site"] - -``` - -You can use `note`, `tip`, `warning` and `danger` in triple backticks to add a note, tip, warning or danger box. -Do not use vitepress specific markdown syntax `::: warning` as it will not be processed correctly. - -```` -```note -Note content -``` - -```tip -Tip content -``` - -```warning -Warning content -``` - -```danger -Danger content -``` - -```` - -> **Note** -> If the change is _only_ to the documentation, you can get your changes published to the site quicker by making a PR to the `master` branch. - -We encourage contributions to the documentation at [packages/mermaid/src/docs in the _develop_ branch](https://github.com/mermaid-js/mermaid/tree/develop/packages/mermaid/src/docs). - -**_DO NOT CHANGE FILES IN `/docs`_** - -### The official documentation site - -**[The mermaid documentation site](https://mermaid.js.org/) is powered by [Vitepress](https://vitepress.vuejs.org/).** - -To run the documentation site locally: - -1. Run `pnpm --filter mermaid run docs:dev` to start the dev server. (Or `pnpm docs:dev` inside the `packages/mermaid` directory.) -2. Open in your browser. - -Markdown is used to format the text, for more information about Markdown [see the GitHub Markdown help page](https://help.github.com/en/github/writing-on-github/basic-writing-and-formatting-syntax). - -To edit Docs on your computer: - -_\[TODO: need to keep this in sync with [check out a git branch in Contributing Code above](#1-checkout-a-git-branch) ]_ - -1. Create a fork of the develop branch to work on. -2. Find the Markdown file (.md) to edit in the `packages/mermaid/src/docs` directory. -3. Make changes or add new documentation. -4. Commit changes to your branch and push it to GitHub (which should create a new branch). -5. Create a Pull Request of your fork. - -To edit Docs on GitHub: - -1. Login to [GitHub.com](https://www.github.com). -2. Navigate to [packages/mermaid/src/docs](https://github.com/mermaid-js/mermaid/tree/develop/packages/mermaid/src/docs) in the mermaid-js repository. -3. To edit a file, click the pencil icon at the top-right of the file contents panel. -4. Describe what you changed in the **Propose file change** section, located at the bottom of the page. -5. Submit your changes by clicking the button **Propose file change** at the bottom (by automatic creation of a fork and a new branch). -6. Visit the Actions tab in Github, `https://github.com//mermaid/actions` and enable the actions for your fork. This will ensure that the documentation is built and updated in your fork. -7. Create a Pull Request of your newly forked branch by clicking the green **Create Pull Request** button. - -### Documentation organization: Sidebar navigation - -If you want to propose changes to how the documentation is _organized_, such as adding a new section or re-arranging or renaming a section, you must update the **sidebar navigation.** - -The sidebar navigation is defined in [the vitepress configuration file config.ts](../.vitepress/config.ts). - -## Questions or Suggestions? - -#### First search to see if someone has already asked (and hopefully been answered) or suggested the same thing. - -- Search in Discussions -- Search in open Issues -- Search in closed Issues - -If you find an open issue or discussion thread that is similar to your question but isn't answered, you can let us know that you are also interested in it. -Use the GitHub reactions to add a thumbs-up to the issue or discussion thread. - -This helps the team know the relative interest in something and helps them set priorities and assignments. - -Feel free to add to the discussion on the issue or topic. - -If you can't find anything that already addresses your question or suggestion, _open a new issue:_ - -Log in to [GitHub.com](https://www.github.com), open or append to an issue [using the GitHub issue tracker of the mermaid-js repository](https://github.com/mermaid-js/mermaid/issues?q=is%3Aissue+is%3Aopen+label%3A%22Area%3A+Documentation%22). +The `test` script and others are in the top-level `package.json` file. -### How to Contribute a Suggestion +All tests should run successfully without any errors or failures. (You might see _lint_ or _formatting_ warnings; those are ok during this step.) ## Last Words diff --git a/docs/community/docker-development.md b/docs/community/docker-development.md new file mode 100644 index 0000000000..9df0c8d59c --- /dev/null +++ b/docs/community/docker-development.md @@ -0,0 +1,109 @@ +> **Warning** +> +> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT. +> +> ## Please edit the corresponding file in [/packages/mermaid/src/docs/community/docker-development.md](../../packages/mermaid/src/docs/community/docker-development.md). + +# Contributing to Mermaid via Docker + +> The following documentation describes how to work with Mermaid in a Docker environment. +> There's also a [host installation guide](../community/development.md) +> if you prefer to work without a Docker environment. + +So you want to help? That's great! + +![Image of happy people jumping with excitement](https://media.giphy.com/media/BlVnrxJgTGsUw/giphy.gif) + +Here are a few things to get you started on the right path. + +## Get the Source Code + +In GitHub, you first **fork** a repository when you are going to make changes and submit pull requests. + +Then you **clone** a copy to your local development machine (e.g. where you code) to make a copy with all the files to work with. + +[Fork mermaid](https://github.com/mermaid-js/mermaid/fork) to start contributing to the main project and its documentation, or [search for other repositories](https://github.com/orgs/mermaid-js/repositories). + +[Here is a GitHub document that gives an overview of the process.](https://docs.github.com/en/get-started/quickstart/fork-a-repo) + +## Technical Requirements + +> The following documentation describes how to work with Mermaid in a Docker environment. +> There's also a [host installation guide](../community/development.md) +> if you prefer to work without a Docker environment. + +[Install Docker](https://docs.docker.com/engine/install/). And that is pretty much all you need. + +Optionally, to run GUI (Cypress) within Docker you will also need an X11 server installed. +You might already have it installed, so check this by running: + +```bash +echo $DISPLAY +``` + +If the `$DISPLAY` variable is not empty, then an X11 server is running. Otherwise you may need to install one. + +## Setup and Launch + +### Switch to project + +Once you have cloned the repository onto your development machine, change into the `mermaid` project folder (the top level directory of the mermaid project repository) + +```bash +cd mermaid +``` + +### Make `./run` executable + +For development using Docker there is a self-documented `run` bash script, which provides convenient aliases for `docker compose` commands. + +Ensure `./run` script is executable: + +```bash +chmod +x run +``` + +> **💡 Tip** +> To get detailed help simply type `./run` or `./run help`. +> +> It also has short _Development quick start guide_ embedded. + +### Install packages + +```bash +./run pnpm install # Install packages +``` + +### Launch + +```bash +./run dev +``` + +Now you are ready to make your changes! Edit whichever files in `src` as required. + +Open in your browser, after starting the dev server. +There is a list of demos that can be used to see and test your changes. + +If you need a specific diagram, you can duplicate the `example.html` file in `/demos/dev` and add your own mermaid code to your copy. + +That will be served at . +After making code changes, the dev server will rebuild the mermaid library. You will need to reload the browser page yourself to see the changes. (PRs for auto reload are welcome!) + +## Verify Everything is Working + +```bash +./run pnpm test +``` + +The `test` script and others are in the top-level `package.json` file. + +All tests should run successfully without any errors or failures. (You might see _lint_ or _formatting_ warnings; those are ok during this step.) + +## Last Words + +Don't get daunted if it is hard in the beginning. We have a great community with only encouraging words. So, if you get stuck, ask for help and hints in the Slack forum. If you want to show off something good, show it off there. + +[Join our Slack community if you want closer contact!](https://join.slack.com/t/mermaid-talk/shared_invite/enQtNzc4NDIyNzk4OTAyLWVhYjQxOTI2OTg4YmE1ZmJkY2Y4MTU3ODliYmIwOTY3NDJlYjA0YjIyZTdkMDMyZTUwOGI0NjEzYmEwODcwOTE) + +![Image of superhero wishing you good luck](https://media.giphy.com/media/l49JHz7kJvl6MCj3G/giphy.gif) diff --git a/docs/community/documentation.md b/docs/community/documentation.md new file mode 100644 index 0000000000..67d35d3958 --- /dev/null +++ b/docs/community/documentation.md @@ -0,0 +1,105 @@ +> **Warning** +> +> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT. +> +> ## Please edit the corresponding file in [/packages/mermaid/src/docs/community/documentation.md](../../packages/mermaid/src/docs/community/documentation.md). + +# Contributing Documentation + +**_\[TODO: This section is still a WIP. It still needs MAJOR revision.]_** + +If it is not in the documentation, it's like it never happened. Wouldn't that be sad? With all the effort that was put into the feature? + +The docs are located in the `packages/mermaid/src/docs` folder and are written in Markdown. Just pick the right section and start typing. + +The contents of [mermaid.js.org](https://mermaid.js.org/) are based on the docs from the `master` branch. +Updates committed to the `master` branch are reflected in the [Mermaid Docs](https://mermaid.js.org/) once published. + +## How to Contribute to Documentation + +We are a little less strict here, it is OK to commit directly in the `develop` branch if you are a collaborator. + +The documentation is located in the `packages/mermaid/src/docs` directory and organized according to relevant subfolder. + +The `docs` folder will be automatically generated when committing to `packages/mermaid/src/docs` and **should not** be edited manually. + +```mermaid-example +flowchart LR + classDef default fill:#fff,color:black,stroke:black + + source["files in /packages/mermaid/src/docs\n(changes should be done here)"] -- automatic processing\nto generate the final documentation--> published["files in /docs\ndisplayed on the official documentation site"] + +``` + +```mermaid +flowchart LR + classDef default fill:#fff,color:black,stroke:black + + source["files in /packages/mermaid/src/docs\n(changes should be done here)"] -- automatic processing\nto generate the final documentation--> published["files in /docs\ndisplayed on the official documentation site"] + +``` + +You can use `note`, `tip`, `warning` and `danger` in triple backticks to add a note, tip, warning or danger box. +Do not use vitepress specific markdown syntax `::: warning` as it will not be processed correctly. + +````markdown +```note +Note content +``` + +```tip +Tip content +``` + +```warning +Warning content +``` + +```danger +Danger content +``` +```` + +> **Note** +> If the change is _only_ to the documentation, you can get your changes published to the site quicker by making a PR to the `master` branch. In that case, your branch should be based on master, not develop. + +We encourage contributions to the documentation at [packages/mermaid/src/docs in the _develop_ branch](https://github.com/mermaid-js/mermaid/tree/develop/packages/mermaid/src/docs). + +**_DO NOT CHANGE FILES IN `/docs`_** + +## The official documentation site + +**[The mermaid documentation site](https://mermaid.js.org/) is powered by [Vitepress](https://vitepress.vuejs.org/).** + +To run the documentation site locally: + +1. Run `pnpm --filter mermaid run docs:dev` to start the dev server. (Or `pnpm docs:dev` inside the `packages/mermaid` directory.) +2. Open in your browser. + +Markdown is used to format the text, for more information about Markdown [see the GitHub Markdown help page](https://help.github.com/en/github/writing-on-github/basic-writing-and-formatting-syntax). + +To edit Docs on your computer: + +_\[TODO: need to keep this in sync with [check out a git branch in Contributing Code above](#1-checkout-a-git-branch) ]_ + +1. Create a fork of the develop branch to work on. +2. Find the Markdown file (.md) to edit in the `packages/mermaid/src/docs` directory. +3. Make changes or add new documentation. +4. Commit changes to your branch and push it to GitHub (which should create a new branch). +5. Create a Pull Request from the branch of your fork. + +To edit Docs on GitHub: + +1. Login to [GitHub.com](https://www.github.com). +2. Navigate to [packages/mermaid/src/docs](https://github.com/mermaid-js/mermaid/tree/develop/packages/mermaid/src/docs) in the mermaid-js repository. +3. To edit a file, click the pencil icon at the top-right of the file contents panel. +4. Describe what you changed in the **Propose file change** section, located at the bottom of the page. +5. Submit your changes by clicking the button **Propose file change** at the bottom (by automatic creation of a fork and a new branch). +6. Visit the Actions tab in Github, `https://github.com//mermaid/actions` and enable the actions for your fork. This will ensure that the documentation is built and updated in your fork. +7. Create a Pull Request of your newly forked branch by clicking the green **Create Pull Request** button. + +## Documentation organization: Sidebar navigation + +If you want to propose changes to how the documentation is _organized_, such as adding a new section or re-arranging or renaming a section, you must update the **sidebar navigation.** + +The sidebar navigation is defined in [the vitepress configuration file config.ts](../.vitepress/config.ts). diff --git a/docs/community/n00b-overview.md b/docs/community/n00b-overview.md deleted file mode 100644 index afb110e9ec..0000000000 --- a/docs/community/n00b-overview.md +++ /dev/null @@ -1,74 +0,0 @@ -> **Warning** -> -> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT. -> -> ## Please edit the corresponding file in [/packages/mermaid/src/docs/community/n00b-overview.md](../../packages/mermaid/src/docs/community/n00b-overview.md). - -# Overview for Beginners - -**Explaining with a Diagram** - -A picture is worth a thousand words, a good diagram is undoubtedly worth more. They make understanding easier. - -## Creating and Maintaining Diagrams - -Anyone who has used Visio, or (God Forbid) Excel to make a Gantt Chart, knows how hard it is to create, edit and maintain good visualizations. - -Diagrams/Charts are significant but also become obsolete/inaccurate very fast. This catch-22 hobbles the productivity of teams. - -# Doc Rot in Diagrams - -Doc-Rot kills diagrams as quickly as it does text, but it takes hours in a desktop application to produce a diagram. - -Mermaid seeks to change using markdown-inspired syntax. The process is a quicker, less complicated, and more convenient way of going from concept to visualization. - -It is a relatively straightforward solution to a significant hurdle with the software teams. - -# Definition of Terms/ Dictionary - -**Mermaid text definitions can be saved for later reuse and editing.** - -> These are the Mermaid diagram definitions inside `
` tags, with the `class=mermaid`. - -```html -
-    graph TD
-    A[Client] --> B[Load Balancer]
-    B --> C[Server01]
-    B --> D[Server02]
-
-``` - -**render** - -> This is the core function of the Mermaid API. It reads all the `Mermaid Definitions` inside `div` tags and returns an SVG file, based on the definition. - -**Nodes** - -> These are the boxes that contain text or otherwise discrete pieces of each diagram, separated generally by arrows, except for Gantt Charts and User Journey Diagrams. They will be referred often in the instructions. Read for Diagram Specific [Syntax](../intro/n00b-syntaxReference.md) - -## Advantages of using Mermaid - -- Ease to generate, modify and render diagrams when you make them. -- The number of integrations and plugins it has. -- You can add it to your or companies website. -- Diagrams can be created through comments like this in a script: - -## The catch-22 of Diagrams and Charts: - -**Diagramming and charting is a large waste of developer's time, but not having diagrams ruins productivity.** - -Mermaid solves this by reducing the time and effort required to create diagrams and charts. - -Because, the text base for the diagrams allows it to be updated easily. Also, it can be made part of production scripts (and other pieces of code). So less time is spent on documenting, as a separate task. - -## Catching up with Development - -Being based on markdown, Mermaid can be used, not only by accomplished front-end developers, but by most computer savvy people to render diagrams, at much faster speeds. -In fact one can pick up the syntax for it quite easily from the examples given and there are many tutorials available in the internet. - -## Mermaid is for everyone. - -Video [Tutorials](https://mermaid.js.org/config/Tutorials.html) are also available for the mermaid [live editor](https://mermaid.live/). - -Alternatively you can use Mermaid [Plug-Ins](https://mermaid-js.github.io/mermaid/#/./integrations), with tools you already use, like Google Docs. diff --git a/docs/community/newDiagram.md b/docs/community/newDiagram.md index bb7e2a9615..b90d192b27 100644 --- a/docs/community/newDiagram.md +++ b/docs/community/newDiagram.md @@ -10,7 +10,7 @@ #### Grammar -This would be to define a jison grammar for the new diagram type. That should start with a way to identify that the text in the mermaid tag is a diagram of that type. Create a new folder under diagrams for your new diagram type and a parser folder in it. This leads us to step 2. +This would be to define a JISON grammar for the new diagram type. That should start with a way to identify that the text in the mermaid tag is a diagram of that type. Create a new folder under diagrams for your new diagram type and a parser folder in it. This leads us to step 2. For instance: @@ -19,7 +19,7 @@ For instance: #### Store data found during parsing -There are some jison specific sub steps here where the parser stores the data encountered when parsing the diagram, this data is later used by the renderer. You can during the parsing call a object provided to the parser by the user of the parser. This object can be called during parsing for storing data. +There are some jison specific sub steps here where the parser stores the data encountered when parsing the diagram, this data is later used by the renderer. You can during the parsing call an object provided to the parser by the user of the parser. This object can be called during parsing for storing data. ```jison statement @@ -35,7 +35,7 @@ In the extract of the grammar above, it is defined that a call to the setTitle m > **Note** > Make sure that the `parseError` function for the parser is defined and calling `mermaid.parseError`. This way a common way of detecting parse errors is provided for the end-user. -For more info look in the example diagram type: +For more info look at the example diagram type: The `yy` object has the following function: @@ -54,15 +54,15 @@ parser.yy = db; ### Step 2: Rendering -Write a renderer that given the data found during parsing renders the diagram. To look at an example look at sequenceRenderer.js rather then the flowchart renderer as this is a more generic example. +Write a renderer that given the data found during parsing renders the diagram. To look at an example look at sequenceRenderer.js rather than the flowchart renderer as this is a more generic example. Place the renderer in the diagram folder. ### Step 3: Detection of the new diagram type -The second thing to do is to add the capability to detect the new diagram to type to the detectType in utils.js. The detection should return a key for the new diagram type. +The second thing to do is to add the capability to detect the new diagram to type to the detectType in `diagram-api/detectType.ts`. The detection should return a key for the new diagram type. [This key will be used to as the aria roledescription](#aria-roledescription), so it should be a word that clearly describes the diagram type. -For example, if your new diagram use a UML deployment diagram, a good key would be "UMLDeploymentDiagram" because assistive technologies such as a screen reader +For example, if your new diagram uses a UML deployment diagram, a good key would be "UMLDeploymentDiagram" because assistive technologies such as a screen reader would voice that as "U-M-L Deployment diagram." Another good key would be "deploymentDiagram" because that would be voiced as "Deployment Diagram." A bad key would be "deployment" because that would not sufficiently describe the diagram. Note that the diagram type key does not have to be the same as the diagram keyword chosen for the [grammar](#grammar), but it is helpful if they are the same. @@ -122,54 +122,7 @@ There are a few features that are common between the different types of diagrams - Themes, there is a common way to modify the styling of diagrams in Mermaid. - Comments should follow mermaid standards -Here some pointers on how to handle these different areas. - -#### [Directives](../config/directives.md) - -Here is example handling from flowcharts: -Jison: - -```jison -/* lexical grammar */ -%lex -%x open_directive -%x type_directive -%x arg_directive -%x close_directive - -\%\%\{ { this.begin('open_directive'); return 'open_directive'; } -((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; } -":" { this.popState(); this.begin('arg_directive'); return ':'; } -\}\%\% { this.popState(); this.popState(); return 'close_directive'; } -((?:(?!\}\%\%).|\n)*) return 'arg_directive'; - -/* language grammar */ - -/* ... */ - -directive - : openDirective typeDirective closeDirective separator - | openDirective typeDirective ':' argDirective closeDirective separator - ; - -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', 'flowchart'); } - ; -``` - -It is probably a good idea to keep the handling similar to this in your new diagram. The parseDirective function is provided by the mermaidAPI. +Here are some pointers on how to handle these different areas. ## Accessibility @@ -187,9 +140,9 @@ See [the definition of aria-roledescription](https://www.w3.org/TR/wai-aria-1.1/ ### accessible title and description -The syntax for accessible titles and descriptions is described in [the Accessibility documenation section.](../config/accessibility.md) +The syntax for accessible titles and descriptions is described in [the Accessibility documentation section.](../config/accessibility.md) -In a similar way to the directives, the jison syntax are quite similar between the diagrams. +As a design goal, the jison syntax should be similar between the diagrams. ```jison diff --git a/docs/community/questions-and-suggestions.md b/docs/community/questions-and-suggestions.md new file mode 100644 index 0000000000..23d6de50f3 --- /dev/null +++ b/docs/community/questions-and-suggestions.md @@ -0,0 +1,26 @@ +> **Warning** +> +> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT. +> +> ## Please edit the corresponding file in [/packages/mermaid/src/docs/community/questions-and-suggestions.md](../../packages/mermaid/src/docs/community/questions-and-suggestions.md). + +# Questions or Suggestions? + +**_\[TODO: This section is still a WIP. It still needs MAJOR revision.]_** + +## First search to see if someone has already asked (and hopefully been answered) or suggested the same thing. + +- Search in Discussions +- Search in open Issues +- Search in closed Issues + +If you find an open issue or discussion thread that is similar to your question but isn't answered, you can let us know that you are also interested in it. +Use the GitHub reactions to add a thumbs-up to the issue or discussion thread. + +This helps the team know the relative interest in something and helps them set priorities and assignments. + +Feel free to add to the discussion on the issue or topic. + +If you can't find anything that already addresses your question or suggestion, _open a new issue:_ + +Log in to [GitHub.com](https://www.github.com), open or append to an issue [using the GitHub issue tracker of the mermaid-js repository](https://github.com/mermaid-js/mermaid/issues?q=is%3Aissue+is%3Aopen+label%3A%22Area%3A+Documentation%22). diff --git a/docs/config/Tutorials.md b/docs/config/Tutorials.md index 7b1530eaa8..9ae1779545 100644 --- a/docs/config/Tutorials.md +++ b/docs/config/Tutorials.md @@ -26,9 +26,13 @@ The definitions that can be generated the Live-Editor are also backwards-compati [Eddie Jaoude: Can you code your diagrams?](https://www.youtube.com/watch?v=9HZzKkAqrX8) +## Mermaid with OpenAI + +[Elle Neal: Mind Mapping with AI: An Accessible Approach for Neurodiverse Learners Tutorial:](https://medium.com/@elle.neal_71064/mind-mapping-with-ai-an-accessible-approach-for-neurodiverse-learners-1a74767359ff), [Demo:](https://databutton.com/v/jk9vrghc) + ## Mermaid with HTML -Examples are provided in [Getting Started](../intro/n00b-gettingStarted.md) +Examples are provided in [Getting Started](../intro/getting-started.md) **CodePen Examples:** diff --git a/docs/config/n00b-advanced.md b/docs/config/advanced.md similarity index 79% rename from docs/config/n00b-advanced.md rename to docs/config/advanced.md index 39c20d6326..28c0ca7245 100644 --- a/docs/config/n00b-advanced.md +++ b/docs/config/advanced.md @@ -2,9 +2,9 @@ > > ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT. > -> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/n00b-advanced.md](../../packages/mermaid/src/docs/config/n00b-advanced.md). +> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/advanced.md](../../packages/mermaid/src/docs/config/advanced.md). -# Advanced n00b mermaid (Coming soon..) +# Advanced mermaid (Coming soon..) ## splitting mermaid code from html diff --git a/docs/config/configuration.md b/docs/config/configuration.md index c7b780143d..1e85427ea7 100644 --- a/docs/config/configuration.md +++ b/docs/config/configuration.md @@ -10,10 +10,41 @@ When mermaid starts, configuration is extracted to determine a configuration to - The default configuration - Overrides at the site level are set by the initialize call, and will be applied to all diagrams in the site/app. The term for this is the **siteConfig**. -- Directives - diagram authors can update select configuration parameters directly in the diagram code via directives. These are applied to the render config. +- Frontmatter (v\+) - diagram authors can update select configuration parameters in the frontmatter of the diagram. These are applied to the render config. +- Directives (Deprecated by Frontmatter) - diagram authors can update select configuration parameters directly in the diagram code via directives. These are applied to the render config. **The render config** is configuration that is used when rendering by applying these configurations. +## Frontmatter config + +The entire mermaid configuration (except the secure configs) can be overridden by the diagram author in the frontmatter of the diagram. The frontmatter is a YAML block at the top of the diagram. + +```mermaid-example +--- +title: Hello Title +config: + theme: base + themeVariables: + primaryColor: "#00ff00" +--- +flowchart + Hello --> World + +``` + +```mermaid +--- +title: Hello Title +config: + theme: base + themeVariables: + primaryColor: "#00ff00" +--- +flowchart + Hello --> World + +``` + ## Theme configuration ## Starting mermaid diff --git a/docs/config/directives.md b/docs/config/directives.md index 27fd767c75..943d512173 100644 --- a/docs/config/directives.md +++ b/docs/config/directives.md @@ -6,6 +6,9 @@ # Directives +> **Warning** +> Directives are deprecated from v\. Please use the `config` key in frontmatter to pass configuration. See [Configuration](./configuration.md) for more details. + ## Directives Directives give a diagram author the capability to alter the appearance of a diagram before rendering by changing the applied configuration. @@ -123,7 +126,7 @@ The following code snippet changes `theme` to `forest`: `%%{init: { "theme": "forest" } }%%` -Possible theme values are: `default`,`base`, `dark`, `forest` and `neutral`. +Possible theme values are: `default`, `base`, `dark`, `forest` and `neutral`. Default Value is `default`. Example: @@ -288,7 +291,7 @@ Let us see an example: sequenceDiagram Alice->Bob: Hello Bob, how are you? -Bob->Alice: Fine, how did you mother like the book I suggested? And did you catch the new book about alien invasion? +Bob->Alice: Fine, how did your mother like the book I suggested? And did you catch the new book about alien invasion? Alice->Bob: Good. Bob->Alice: Cool ``` @@ -297,7 +300,7 @@ Bob->Alice: Cool sequenceDiagram Alice->Bob: Hello Bob, how are you? -Bob->Alice: Fine, how did you mother like the book I suggested? And did you catch the new book about alien invasion? +Bob->Alice: Fine, how did your mother like the book I suggested? And did you catch the new book about alien invasion? Alice->Bob: Good. Bob->Alice: Cool ``` @@ -314,7 +317,7 @@ By applying that snippet to the diagram above, `wrap` will be enabled: %%{init: { "sequence": { "wrap": true, "width":300 } } }%% sequenceDiagram Alice->Bob: Hello Bob, how are you? -Bob->Alice: Fine, how did you mother like the book I suggested? And did you catch the new book about alien invasion? +Bob->Alice: Fine, how did your mother like the book I suggested? And did you catch the new book about alien invasion? Alice->Bob: Good. Bob->Alice: Cool ``` @@ -323,7 +326,7 @@ Bob->Alice: Cool %%{init: { "sequence": { "wrap": true, "width":300 } } }%% sequenceDiagram Alice->Bob: Hello Bob, how are you? -Bob->Alice: Fine, how did you mother like the book I suggested? And did you catch the new book about alien invasion? +Bob->Alice: Fine, how did your mother like the book I suggested? And did you catch the new book about alien invasion? Alice->Bob: Good. Bob->Alice: Cool ``` diff --git a/docs/config/setup/interfaces/mermaidAPI.ParseOptions.md b/docs/config/setup/interfaces/mermaidAPI.ParseOptions.md index 8ab2598855..2082a081ea 100644 --- a/docs/config/setup/interfaces/mermaidAPI.ParseOptions.md +++ b/docs/config/setup/interfaces/mermaidAPI.ParseOptions.md @@ -16,4 +16,4 @@ #### Defined in -[mermaidAPI.ts:77](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L77) +[mermaidAPI.ts:78](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L78) diff --git a/docs/config/setup/interfaces/mermaidAPI.RenderResult.md b/docs/config/setup/interfaces/mermaidAPI.RenderResult.md index 527b46d092..f84a51b87b 100644 --- a/docs/config/setup/interfaces/mermaidAPI.RenderResult.md +++ b/docs/config/setup/interfaces/mermaidAPI.RenderResult.md @@ -39,7 +39,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present. #### Defined in -[mermaidAPI.ts:97](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L97) +[mermaidAPI.ts:98](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L98) --- @@ -51,4 +51,4 @@ The svg code for the rendered graph. #### Defined in -[mermaidAPI.ts:87](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L87) +[mermaidAPI.ts:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L88) diff --git a/docs/config/setup/modules/config.md b/docs/config/setup/modules/config.md index 8381dc8c73..f1de64e2df 100644 --- a/docs/config/setup/modules/config.md +++ b/docs/config/setup/modules/config.md @@ -14,7 +14,7 @@ #### Defined in -[config.ts:7](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L7) +[config.ts:8](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L8) ## Functions @@ -26,9 +26,9 @@ Pushes in a directive to the configuration #### Parameters -| Name | Type | Description | -| :---------- | :---- | :----------------------- | -| `directive` | `any` | The directive to push in | +| Name | Type | Description | +| :---------- | :-------------- | :----------------------- | +| `directive` | `MermaidConfig` | The directive to push in | #### Returns @@ -36,7 +36,7 @@ Pushes in a directive to the configuration #### Defined in -[config.ts:191](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L191) +[config.ts:188](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L188) --- @@ -60,7 +60,7 @@ The currentConfig #### Defined in -[config.ts:137](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L137) +[config.ts:131](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L131) --- @@ -118,7 +118,7 @@ The siteConfig #### Defined in -[config.ts:223](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L223) +[config.ts:218](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L218) --- @@ -147,7 +147,7 @@ options in-place #### Defined in -[config.ts:152](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L152) +[config.ts:146](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L146) --- @@ -242,10 +242,10 @@ The new siteConfig #### Parameters -| Name | Type | -| :------------ | :-------------- | -| `siteCfg` | `MermaidConfig` | -| `_directives` | `any`\[] | +| Name | Type | +| :------------ | :----------------- | +| `siteCfg` | `MermaidConfig` | +| `_directives` | `MermaidConfig`\[] | #### Returns @@ -253,7 +253,7 @@ The new siteConfig #### Defined in -[config.ts:14](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L14) +[config.ts:15](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L15) --- diff --git a/docs/config/setup/modules/defaultConfig.md b/docs/config/setup/modules/defaultConfig.md index d95ec4e922..effaec7b13 100644 --- a/docs/config/setup/modules/defaultConfig.md +++ b/docs/config/setup/modules/defaultConfig.md @@ -10,47 +10,24 @@ ### configKeys -• `Const` **configKeys**: `string`\[] +• `Const` **configKeys**: `Set`<`string`> #### Defined in -[defaultConfig.ts:2293](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L2293) +[defaultConfig.ts:268](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L268) --- ### default -• `Const` **default**: `Partial`<`MermaidConfig`> +• `Const` **default**: `RequiredDeep`<`MermaidConfig`> -**Configuration methods in Mermaid version 8.6.0 have been updated, to learn more\[[click -here](8.6.0_docs.md)].** +Default mermaid configuration options. -## **What follows are config instructions for older versions** - -These are the default options which can be overridden with the initialization call like so: - -**Example 1:** - -```js -mermaid.initialize({ flowchart: { htmlLabels: false } }); -``` - -**Example 2:** - -```html - -``` - -A summary of all options and their defaults is found [here](#mermaidapi-configuration-defaults). -A description of each option follows below. +Please see the Mermaid config JSON Schema for the default JSON values. +Non-JSON JS default values are listed in this file, e.g. functions, or +`undefined` (explicitly set so that `configKeys` finds them). #### Defined in -[defaultConfig.ts:33](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L33) +[defaultConfig.ts:18](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L18) diff --git a/docs/config/setup/modules/mermaidAPI.md b/docs/config/setup/modules/mermaidAPI.md index 591b6841a0..d5d4a1cbc1 100644 --- a/docs/config/setup/modules/mermaidAPI.md +++ b/docs/config/setup/modules/mermaidAPI.md @@ -25,7 +25,7 @@ Renames and re-exports [mermaidAPI](mermaidAPI.md#mermaidapi) #### Defined in -[mermaidAPI.ts:81](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L81) +[mermaidAPI.ts:82](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L82) ## Variables @@ -96,7 +96,7 @@ mermaid.initialize(config); #### Defined in -[mermaidAPI.ts:663](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L663) +[mermaidAPI.ts:673](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L673) ## Functions @@ -127,7 +127,7 @@ Return the last node appended #### Defined in -[mermaidAPI.ts:308](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L308) +[mermaidAPI.ts:310](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L310) --- @@ -320,4 +320,4 @@ Remove any existing elements from the given document #### Defined in -[mermaidAPI.ts:358](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L358) +[mermaidAPI.ts:360](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L360) diff --git a/docs/config/theming.md b/docs/config/theming.md index 580afb4887..63271a39b7 100644 --- a/docs/config/theming.md +++ b/docs/config/theming.md @@ -73,9 +73,9 @@ To make a custom theme, modify `themeVariables` via `init`. You will need to use the [base](#available-themes) theme as it is the only modifiable theme. -| Parameter | Description | Type | Properties | -| -------------- | ------------------------------------ | ------ | --------------------------------------------------------------------------------------------------- | -| themeVariables | Modifiable with the `init` directive | Object | `primaryColor`, `primaryTextColor`, `lineColor` ([see full list](#theme-variables-reference-table)) | +| Parameter | Description | Type | Properties | +| -------------- | ------------------------------------ | ------ | ----------------------------------------------------------------------------------- | +| themeVariables | Modifiable with the `init` directive | Object | `primaryColor`, `primaryTextColor`, `lineColor` ([see full list](#theme-variables)) | Example of modifying `themeVariables` using the `init` directive: diff --git a/docs/config/usage.md b/docs/config/usage.md index 4203e3a13a..8bd34345f1 100644 --- a/docs/config/usage.md +++ b/docs/config/usage.md @@ -41,7 +41,7 @@ pnpm add mermaid **Hosting mermaid on a web page:** -> Note:This topic explored in greater depth in the [User Guide for Beginners](../intro/n00b-gettingStarted.md) +> Note: This topic is explored in greater depth in the [User Guide for Beginners](../intro/getting-started.md) The easiest way to integrate mermaid on a web page requires two elements: @@ -100,7 +100,7 @@ Mermaid can load multiple diagrams, in the same page. ## Enabling Click Event and Tags in Nodes -A `securityLevel` configuration has to first be cleared. `securityLevel` sets the level of trust for the parsed diagrams and limits click functionality. This was introduce in version 8.2 as a security improvement, aimed at preventing malicious use. +A `securityLevel` configuration has to first be cleared. `securityLevel` sets the level of trust for the parsed diagrams and limits click functionality. This was introduced in version 8.2 as a security improvement, aimed at preventing malicious use. **It is the site owner's responsibility to discriminate between trustworthy and untrustworthy user-bases and we encourage the use of discretion.** @@ -115,13 +115,13 @@ Values: - **strict**: (**default**) HTML tags in the text are encoded and click functionality is disabled. - **antiscript**: HTML tags in text are allowed (only script elements are removed) and click functionality is enabled. - **loose**: HTML tags in text are allowed and click functionality is enabled. -- **sandbox**: With this security level, all rendering takes place in a sandboxed iframe. This prevent any JavaScript from running in the context. This may hinder interactive functionality of the diagram, like scripts, popups in the sequence diagram, links to other tabs or targets, etc. +- **sandbox**: With this security level, all rendering takes place in a sandboxed iframe. This prevents any JavaScript from running in the context. This may hinder interactive functionality of the diagram, like scripts, popups in the sequence diagram, links to other tabs or targets, etc. > **Note** > This changes the default behaviour of mermaid so that after upgrade to 8.2, unless the `securityLevel` is not changed, tags in flowcharts are encoded as tags and clicking is disabled. > **sandbox** security level is still in the beta version. -**If you are taking responsibility for the diagram source security you can set the `securityLevel` to a value of your choosing . This allows clicks and tags are allowed.** +**If you are taking responsibility for the diagram source security you can set the `securityLevel` to a value of your choosing. This allows clicks and tags are allowed.** **To change `securityLevel`, you have to call `mermaid.initialize`:** @@ -228,7 +228,7 @@ mermaid fully supports webpack. Here is a [working demo](https://github.com/merm The main idea of the API is to be able to call a render function with the graph definition as a string. The render function will render the graph and call a callback with the resulting SVG code. With this approach it is up to the site creator to fetch the graph definition from the site (perhaps from a textarea), render it and place the graph somewhere in the site. -The example below show an outline of how this could be used. The example just logs the resulting SVG to the JavaScript console. +The example below shows an example of how this could be used. The example just logs the resulting SVG to the JavaScript console. ```html +``` + +### Links Coloring + +You can adjust links' color by setting `linkColor` to one of those: + +- `source` - link will be of a source node color +- `target` - link will be of a target node color +- `gradient` - link color will be smoothly transient between source and target node colors +- hex code of color, like `#a1a1a1` + +### Node Alignment + +Graph layout can be changed by setting `nodeAlignment` to: + +- `justify` +- `center` +- `left` +- `right` diff --git a/docs/syntax/sequenceDiagram.md b/docs/syntax/sequenceDiagram.md index 26f81452d8..fbfa59d139 100644 --- a/docs/syntax/sequenceDiagram.md +++ b/docs/syntax/sequenceDiagram.md @@ -94,6 +94,43 @@ sequenceDiagram J->>A: Great! ``` +### Actor Creation and Destruction (v10.3.0+) + +It is possible to create and destroy actors by messages. To do so, add a create or destroy directive before the message. + + create participant B + A --> B: Hello + +Create directives support actor/participant distinction and aliases. The sender or the recipient of a message can be destroyed but only the recipient can be created. + +```mermaid-example +sequenceDiagram + Alice->>Bob: Hello Bob, how are you ? + Bob->>Alice: Fine, thank you. And you? + create participant Carl + Alice->>Carl: Hi Carl! + create actor D as Donald + Carl->>D: Hi! + destroy Carl + Alice-xCarl: We are too many + destroy Bob + Bob->>Alice: I agree +``` + +```mermaid +sequenceDiagram + Alice->>Bob: Hello Bob, how are you ? + Bob->>Alice: Fine, thank you. And you? + create participant Carl + Alice->>Carl: Hi Carl! + create actor D as Donald + Carl->>D: Hi! + destroy Carl + Alice-xCarl: We are too many + destroy Bob + Bob->>Alice: I agree +``` + ### 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: @@ -127,7 +164,7 @@ The actor(s) can be grouped in vertical boxes. You can define a color (if not, i end A->>J: Hello John, how are you? J->>A: Great! - A->>B: Hello Bob, how is Charly ? + A->>B: Hello Bob, how is Charly? B->>C: Hello Charly, how are you? ``` @@ -143,7 +180,7 @@ The actor(s) can be grouped in vertical boxes. You can define a color (if not, i end A->>J: Hello John, how are you? J->>A: Great! - A->>B: Hello Bob, how is Charly ? + A->>B: Hello Bob, how is Charly? B->>C: Hello Charly, how are you? ``` diff --git a/docs/syntax/stateDiagram.md b/docs/syntax/stateDiagram.md index 807d6149a9..4c28c82b30 100644 --- a/docs/syntax/stateDiagram.md +++ b/docs/syntax/stateDiagram.md @@ -487,7 +487,7 @@ where - the second _property_ is `color` and its _value_ is `white` - the third _property_ is `font-weight` and its _value_ is `bold` - the fourth _property_ is `stroke-width` and its _value_ is `2px` -- the fifth _property_ is `stroke` and its _value_ is `yello` +- the fifth _property_ is `stroke` and its _value_ is `yellow` ### Apply classDef styles to states diff --git a/docs/syntax/timeline.md b/docs/syntax/timeline.md index d9915ff3ef..d42a2dc7c4 100644 --- a/docs/syntax/timeline.md +++ b/docs/syntax/timeline.md @@ -257,9 +257,11 @@ let us look at same example, where we have disabled the multiColor option. ### 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 sections, 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. +You can customize the color scheme using the `cScale0` to `cScale11` theme variables, which will change the background colors. Mermaid allows you to set unique colors for up-to 12 sections, 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. +If you also want to change the foreground color of a section, you can do so use theme variables corresponding `cScaleLabel0` to `cScaleLabel11` variables. + 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: @@ -268,9 +270,9 @@ Now let's override the default values for the `cScale0` to `cScale2` variables: ```mermaid-example %%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': { - 'cScale0': '#ff0000', + 'cScale0': '#ff0000', 'cScaleLabel0': '#ffffff', 'cScale1': '#00ff00', - 'cScale2': '#0000ff' + 'cScale2': '#0000ff', 'cScaleLabel2': '#ffffff' } } }%% timeline title History of Social Media Platform @@ -286,9 +288,9 @@ Now let's override the default values for the `cScale0` to `cScale2` variables: ```mermaid %%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': { - 'cScale0': '#ff0000', + 'cScale0': '#ff0000', 'cScaleLabel0': '#ffffff', 'cScale1': '#00ff00', - 'cScale2': '#0000ff' + 'cScale2': '#0000ff', 'cScaleLabel2': '#ffffff' } } }%% timeline title History of Social Media Platform diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000000..2853f4c82d --- /dev/null +++ b/netlify.toml @@ -0,0 +1,18 @@ +# Settings in the [build] context are global and are applied to +# all contexts unless otherwise overridden by more specific contexts. +[build] + # Directory where the build system installs dependencies + # and runs your build. Store your package.json, .nvmrc, etc here. + # If not set, defaults to the root directory. + base = "" + + # Directory that contains the deploy-ready HTML files and + # assets generated by the build. This is an absolute path relative + # to the base directory, which is the root by default (/). + # This sample publishes the directory located at the absolute + # path "root/project/build-output" + + publish = "mermaid-live-editor/docs" + + # Default build command. + command = "./scripts/editor.bash" diff --git a/package.json b/package.json index ac197712cb..232f23be1d 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "mermaid-monorepo", "private": true, - "version": "10.2.3", + "version": "10.2.4", "description": "Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.", "type": "module", - "packageManager": "pnpm@8.6.2", + "packageManager": "pnpm@8.7.1", "keywords": [ "diagram", "markdown", @@ -22,7 +22,7 @@ "build:watch": "pnpm build:vite --watch", "build": "pnpm run -r clean && pnpm build:types && pnpm build:vite", "dev": "concurrently \"pnpm build:vite --watch\" \"ts-node-esm .vite/server.ts\"", - "dev:coverage": "VITE_COVERAGE=true pnpm dev", + "dev:coverage": "pnpm coverage:cypress:clean && VITE_COVERAGE=true pnpm dev", "release": "pnpm build", "lint": "eslint --cache --cache-strategy content --ignore-path .gitignore . && pnpm lint:jison && prettier --cache --check .", "lint:fix": "eslint --cache --cache-strategy content --fix --ignore-path .gitignore . && prettier --write . && ts-node-esm scripts/fixCSpell.ts", @@ -31,7 +31,8 @@ "cypress": "cypress run", "cypress:open": "cypress open", "e2e": "start-server-and-test dev http://localhost:9000/ cypress", - "e2e:coverage": "VITE_COVERAGE=true pnpm e2e", + "coverage:cypress:clean": "rimraf .nyc_output coverage/cypress", + "e2e:coverage": "pnpm coverage:cypress:clean && VITE_COVERAGE=true pnpm e2e", "coverage:merge": "ts-node-esm scripts/coverage.ts", "coverage": "pnpm test:coverage --run && pnpm e2e:coverage && pnpm coverage:merge", "ci": "vitest run", @@ -77,38 +78,38 @@ "@types/rollup-plugin-visualizer": "^4.2.1", "@typescript-eslint/eslint-plugin": "^5.59.0", "@typescript-eslint/parser": "^5.59.0", - "@vitest/coverage-istanbul": "^0.32.2", - "@vitest/spy": "^0.32.2", - "@vitest/ui": "^0.32.2", + "@vitest/coverage-v8": "^0.34.0", + "@vitest/spy": "^0.34.0", + "@vitest/ui": "^0.34.0", + "ajv": "^8.12.0", "concurrently": "^8.0.1", "cors": "^2.8.5", - "coveralls": "^3.1.1", "cypress": "^12.10.0", "cypress-image-snapshot": "^4.0.1", - "esbuild": "^0.18.0", + "esbuild": "^0.19.0", "eslint": "^8.39.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-cypress": "^2.13.2", "eslint-plugin-html": "^7.1.0", "eslint-plugin-jest": "^27.2.1", - "eslint-plugin-jsdoc": "^43.0.7", + "eslint-plugin-jsdoc": "^46.0.0", "eslint-plugin-json": "^3.1.0", "eslint-plugin-lodash": "^7.4.0", "eslint-plugin-markdown": "^3.0.0", "eslint-plugin-no-only-tests": "^3.1.0", "eslint-plugin-tsdoc": "^0.2.17", - "eslint-plugin-unicorn": "^46.0.0", + "eslint-plugin-unicorn": "^47.0.0", "express": "^4.18.2", "globby": "^13.1.4", "husky": "^8.0.3", "jest": "^29.5.0", "jison": "^0.4.18", "js-yaml": "^4.1.0", - "jsdom": "^21.1.1", + "jsdom": "^22.0.0", "lint-staged": "^13.2.1", "nyc": "^15.1.0", "path-browserify": "^1.0.1", - "pnpm": "^8.3.1", + "pnpm": "^8.6.8", "prettier": "^2.8.8", "prettier-plugin-jsdoc": "^0.4.2", "rimraf": "^5.0.0", @@ -118,10 +119,10 @@ "typescript": "^5.1.3", "vite": "^4.3.9", "vite-plugin-istanbul": "^4.1.0", - "vitest": "^0.32.2" + "vitest": "^0.34.0" }, "volta": { - "node": "18.16.0" + "node": "18.17.1" }, "nyc": { "report-dir": "coverage/cypress" diff --git a/packages/mermaid-example-diagram/package.json b/packages/mermaid-example-diagram/package.json index 1ea4135ef1..dc468a6c73 100644 --- a/packages/mermaid-example-diagram/package.json +++ b/packages/mermaid-example-diagram/package.json @@ -38,7 +38,7 @@ ] }, "dependencies": { - "@braintree/sanitize-url": "^6.0.0", + "@braintree/sanitize-url": "^6.0.1", "cytoscape": "^3.23.0", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.1.0", diff --git a/packages/mermaid-example-diagram/src/exampleDiagramDb.js b/packages/mermaid-example-diagram/src/exampleDiagramDb.js index a5fa88e6d7..21ba9e2482 100644 --- a/packages/mermaid-example-diagram/src/exampleDiagramDb.js +++ b/packages/mermaid-example-diagram/src/exampleDiagramDb.js @@ -22,7 +22,6 @@ export const setInfo = (inf) => { info = inf; }; -/** @returns Returns the info flag */ export const getInfo = () => { return info; }; diff --git a/packages/mermaid-example-diagram/src/exampleDiagramRenderer.js b/packages/mermaid-example-diagram/src/exampleDiagramRenderer.js index 2c6839203e..9b3854aafc 100644 --- a/packages/mermaid-example-diagram/src/exampleDiagramRenderer.js +++ b/packages/mermaid-example-diagram/src/exampleDiagramRenderer.js @@ -8,7 +8,6 @@ import { log, getConfig, setupGraphViewbox } from './mermaidUtils.js'; * @param {any} text * @param {any} id * @param {any} version - * @param diagObj */ export const draw = (text, id, version) => { try { diff --git a/packages/mermaid-example-diagram/src/mermaidUtils.ts b/packages/mermaid-example-diagram/src/mermaidUtils.ts index 44cc85f73c..eeeca05c5d 100644 --- a/packages/mermaid-example-diagram/src/mermaidUtils.ts +++ b/packages/mermaid-example-diagram/src/mermaidUtils.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ const warning = (s: string) => { // Todo remove debug code // eslint-disable-next-line no-console @@ -28,7 +29,6 @@ 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, svgElem: any, diff --git a/packages/mermaid-zenuml/README.md b/packages/mermaid-zenuml/README.md index e807400636..4300aecbe0 120000 --- a/packages/mermaid-zenuml/README.md +++ b/packages/mermaid-zenuml/README.md @@ -1 +1 @@ -../mermaid/src/docs/syntax/zenuml.md \ No newline at end of file +../mermaid/src/docs/syntax/zenuml.md diff --git a/packages/mermaid-zenuml/package.json b/packages/mermaid-zenuml/package.json index cb8da96b60..b907e2cbae 100644 --- a/packages/mermaid-zenuml/package.json +++ b/packages/mermaid-zenuml/package.json @@ -1,6 +1,6 @@ { "name": "@mermaid-js/mermaid-zenuml", - "version": "0.1.0", + "version": "0.1.2", "description": "MermaidJS plugin for ZenUML integration", "module": "dist/mermaid-zenuml.core.mjs", "types": "dist/detector.d.ts", @@ -33,7 +33,7 @@ ], "license": "MIT", "dependencies": { - "@zenuml/core": "^3.0.0" + "@zenuml/core": "^3.0.6" }, "devDependencies": { "mermaid": "workspace:^" diff --git a/packages/mermaid/.lintstagedrc.mjs b/packages/mermaid/.lintstagedrc.mjs index fe79ad2546..955000e20c 100644 --- a/packages/mermaid/.lintstagedrc.mjs +++ b/packages/mermaid/.lintstagedrc.mjs @@ -4,4 +4,5 @@ export default { 'src/docs/**': ['pnpm --filter mermaid run docs:build --git'], 'src/docs.mts': ['pnpm --filter mermaid run docs:build --git'], 'src/(defaultConfig|config|mermaidAPI).ts': ['pnpm --filter mermaid run docs:build --git'], + 'src/schemas/config.schema.yaml': ['pnpm --filter mermaid run types:build-config --git'], }; diff --git a/packages/mermaid/.madgerc b/packages/mermaid/.madgerc new file mode 100644 index 0000000000..1a558d9e62 --- /dev/null +++ b/packages/mermaid/.madgerc @@ -0,0 +1,22 @@ +{ + "detectiveOptions": { + "ts": { + "skipTypeImports": true + }, + "es6": { + "skipTypeImports": true + } + }, + "fileExtensions": [ + "js", + "ts" + ], + "excludeRegExp": [ + "node_modules", + "docs", + "vitepress", + "detector", + "Detector" + ], + "tsConfig": "./tsconfig.json" +} diff --git a/packages/mermaid/package.json b/packages/mermaid/package.json index d0c67ee58a..7b4de70a98 100644 --- a/packages/mermaid/package.json +++ b/packages/mermaid/package.json @@ -1,6 +1,6 @@ { "name": "mermaid", - "version": "10.2.3", + "version": "10.4.0", "description": "Markdown-ish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.", "type": "module", "module": "./dist/mermaid.core.mjs", @@ -24,14 +24,21 @@ ], "scripts": { "clean": "rimraf dist", + "dev": "pnpm -w dev", "docs:code": "typedoc src/defaultConfig.ts src/config.ts src/mermaidAPI.ts && prettier --write ./src/docs/config/setup", - "docs:build": "rimraf ../../docs && pnpm docs:spellcheck && pnpm docs:code && ts-node-esm src/docs.mts", - "docs:verify": "pnpm docs:spellcheck && pnpm docs:code && ts-node-esm src/docs.mts --verify", - "docs:pre:vitepress": "rimraf src/vitepress && pnpm docs:code && ts-node-esm src/docs.mts --vitepress", - "docs:build:vitepress": "pnpm docs:pre:vitepress && (cd src/vitepress && pnpm --filter ./ install --no-frozen-lockfile --ignore-scripts && pnpm run build) && cpy --flat src/docs/landing/ ./src/vitepress/.vitepress/dist/landing", - "docs:dev": "pnpm docs:pre:vitepress && concurrently \"pnpm --filter ./ src/vitepress dev\" \"ts-node-esm src/docs.mts --watch --vitepress\"", + "docs:build": "rimraf ../../docs && pnpm docs:spellcheck && pnpm docs:code && ts-node-esm scripts/docs.cli.mts", + "docs:verify": "pnpm docs:spellcheck && pnpm docs:code && ts-node-esm scripts/docs.cli.mts --verify", + "docs:pre:vitepress": "pnpm --filter ./src/docs prefetch && rimraf src/vitepress && pnpm docs:code && ts-node-esm scripts/docs.cli.mts --vitepress && pnpm --filter ./src/vitepress install --no-frozen-lockfile --ignore-scripts", + "docs:build:vitepress": "pnpm docs:pre:vitepress && (cd src/vitepress && pnpm run build) && cpy --flat src/docs/landing/ ./src/vitepress/.vitepress/dist/landing", + "docs:dev": "pnpm docs:pre:vitepress && concurrently \"pnpm --filter ./src/vitepress dev\" \"ts-node-esm scripts/docs.cli.mts --watch --vitepress\"", + "docs:dev:docker": "pnpm docs:pre:vitepress && concurrently \"pnpm --filter ./src/vitepress dev:docker\" \"ts-node-esm scripts/docs.cli.mts --watch --vitepress\"", "docs:serve": "pnpm docs:build:vitepress && vitepress serve src/vitepress", "docs:spellcheck": "cspell --config ../../cSpell.json \"src/docs/**/*.md\"", + "docs:release-version": "ts-node-esm scripts/update-release-version.mts", + "docs:verify-version": "ts-node-esm scripts/update-release-version.mts --verify", + "types:build-config": "ts-node-esm --transpileOnly scripts/create-types-from-json-schema.mts", + "types:verify-config": "ts-node-esm scripts/create-types-from-json-schema.mts --verify", + "checkCircle": "npx madge --circular ./src", "release": "pnpm build", "prepublishOnly": "cpy '../../README.*' ./ --cwd=. && pnpm -w run build" }, @@ -52,14 +59,17 @@ ] }, "dependencies": { - "@braintree/sanitize-url": "^6.0.2", + "@braintree/sanitize-url": "^6.0.1", + "@types/d3-scale": "^4.0.3", + "@types/d3-scale-chromatic": "^3.0.0", "cytoscape": "^3.23.0", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.1.0", "d3": "^7.4.0", + "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.10", "dayjs": "^1.11.7", - "dompurify": "3.0.3", + "dompurify": "^3.0.5", "elkjs": "^0.8.2", "khroma": "^2.0.0", "lodash-es": "^4.17.21", @@ -71,9 +81,13 @@ "web-worker": "^1.2.0" }, "devDependencies": { + "@adobe/jsonschema2md": "^7.1.4", "@types/cytoscape": "^3.19.9", "@types/d3": "^7.4.0", + "@types/d3-sankey": "^0.12.1", + "@types/d3-scale": "^4.0.3", "@types/d3-selection": "^3.0.5", + "@types/d3-shape": "^3.1.1", "@types/dompurify": "^3.0.2", "@types/jsdom": "^21.1.1", "@types/lodash-es": "^4.17.7", @@ -83,16 +97,17 @@ "@types/uuid": "^9.0.1", "@typescript-eslint/eslint-plugin": "^5.59.0", "@typescript-eslint/parser": "^5.59.0", + "ajv": "^8.11.2", "chokidar": "^3.5.3", "concurrently": "^8.0.1", - "coveralls": "^3.1.1", "cpy-cli": "^4.2.0", "cspell": "^6.31.1", "csstree-validator": "^3.0.0", "globby": "^13.1.4", "jison": "^0.4.18", "js-base64": "^3.7.5", - "jsdom": "^21.1.1", + "jsdom": "^22.0.0", + "json-schema-to-typescript": "^11.0.3", "micromatch": "^4.0.5", "path-browserify": "^1.0.1", "prettier": "^2.8.8", @@ -101,10 +116,12 @@ "remark-gfm": "^3.0.1", "rimraf": "^5.0.0", "start-server-and-test": "^2.0.0", - "typedoc": "^0.24.5", + "type-fest": "^4.1.0", + "typedoc": "^0.25.0", "typedoc-plugin-markdown": "^3.15.2", "typescript": "^5.0.4", "unist-util-flatmap": "^1.0.0", + "unist-util-visit": "^4.1.2", "vitepress": "^1.0.0-alpha.72", "vitepress-plugin-search": "^1.0.4-alpha.20" }, diff --git a/packages/mermaid/scripts/create-types-from-json-schema.mts b/packages/mermaid/scripts/create-types-from-json-schema.mts new file mode 100644 index 0000000000..836aaa4481 --- /dev/null +++ b/packages/mermaid/scripts/create-types-from-json-schema.mts @@ -0,0 +1,254 @@ +/** + * Script to load Mermaid Config JSON Schema from YAML and to: + * + * - Validate JSON Schema + * + * Then to generate: + * + * - config.types.ts TypeScript file + */ + +/* eslint-disable no-console */ + +import { readFile, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import assert from 'node:assert'; +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; + +import { load, JSON_SCHEMA } from 'js-yaml'; +import { compile, type JSONSchema } from 'json-schema-to-typescript'; +import prettier from 'prettier'; + +import _Ajv2019, { type JSONSchemaType } from 'ajv/dist/2019.js'; + +// Workaround for wrong AJV types, see +// https://github.com/ajv-validator/ajv/issues/2132#issuecomment-1290409907 +const Ajv2019 = _Ajv2019 as unknown as typeof _Ajv2019.default; + +// !!! -- The config.type.js file is created by this script -- !!! +import type { MermaidConfig } from '../src/config.type.js'; + +// options for running the main command +const verifyOnly = process.argv.includes('--verify'); +/** If `true`, automatically `git add` any changes (i.e. during `pnpm run pre-commit`)*/ +const git = process.argv.includes('--git'); + +/** + * All of the keys in the mermaid config that have a mermaid diagram config. + */ +const MERMAID_CONFIG_DIAGRAM_KEYS = [ + 'flowchart', + 'sequence', + 'gantt', + 'journey', + 'class', + 'state', + 'er', + 'pie', + 'quadrantChart', + 'requirement', + 'mindmap', + 'timeline', + 'gitGraph', + 'c4', + 'sankey', +]; + +/** + * Loads the MermaidConfig JSON schema YAML file. + * + * @returns The loaded JSON Schema, use {@link validateSchema} to confirm it is a valid JSON Schema. + */ +async function loadJsonSchemaFromYaml() { + const configSchemaFile = join('src', 'schemas', 'config.schema.yaml'); + const contentsYaml = await readFile(configSchemaFile, { encoding: 'utf8' }); + const jsonSchema = load(contentsYaml, { + filename: configSchemaFile, + // only allow JSON types in our YAML doc (will probably be default in YAML 1.3) + // e.g. `true` will be parsed a boolean `true`, `True` will be parsed as string `"True"`. + schema: JSON_SCHEMA, + }); + return jsonSchema; +} + +/** + * Asserts that the given value is a valid JSON Schema object. + * + * @param jsonSchema - The value to validate as JSON Schema 2019-09 + * @throws {Error} if the given object is invalid. + */ +function validateSchema(jsonSchema: unknown): asserts jsonSchema is JSONSchemaType { + if (typeof jsonSchema !== 'object') { + throw new Error(`jsonSchema param is not an object: actual type is ${typeof jsonSchema}`); + } + if (jsonSchema === null) { + throw new Error('jsonSchema param must not be null'); + } + const ajv = new Ajv2019({ + allErrors: true, + allowUnionTypes: true, + strict: true, + }); + + ajv.addKeyword({ + keyword: 'meta:enum', // used by jsonschema2md (in docs.mts script) + errors: false, + }); + ajv.addKeyword({ + keyword: 'tsType', // used by json-schema-to-typescript + errors: false, + }); + + ajv.compile(jsonSchema); +} + +/** + * Generate a typescript definition from a JSON Schema using json-schema-to-typescript. + * + * @param mermaidConfigSchema - The input JSON Schema. + */ +async function generateTypescript(mermaidConfigSchema: JSONSchemaType) { + /** + * Replace all usages of `allOf` with `extends`. + * + * `extends` is only valid JSON Schema in very old versions of JSON Schema. + * However, json-schema-to-typescript creates much nicer types when using + * `extends`, so we should use them instead when possible. + * + * @param schema - The input schema. + * @returns The schema with `allOf` replaced with `extends`. + */ + function replaceAllOfWithExtends(schema: JSONSchemaType>) { + if (schema['allOf']) { + const { allOf, ...schemaWithoutAllOf } = schema; + return { + ...schemaWithoutAllOf, + extends: allOf, + }; + } + return schema; + } + + /** + * For backwards compatibility with older Mermaid Typescript defs, + * we need to make all value optional instead of required. + * + * This is because the `MermaidConfig` type is used as an input, and everything is optional, + * since all the required values have default values.s + * + * In the future, we should make make the input to Mermaid `Partial`. + * + * @todo TODO: Remove this function when Mermaid releases a new breaking change. + * @param schema - The input schema. + * @returns The schema with all required values removed. + */ + function removeRequired(schema: JSONSchemaType>) { + return { ...schema, required: [] }; + } + + /** + * This is a temporary hack to control the order the types are generated in. + * + * By default, json-schema-to-typescript outputs the $defs in the order they + * are used, then any unused schemas at the end. + * + * **The only purpose of this function is to make the `git diff` simpler** + * **We should remove this later to simplify the code** + * + * @todo TODO: Remove this function in a future PR. + * @param schema - The input schema. + * @returns The schema with all `$ref`s removed. + */ + function unrefSubschemas(schema: JSONSchemaType>) { + return { + ...schema, + properties: Object.fromEntries( + Object.entries(schema.properties).map(([key, propertySchema]) => { + if (MERMAID_CONFIG_DIAGRAM_KEYS.includes(key)) { + const { $ref, ...propertySchemaWithoutRef } = propertySchema as JSONSchemaType; + if ($ref === undefined) { + throw Error( + `subSchema ${key} is in MERMAID_CONFIG_DIAGRAM_KEYS but does not have a $ref field` + ); + } + const [ + _root, // eslint-disable-line @typescript-eslint/no-unused-vars + _defs, // eslint-disable-line @typescript-eslint/no-unused-vars + defName, + ] = $ref.split('/'); + return [ + key, + { + ...propertySchemaWithoutRef, + tsType: defName, + }, + ]; + } + return [key, propertySchema]; + }) + ), + }; + } + + assert.ok(mermaidConfigSchema.$defs); + const modifiedSchema = { + ...unrefSubschemas(removeRequired(mermaidConfigSchema)), + + $defs: Object.fromEntries( + Object.entries(mermaidConfigSchema.$defs).map(([key, subSchema]) => { + return [key, removeRequired(replaceAllOfWithExtends(subSchema))]; + }) + ), + }; + + const typescriptFile = await compile( + modifiedSchema as JSONSchema, // json-schema-to-typescript only allows JSON Schema 4 as input type + 'MermaidConfig', + { + additionalProperties: false, // in JSON Schema 2019-09, these are called `unevaluatedProperties` + unreachableDefinitions: true, // definition for FontConfig is unreachable + style: (await prettier.resolveConfig('.')) ?? {}, + } + ); + + // TODO, should we somehow use the functions from `docs.mts` instead? + if (verifyOnly) { + const originalFile = await readFile('./src/config.type.ts', { encoding: 'utf-8' }); + if (typescriptFile !== originalFile) { + console.error('❌ Error: ./src/config.type.ts will be changed.'); + console.error("Please run 'pnpm run --filter mermaid types:build-config' to update this"); + process.exitCode = 1; + } else { + console.log('✅ ./src/config.type.ts will be unchanged'); + } + } else { + console.log('Writing typescript file to ./src/config.type.ts'); + await writeFile('./src/config.type.ts', typescriptFile, { encoding: 'utf8' }); + if (git) { + console.log('📧 Git: Adding ./src/config.type.ts changed'); + await promisify(execFile)('git', ['add', './src/config.type.ts']); + } + } +} + +/** Main function */ +async function main() { + if (verifyOnly) { + console.log( + 'Verifying that ./src/config.type.ts is in sync with src/schemas/config.schema.yaml' + ); + } + + const configJsonSchema = await loadJsonSchemaFromYaml(); + + validateSchema(configJsonSchema); + + // Generate types from JSON Schema + await generateTypescript(configJsonSchema); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/packages/mermaid/scripts/docs.cli.mts b/packages/mermaid/scripts/docs.cli.mts new file mode 100644 index 0000000000..067eec1c50 --- /dev/null +++ b/packages/mermaid/scripts/docs.cli.mts @@ -0,0 +1,3 @@ +import { processDocs } from './docs.mjs'; + +void processDocs(); diff --git a/packages/mermaid/src/docs.mts b/packages/mermaid/scripts/docs.mts similarity index 71% rename from packages/mermaid/src/docs.mts rename to packages/mermaid/scripts/docs.mts index 64c77254d1..3689cb68ba 100644 --- a/packages/mermaid/src/docs.mts +++ b/packages/mermaid/scripts/docs.mts @@ -27,14 +27,21 @@ * get their absolute paths. Ensures that the location of those 2 directories is not dependent on * where this file resides. * - * @todo Write a test file for this. (Will need to be able to deal .mts file. Jest has trouble with - * it.) */ +// @ts-ignore: we're importing internal jsonschema2md functions +import { default as schemaLoader } from '@adobe/jsonschema2md/lib/schemaProxy.js'; +// @ts-ignore: we're importing internal jsonschema2md functions +import { default as traverseSchemas } from '@adobe/jsonschema2md/lib/traverseSchema.js'; +// @ts-ignore: we're importing internal jsonschema2md functions +import { default as buildMarkdownFromSchema } from '@adobe/jsonschema2md/lib/markdownBuilder.js'; +// @ts-ignore: we're importing internal jsonschema2md functions +import { default as jsonSchemaReadmeBuilder } from '@adobe/jsonschema2md/lib/readmeBuilder.js'; import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync, rmdirSync } from 'fs'; import { exec } from 'child_process'; import { globby } from 'globby'; import { JSDOM } from 'jsdom'; -import type { Code, Root } from 'mdast'; +import { dump, load, JSON_SCHEMA } from 'js-yaml'; +import type { Code, ListItem, Root, Text, YAML } from 'mdast'; import { posix, dirname, relative, join } from 'path'; import prettier from 'prettier'; import { remark } from 'remark'; @@ -44,10 +51,11 @@ import chokidar from 'chokidar'; import mm from 'micromatch'; // @ts-ignore No typescript declaration file import flatmap from 'unist-util-flatmap'; +import { visit } from 'unist-util-visit'; -const MERMAID_MAJOR_VERSION = ( - JSON.parse(readFileSync('../mermaid/package.json', 'utf8')).version as string -).split('.')[0]; +export const MERMAID_RELEASE_VERSION = JSON.parse(readFileSync('../mermaid/package.json', 'utf8')) + .version as string; +const MERMAID_MAJOR_VERSION = MERMAID_RELEASE_VERSION.split('.')[0]; const CDN_URL = 'https://cdn.jsdelivr.net/npm'; // 'https://unpkg.com'; const MERMAID_KEYWORD = 'mermaid'; @@ -70,7 +78,7 @@ const vitepress: boolean = process.argv.includes('--vitepress'); const noHeader: boolean = process.argv.includes('--noHeader') || vitepress; // These paths are from the root of the mono-repo, not from the mermaid subdirectory -const SOURCE_DOCS_DIR = 'src/docs'; +export const SOURCE_DOCS_DIR = 'src/docs'; const FINAL_DOCS_DIR = vitepress ? 'src/vitepress' : '../../docs'; const LOGMSG_TRANSFORMED = 'transformed'; @@ -122,7 +130,7 @@ const changeToFinalDocDir = (file: string): string => { const logWasOrShouldBeTransformed = (filename: string, wasCopied: boolean) => { const changeMsg = wasCopied ? LOGMSG_TRANSFORMED : LOGMSG_TO_BE_TRANSFORMED; let logMsg: string; - logMsg = ` File ${changeMsg}: ${filename}`; + logMsg = ` File ${changeMsg}: ${filename.replace(FINAL_DOCS_DIR, SOURCE_DOCS_DIR)}`; if (wasCopied) { logMsg += LOGMSG_COPIED; } @@ -150,13 +158,14 @@ const copyTransformedContents = (filename: string, doCopy = false, transformedCo } filesTransformed.add(fileInFinalDocDir); + if (doCopy) { writeFileSync(fileInFinalDocDir, newBuffer); } logWasOrShouldBeTransformed(fileInFinalDocDir, doCopy); }; -const readSyncedUTF8file = (filename: string): string => { +export const readSyncedUTF8file = (filename: string): string => { return readFileSync(filename, 'utf8'); }; @@ -207,6 +216,8 @@ interface TransformMarkdownAstOptions { originalFilename: string; /** If `true`, add a warning that the file is autogenerated */ addAutogeneratedWarning?: boolean; + /** If `true`, adds an `editLink: "https://..."` YAML frontmatter field */ + addEditLink?: boolean; /** * If `true`, remove the YAML metadata from the Markdown input. * Generally, YAML metadata is only used for Vitepress. @@ -229,6 +240,7 @@ interface TransformMarkdownAstOptions { export function transformMarkdownAst({ originalFilename, addAutogeneratedWarning, + addEditLink, removeYAML, }: TransformMarkdownAstOptions) { return (tree: Root, _file?: any): Root => { @@ -268,6 +280,27 @@ export function transformMarkdownAst({ } } + if (addEditLink) { + // add originalFilename as custom editLink in YAML frontmatter + let yamlFrontMatter: YAML; + if (astWithTransformedBlocks.children[0].type === 'yaml') { + yamlFrontMatter = astWithTransformedBlocks.children[0]; + } else { + yamlFrontMatter = { + type: 'yaml', + value: '', + }; + astWithTransformedBlocks.children.unshift(yamlFrontMatter); + } + const filePathFromRoot = posix.join('packages/mermaid', originalFilename); + yamlFrontMatter.value = dump({ + ...(load(yamlFrontMatter.value, { schema: JSON_SCHEMA }) as + | Record + | undefined), + editLink: `https://github.com/mermaid-js/mermaid/edit/develop/${filePathFromRoot}`, + }); + } + if (removeYAML) { const firstNode = astWithTransformedBlocks.children[0]; if (firstNode.type == 'yaml') { @@ -304,6 +337,7 @@ const transformMarkdown = (file: string) => { // mermaid project specific plugin originalFilename: file, addAutogeneratedWarning: !noHeader, + addEditLink: noHeader, removeYAML: !noHeader, }) .processSync(doc) @@ -321,6 +355,115 @@ const transformMarkdown = (file: string) => { copyTransformedContents(file, !verifyOnly, formatted); }; +/** + * Transforms the given JSON Schema into Markdown documentation + */ +async function transformJsonSchema(file: string) { + const yamlContents = readSyncedUTF8file(file); + const jsonSchema = load(yamlContents, { + filename: file, + // only allow JSON types in our YAML doc (will probably be default in YAML 1.3) + // e.g. `true` will be parsed a boolean `true`, `True` will be parsed as string `"True"`. + schema: JSON_SCHEMA, + }); + + /** Location of the `schema.yaml` files */ + const SCHEMA_INPUT_DIR = 'src/schemas/'; + /** + * Location to store the generated `schema.json` file for the website + * + * Because Vitepress doesn't handle bundling `.json` files properly, we need + * to instead place it into a `public/` subdirectory. + */ + const SCHEMA_OUTPUT_DIR = 'src/docs/public/schemas/'; + const VITEPRESS_PUBLIC_DIR = 'src/docs/public'; + /** + * Location to store the generated Schema Markdown docs. + * Links to JSON Schemas should automatically be rewritten to point to + * `SCHEMA_OUTPUT_DIR`. + */ + const SCHEMA_MARKDOWN_OUTPUT_DIR = join('src', 'docs', 'config', 'schema-docs'); + + // write .schema.json files + const jsonFileName = file + .replace('.schema.yaml', '.schema.json') + .replace(SCHEMA_INPUT_DIR, SCHEMA_OUTPUT_DIR); + copyTransformedContents(jsonFileName, !verifyOnly, JSON.stringify(jsonSchema, undefined, 2)); + + const schemas = traverseSchemas([schemaLoader()(jsonFileName, jsonSchema)]); + + // ignore output of this function + // for some reason, without calling this function, we get some broken links + // this is probably a bug in @adobe/jsonschema2md + jsonSchemaReadmeBuilder({ readme: true })(schemas); + + // write Markdown files + const markdownFiles = buildMarkdownFromSchema({ + header: true, + // links, + includeProperties: ['tsType'], // Custom TypeScript type + exampleFormat: 'json', + // skipProperties, + /** + * Automatically rewrite schema paths passed to `schemaLoader` + * (e.g. src/docs/schemas/config.schema.json) + * to relative links (e.g. /schemas/config.schema.json) + * + * See https://vitepress.vuejs.org/guide/asset-handling + * + * @param origin - Original schema path (relative to this script). + * @returns New absolute Vitepress path to schema + */ + rewritelinks: (origin: string) => { + return `/${relative(VITEPRESS_PUBLIC_DIR, origin)}`; + }, + })(schemas); + + for (const [name, markdownAst] of Object.entries(markdownFiles)) { + /* + * Converts list entries of shape '- tsType: () => Partial' + * into '- tsType: `() => Partial`' (e.g. escaping with back-ticks), + * as otherwise VitePress doesn't like the bit. + */ + visit(markdownAst as Root, 'listItem', (listEntry: ListItem) => { + let listText: Text; + const blockItem = listEntry.children[0]; + if (blockItem.type === 'paragraph' && blockItem.children[0].type === 'text') { + listText = blockItem.children[0]; + } // @ts-expect-error: MD AST output from @adobe/jsonschema2md is technically wrong + else if (blockItem.type === 'text') { + listText = blockItem; + } else { + return; // skip + } + + if (listText.value.startsWith('tsType: ')) { + listText.value = listText.value.replace(/tsType: (.*)/g, 'tsType: `$1`'); + } + }); + + const transformer = remark() + .use(remarkGfm) + .use(remarkFrontmatter, ['yaml']) // support YAML front-matter in Markdown + .use(transformMarkdownAst, { + // mermaid project specific plugin + originalFilename: file, + addAutogeneratedWarning: !noHeader, + addEditLink: noHeader, + removeYAML: !noHeader, + }); + + const transformed = transformer.stringify(await transformer.run(markdownAst as Root)); + + const formatted = prettier.format(transformed, { + parser: 'markdown', + ...prettierConfig, + }); + const newFileName = join(SCHEMA_MARKDOWN_OUTPUT_DIR, `${name}.md`); + copyTransformedContents(newFileName, !verifyOnly, formatted); + } +} + /** * Transform an HTML file and write the transformed file to the directory for published * documentation @@ -361,26 +504,26 @@ const transformHtml = (filename: string) => { copyTransformedContents(filename, !verifyOnly, formattedHTML); }; -const getGlobs = (globs: string[]): string[] => { - globs.push( - '!**/dist', - '!**/redirect.spec.ts', - '!**/landing', - '!**/node_modules', - '!**/user-avatars' - ); +export const getGlobs = (globs: string[]): string[] => { + globs.push('!**/dist/**', '!**/redirect.spec.ts', '!**/landing/**', '!**/node_modules/**'); if (!vitepress) { - globs.push('!**/.vitepress', '!**/vite.config.ts', '!src/docs/index.md', '!**/package.json'); + globs.push( + '!**/.vitepress/**', + '!**/vite.config.ts', + '!src/docs/index.md', + '!**/package.json', + '!**/user-avatars/**' + ); } return globs; }; -const getFilesFromGlobs = async (globs: string[]): Promise => { +export const getFilesFromGlobs = async (globs: string[]): Promise => { return await globby(globs, { dot: true }); }; /** Main method (entry point) */ -const main = async () => { +export const processDocs = async () => { if (verifyOnly) { console.log('Verifying that all files are in sync with the source files'); } @@ -388,6 +531,14 @@ const main = async () => { const sourceDirGlob = posix.join('.', SOURCE_DOCS_DIR, '**'); const action = verifyOnly ? 'Verifying' : 'Transforming'; + if (vitepress) { + console.log(`${action} 1 .schema.yaml file`); + await transformJsonSchema('src/schemas/config.schema.yaml'); + } else { + // skip because this creates so many Markdown files that it lags git + console.log('Skipping 1 .schema.yaml file'); + } + const mdFileGlobs = getGlobs([posix.join(sourceDirGlob, '*.md')]); const mdFiles = await getFilesFromGlobs(mdFileGlobs); console.log(`${action} ${mdFiles.length} markdown files...`); @@ -450,5 +601,3 @@ const main = async () => { }); } }; - -void main(); diff --git a/packages/mermaid/src/docs.spec.ts b/packages/mermaid/scripts/docs.spec.ts similarity index 90% rename from packages/mermaid/src/docs.spec.ts rename to packages/mermaid/scripts/docs.spec.ts index 50feaee6ad..c84bc1bac9 100644 --- a/packages/mermaid/src/docs.spec.ts +++ b/packages/mermaid/scripts/docs.spec.ts @@ -105,6 +105,29 @@ This Markdown should be kept. }); }); + it('should add an editLink in the YAML frontmatter if `addEditLink: true`', async () => { + const contents = `--- +title: Flowcharts Syntax +--- + +This Markdown should be kept. +`; + const withYaml = ( + await remarkBuilder() + .use(transformMarkdownAst, { originalFilename, addEditLink: true }) + .process(contents) + ).toString(); + expect(withYaml).toEqual(`--- +title: Flowcharts Syntax +editLink: >- + https://github.com/mermaid-js/mermaid/edit/develop/packages/mermaid/example-input-filename.md + +--- + +This Markdown should be kept. +`); + }); + describe('transformToBlockQuote', () => { // TODO Is there a way to test this with --vitepress given as a process argument? diff --git a/packages/mermaid/scripts/update-release-version.mts b/packages/mermaid/scripts/update-release-version.mts new file mode 100644 index 0000000000..7f292f21b2 --- /dev/null +++ b/packages/mermaid/scripts/update-release-version.mts @@ -0,0 +1,53 @@ +/* eslint-disable no-console */ + +/** + * @file Update the MERMAID_RELEASE_VERSION placeholder in the documentation source files with the current version of mermaid. + * So contributors adding new features will only have to add the placeholder and not worry about updating the version number. + * + */ +import { posix } from 'path'; +import { + getGlobs, + getFilesFromGlobs, + SOURCE_DOCS_DIR, + readSyncedUTF8file, + MERMAID_RELEASE_VERSION, +} from './docs.mjs'; +import { writeFile } from 'fs/promises'; + +const verifyOnly: boolean = process.argv.includes('--verify'); +const versionPlaceholder = ''; + +const main = async () => { + const sourceDirGlob = posix.join('.', SOURCE_DOCS_DIR, '**'); + const mdFileGlobs = getGlobs([posix.join(sourceDirGlob, '*.md')]); + mdFileGlobs.push('!**/community/development.md'); + const mdFiles = await getFilesFromGlobs(mdFileGlobs); + mdFiles.sort(); + const mdFilesWithPlaceholder: string[] = []; + for (const mdFile of mdFiles) { + const content = readSyncedUTF8file(mdFile); + if (content.includes(versionPlaceholder)) { + mdFilesWithPlaceholder.push(mdFile); + } + } + + if (mdFilesWithPlaceholder.length === 0) { + return; + } + + if (verifyOnly) { + console.log( + `${mdFilesWithPlaceholder.length} file(s) were found with the placeholder ${versionPlaceholder}. Run \`pnpm --filter mermaid docs:release-version\` to update them.` + ); + process.exit(1); + } + + for (const mdFile of mdFilesWithPlaceholder) { + const content = readSyncedUTF8file(mdFile); + const newContent = content.replace(versionPlaceholder, MERMAID_RELEASE_VERSION); + await writeFile(mdFile, newContent); + } +}; + +void main(); diff --git a/packages/mermaid/src/Diagram.ts b/packages/mermaid/src/Diagram.ts index 4fb329b28e..308e141d03 100644 --- a/packages/mermaid/src/Diagram.ts +++ b/packages/mermaid/src/Diagram.ts @@ -4,8 +4,9 @@ import { getDiagram, registerDiagram } from './diagram-api/diagramAPI.js'; import { detectType, getDiagramLoader } from './diagram-api/detectType.js'; import { extractFrontMatter } from './diagram-api/frontmatter.js'; import { UnknownDiagramError } from './errors.js'; -import { DetailedError } from './utils.js'; import { cleanupComments } from './diagram-api/comments.js'; +import type { DetailedError } from './utils.js'; +import type { DiagramDefinition } from './diagram-api/types.js'; export type ParseErrorFunction = (err: string | DetailedError | unknown, hash?: any) => void; @@ -15,9 +16,11 @@ export type ParseErrorFunction = (err: string | DetailedError | unknown, hash?: */ export class Diagram { type = 'graph'; - parser; - renderer; - db; + parser: DiagramDefinition['parser']; + renderer: DiagramDefinition['renderer']; + db: DiagramDefinition['db']; + private init?: DiagramDefinition['init']; + private detectError?: UnknownDiagramError; constructor(public text: string) { this.text += '\n'; @@ -32,7 +35,6 @@ export class Diagram { log.debug('Type ' + this.type); // Setup diagram this.db = diagram.db; - this.db.clear?.(); this.renderer = diagram.renderer; this.parser = diagram.parser; const originalParse = this.parser.parse.bind(this.parser); @@ -46,13 +48,10 @@ export class Diagram { // extractFrontMatter(). this.parser.parse = (text: string) => - originalParse(cleanupComments(extractFrontMatter(text, this.db))); + originalParse(cleanupComments(extractFrontMatter(text, this.db, configApi.addDirective))); this.parser.parser.yy = this.db; - if (diagram.init) { - diagram.init(cnf); - log.info('Initialized diagram ' + this.type, cnf); - } + this.init = diagram.init; this.parse(); } @@ -61,6 +60,7 @@ export class Diagram { throw this.detectError; } this.db.clear?.(); + this.init?.(configApi.getConfig()); this.parser.parse(this.text); } diff --git a/packages/mermaid/src/accessibility.spec.ts b/packages/mermaid/src/accessibility.spec.ts index 7a3ab7f105..7745e02ef8 100644 --- a/packages/mermaid/src/accessibility.spec.ts +++ b/packages/mermaid/src/accessibility.spec.ts @@ -1,27 +1,24 @@ import { MockedD3 } from './tests/MockedD3.js'; import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility.js'; -import { D3Element } from './mermaidAPI.js'; +import type { D3Element } from './mermaidAPI.js'; describe('accessibility', () => { - const fauxSvgNode = new MockedD3(); + const fauxSvgNode: MockedD3 = new MockedD3(); describe('setA11yDiagramInfo', () => { - it('sets the svg element role to "graphics-document document"', () => { - // @ts-ignore Required to easily handle the d3 select types + it('should set svg element role to "graphics-document document"', () => { const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); setA11yDiagramInfo(fauxSvgNode, 'flowchart'); expect(svgAttrSpy).toHaveBeenCalledWith('role', 'graphics-document document'); }); - it('sets the aria-roledescription to the diagram type', () => { - // @ts-ignore Required to easily handle the d3 select types + it('should set aria-roledescription to the diagram type', () => { const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); setA11yDiagramInfo(fauxSvgNode, 'flowchart'); expect(svgAttrSpy).toHaveBeenCalledWith('aria-roledescription', 'flowchart'); }); - it('does not set the aria-roledescription if the diagram type is empty', () => { - // @ts-ignore Required to easily handle the d3 select types + it('should not set aria-roledescription if the diagram type is empty', () => { const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); setA11yDiagramInfo(fauxSvgNode, ''); expect(svgAttrSpy).toHaveBeenCalledTimes(1); @@ -32,8 +29,8 @@ describe('accessibility', () => { describe('addSVGa11yTitleDescription', () => { const givenId = 'theBaseId'; - describe('with the given svg d3 object:', () => { - it('does nothing if there is no insert defined', () => { + describe('with svg d3 object', () => { + it('should do nothing if there is no insert defined', () => { const noInsertSvg = { attr: vi.fn(), }; @@ -42,26 +39,25 @@ describe('accessibility', () => { expect(noInsertAttrSpy).not.toHaveBeenCalled(); }); - // ---------------- - // Convenience functions to DRY up the spec + // convenience functions to DRY up the spec - function expectAriaLabelledByIsTitleId( + function expectAriaLabelledByItTitleId( svgD3Node: D3Element, - title: string | null | undefined, - desc: string | null | undefined, + title: string | undefined, + desc: string | undefined, givenId: string - ) { + ): void { const svgAttrSpy = vi.spyOn(svgD3Node, 'attr').mockReturnValue(svgD3Node); addSVGa11yTitleDescription(svgD3Node, title, desc, givenId); expect(svgAttrSpy).toHaveBeenCalledWith('aria-labelledby', `chart-title-${givenId}`); } - function expectAriaDescribedByIsDescId( + function expectAriaDescribedByItDescId( svgD3Node: D3Element, - title: string | null | undefined, - desc: string | null | undefined, + title: string | undefined, + desc: string | undefined, givenId: string - ) { + ): void { const svgAttrSpy = vi.spyOn(svgD3Node, 'attr').mockReturnValue(svgD3Node); addSVGa11yTitleDescription(svgD3Node, title, desc, givenId); expect(svgAttrSpy).toHaveBeenCalledWith('aria-describedby', `chart-desc-${givenId}`); @@ -69,154 +65,148 @@ describe('accessibility', () => { function a11yTitleTagInserted( svgD3Node: D3Element, - title: string | null | undefined, - desc: string | null | undefined, + title: string | undefined, + desc: string | undefined, givenId: string, callNumber: number - ) { + ): void { a11yTagInserted(svgD3Node, title, desc, givenId, callNumber, 'title', title); } function a11yDescTagInserted( svgD3Node: D3Element, - title: string | null | undefined, - desc: string | null | undefined, + title: string | undefined, + desc: string | undefined, givenId: string, callNumber: number - ) { + ): void { a11yTagInserted(svgD3Node, title, desc, givenId, callNumber, 'desc', desc); } function a11yTagInserted( - svgD3Node: D3Element, - title: string | null | undefined, - desc: string | null | undefined, + _svgD3Node: D3Element, + title: string | undefined, + desc: string | undefined, givenId: string, callNumber: number, expectedPrefix: string, - expectedText: string | null | undefined - ) { - const fauxInsertedD3 = new MockedD3(); - const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxInsertedD3); - // @ts-ignore Required to easily handle the d3 select types + expectedText: string | undefined + ): void { + const fauxInsertedD3: MockedD3 = new MockedD3(); + const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxInsertedD3); const titleAttrSpy = vi.spyOn(fauxInsertedD3, 'attr').mockReturnValue(fauxInsertedD3); const titleTextSpy = vi.spyOn(fauxInsertedD3, 'text'); addSVGa11yTitleDescription(fauxSvgNode, title, desc, givenId); - expect(svgInsertSpy).toHaveBeenCalledWith(expectedPrefix, ':first-child'); + expect(svginsertpy).toHaveBeenCalledWith(expectedPrefix, ':first-child'); expect(titleAttrSpy).toHaveBeenCalledWith('id', `chart-${expectedPrefix}-${givenId}`); expect(titleTextSpy).toHaveBeenNthCalledWith(callNumber, expectedText); } - // ---------------- - describe('given an a11y title', () => { + describe('with a11y title', () => { const a11yTitle = 'a11y title'; - describe('given an a11y description', () => { + describe('with a11y description', () => { const a11yDesc = 'a11y description'; - it('sets aria-labelledby to the title id inserted as a child', () => { - expectAriaLabelledByIsTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId); + it('shold set aria-labelledby to the title id inserted as a child', () => { + expectAriaLabelledByItTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId); }); - it('sets aria-describedby to the description id inserted as a child', () => { - expectAriaDescribedByIsDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId); + it('should set aria-describedby to the description id inserted as a child', () => { + expectAriaDescribedByItDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId); }); - it('inserts a title tag as the first child with the text set to the accTitle given', () => { + it('should insert title tag as the first child with the text set to the accTitle given', () => { a11yTitleTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 2); }); - it('inserts a desc tag as the 2nd child with the text set to accDescription given', () => { + it('should insert desc tag as the 2nd child with the text set to accDescription given', () => { a11yDescTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1); }); }); - describe(`no a11y description`, () => { + describe(`without a11y description`, () => { const a11yDesc = undefined; - it('sets aria-labelledby to the title id inserted as a child', () => { - expectAriaLabelledByIsTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId); + it('should set aria-labelledby to the title id inserted as a child', () => { + expectAriaLabelledByItTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId); }); - it('no aria-describedby is set', () => { - // @ts-ignore Required to easily handle the d3 select types + it('should not set aria-describedby', () => { const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-describedby', expect.anything()); }); - it('inserts a title tag as the first child with the text set to the accTitle given', () => { + it('should insert title tag as the first child with the text set to the accTitle given', () => { a11yTitleTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1); }); - it('no description tag is inserted', () => { - const fauxTitle = new MockedD3(); - const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle); + it('should not insert description tag', () => { + const fauxTitle: MockedD3 = new MockedD3(); + const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle); addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); - expect(svgInsertSpy).not.toHaveBeenCalledWith('desc', ':first-child'); + expect(svginsertpy).not.toHaveBeenCalledWith('desc', ':first-child'); }); }); }); - describe('no a11y title', () => { + describe('without a11y title', () => { const a11yTitle = undefined; - describe('given an a11y description', () => { + describe('with a11y description', () => { const a11yDesc = 'a11y description'; - it('no aria-labelledby is set', () => { - // @ts-ignore Required to easily handle the d3 select types + it('should not set aria-labelledby', () => { const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-labelledby', expect.anything()); }); - it('no title tag inserted', () => { - const fauxTitle = new MockedD3(); - const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle); + it('should not insert title tag', () => { + const fauxTitle: MockedD3 = new MockedD3(); + const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle); addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); - expect(svgInsertSpy).not.toHaveBeenCalledWith('title', ':first-child'); + expect(svginsertpy).not.toHaveBeenCalledWith('title', ':first-child'); }); - it('sets aria-describedby to the description id inserted as a child', () => { - expectAriaDescribedByIsDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId); + it('should set aria-describedby to the description id inserted as a child', () => { + expectAriaDescribedByItDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId); }); - it('inserts a desc tag as the 2nd child with the text set to accDescription given', () => { + it('should insert desc tag as the 2nd child with the text set to accDescription given', () => { a11yDescTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1); }); }); - describe('no a11y description', () => { + describe('without a11y description', () => { const a11yDesc = undefined; - it('no aria-labelledby is set', () => { - // @ts-ignore Required to easily handle the d3 select types + it('should not set aria-labelledby', () => { const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-labelledby', expect.anything()); }); - it('no aria-describedby is set', () => { - // @ts-ignore Required to easily handle the d3 select types + it('should not set aria-describedby', () => { const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-describedby', expect.anything()); }); - it('no title tag inserted', () => { - const fauxTitle = new MockedD3(); - const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle); + it('should not insert title tag', () => { + const fauxTitle: MockedD3 = new MockedD3(); + const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle); addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); - expect(svgInsertSpy).not.toHaveBeenCalledWith('title', ':first-child'); + expect(svginsertpy).not.toHaveBeenCalledWith('title', ':first-child'); }); - it('no description tag inserted', () => { - const fauxDesc = new MockedD3(); - const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxDesc); + it('should not insert description tag', () => { + const fauxDesc: MockedD3 = new MockedD3(); + const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxDesc); addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); - expect(svgInsertSpy).not.toHaveBeenCalledWith('desc', ':first-child'); + expect(svginsertpy).not.toHaveBeenCalledWith('desc', ':first-child'); }); }); }); diff --git a/packages/mermaid/src/accessibility.ts b/packages/mermaid/src/accessibility.ts index eba3ba9a75..69e22b3011 100644 --- a/packages/mermaid/src/accessibility.ts +++ b/packages/mermaid/src/accessibility.ts @@ -1,13 +1,11 @@ /** - * Accessibility (a11y) functions, types, helpers + * Accessibility (a11y) functions, types, helpers. + * * @see https://www.w3.org/WAI/ * @see https://www.w3.org/TR/wai-aria-1.1/ * @see https://www.w3.org/TR/svg-aam-1.0/ - * */ -import { D3Element } from './mermaidAPI.js'; - -import isEmpty from 'lodash-es/isEmpty.js'; +import type { D3Element } from './mermaidAPI.js'; /** * SVG element role: @@ -21,50 +19,47 @@ import isEmpty from 'lodash-es/isEmpty.js'; const SVG_ROLE = 'graphics-document document'; /** - * Add role and aria-roledescription to the svg element + * Add role and aria-roledescription to the svg element. * * @param svg - d3 object that contains the SVG HTML element * @param diagramType - diagram name for to the aria-roledescription */ -export function setA11yDiagramInfo(svg: D3Element, diagramType: string | null | undefined) { +export function setA11yDiagramInfo(svg: D3Element, diagramType: string) { svg.attr('role', SVG_ROLE); - if (!isEmpty(diagramType)) { + if (diagramType !== '') { svg.attr('aria-roledescription', diagramType); } } + /** * Add an accessible title and/or description element to a chart. * The title is usually not displayed and the description is never displayed. * - * The following charts display their title as a visual and accessibility element: gantt + * The following charts display their title as a visual and accessibility element: gantt. * * @param svg - d3 node to insert the a11y title and desc info - * @param a11yTitle - a11y title. null and undefined are meaningful: means to skip it - * @param a11yDesc - a11y description. null and undefined are meaningful: means to skip it + * @param a11yTitle - a11y title. undefined or empty strings mean to skip them + * @param a11yDesc - a11y description. undefined or empty strings mean to skip them * @param baseId - id used to construct the a11y title and description id */ export function addSVGa11yTitleDescription( svg: D3Element, - a11yTitle: string | null | undefined, - a11yDesc: string | null | undefined, + a11yTitle: string | undefined, + a11yDesc: string | undefined, baseId: string -) { +): void { if (svg.insert === undefined) { return; } - if (a11yTitle || a11yDesc) { - if (a11yDesc) { - const descId = 'chart-desc-' + baseId; - svg.attr('aria-describedby', descId); - svg.insert('desc', ':first-child').attr('id', descId).text(a11yDesc); - } - if (a11yTitle) { - const titleId = 'chart-title-' + baseId; - svg.attr('aria-labelledby', titleId); - svg.insert('title', ':first-child').attr('id', titleId).text(a11yTitle); - } - } else { - return; + if (a11yDesc) { + const descId = `chart-desc-${baseId}`; + svg.attr('aria-describedby', descId); + svg.insert('desc', ':first-child').attr('id', descId).text(a11yDesc); + } + if (a11yTitle) { + const titleId = `chart-title-${baseId}`; + svg.attr('aria-labelledby', titleId); + svg.insert('title', ':first-child').attr('id', titleId).text(a11yTitle); } } diff --git a/packages/mermaid/src/assignWithDepth.js b/packages/mermaid/src/assignWithDepth.ts similarity index 65% rename from packages/mermaid/src/assignWithDepth.js rename to packages/mermaid/src/assignWithDepth.ts index 898481c30a..125b6f434a 100644 --- a/packages/mermaid/src/assignWithDepth.js +++ b/packages/mermaid/src/assignWithDepth.ts @@ -1,32 +1,36 @@ -'use strict'; +/* eslint-disable @typescript-eslint/no-explicit-any */ + /** - * @function assignWithDepth Extends the functionality of {@link ObjectConstructor.assign} with the + * assignWithDepth Extends the functionality of {@link Object.assign} with the * ability to merge arbitrary-depth objects For each key in src with path `k` (recursively) * performs an Object.assign(dst[`k`], src[`k`]) with a slight change from the typical handling of - * undefined for dst[`k`]: instead of raising an error, dst[`k`] is auto-initialized to {} and + * undefined for dst[`k`]: instead of raising an error, dst[`k`] is auto-initialized to `{}` and * effectively merged with src[`k`]

Additionally, dissimilar types will not clobber unless the * config.clobber parameter === true. Example: * - * ```js - * let config_0 = { foo: { bar: 'bar' }, bar: 'foo' }; - * let config_1 = { foo: 'foo', bar: 'bar' }; - * let result = assignWithDepth(config_0, config_1); - * console.log(result); - * //-> result: { foo: { bar: 'bar' }, bar: 'bar' } - * ``` + * ``` + * const config_0 = { foo: { bar: 'bar' }, bar: 'foo' }; + * const config_1 = { foo: 'foo', bar: 'bar' }; + * const result = assignWithDepth(config_0, config_1); + * console.log(result); + * //-> result: { foo: { bar: 'bar' }, bar: 'bar' } + * ``` * * Traditional Object.assign would have clobbered foo in config_0 with foo in config_1. If src is a * destructured array of objects and dst is not an array, assignWithDepth will apply each element * of src to dst in order. - * @param {any} dst - The destination of the merge - * @param {any} src - The source object(s) to merge into destination - * @param {{ depth: number; clobber: boolean }} [config={ depth: 2, clobber: false }] - Depth: depth - * to traverse within src and dst for merging - clobber: should dissimilar types clobber (default: - * { depth: 2, clobber: false }). Default is `{ depth: 2, clobber: false }` - * @returns {any} + * @param dst - The destination of the merge + * @param src - The source object(s) to merge into destination + * @param config - + * * depth: depth to traverse within src and dst for merging + * * clobber: should dissimilar types clobber */ -const assignWithDepth = function (dst, src, config) { - const { depth, clobber } = Object.assign({ depth: 2, clobber: false }, config); +const assignWithDepth = ( + dst: any, + src: any, + { depth = 2, clobber = false }: { depth?: number; clobber?: boolean } = {} +): any => { + const config: { depth: number; clobber: boolean } = { depth, clobber }; if (Array.isArray(src) && !Array.isArray(dst)) { src.forEach((s) => assignWithDepth(dst, s, config)); return dst; diff --git a/packages/mermaid/src/commonDb.ts b/packages/mermaid/src/commonDb.ts deleted file mode 100644 index 4e1e5141a6..0000000000 --- a/packages/mermaid/src/commonDb.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { sanitizeText as _sanitizeText } from './diagrams/common/common.js'; -import { getConfig } from './config.js'; -let title = ''; -let diagramTitle = ''; -let description = ''; - -const sanitizeText = (txt: string): string => _sanitizeText(txt, getConfig()); - -export const clear = function (): void { - title = ''; - description = ''; - diagramTitle = ''; -}; - -export const setAccTitle = function (txt: string): void { - title = sanitizeText(txt).replace(/^\s+/g, ''); -}; - -export const getAccTitle = function (): string { - return title || diagramTitle; -}; - -export const setAccDescription = function (txt: string): void { - description = sanitizeText(txt).replace(/\n\s+/g, '\n'); -}; - -export const getAccDescription = function (): string { - return description; -}; - -export const setDiagramTitle = function (txt: string): void { - diagramTitle = sanitizeText(txt); -}; - -export const getDiagramTitle = function (): string { - return diagramTitle; -}; - -export default { - getAccTitle, - setAccTitle, - getDiagramTitle, - setDiagramTitle, - getAccDescription, - setAccDescription, - clear, -}; diff --git a/packages/mermaid/src/config.spec.js b/packages/mermaid/src/config.spec.js deleted file mode 100644 index 1f7fd976b3..0000000000 --- a/packages/mermaid/src/config.spec.js +++ /dev/null @@ -1,56 +0,0 @@ -import * as configApi from './config.js'; - -describe('when working with site config', function () { - beforeEach(() => { - // Resets the site config to default config - configApi.setSiteConfig({}); - }); - it('should set site config and config properly', function () { - let config_0 = { foo: 'bar', bar: 0 }; - configApi.setSiteConfig(config_0); - let config_1 = configApi.getSiteConfig(); - let config_2 = configApi.getConfig(); - expect(config_1.foo).toEqual(config_0.foo); - expect(config_1.bar).toEqual(config_0.bar); - expect(config_1).toEqual(config_2); - }); - it('should respect secure keys when applying directives', function () { - let config_0 = { - foo: 'bar', - bar: 'cant-be-changed', - secure: [...configApi.defaultConfig.secure, 'bar'], - }; - configApi.setSiteConfig(config_0); - const directive = { foo: 'baf', bar: 'should-not-be-allowed' }; - const cfg = configApi.updateCurrentConfig(config_0, [directive]); - expect(cfg.foo).toEqual(directive.foo); - expect(cfg.bar).toBe(config_0.bar); - }); - it('should set reset config properly', function () { - let config_0 = { foo: 'bar', bar: 0 }; - configApi.setSiteConfig(config_0); - let config_1 = { foo: 'baf' }; - configApi.setConfig(config_1); - let config_2 = configApi.getConfig(); - expect(config_2.foo).toEqual(config_1.foo); - configApi.reset(); - let config_3 = configApi.getConfig(); - expect(config_3.foo).toEqual(config_0.foo); - let config_4 = configApi.getSiteConfig(); - expect(config_4.foo).toEqual(config_0.foo); - }); - it('should set global reset config properly', function () { - let config_0 = { foo: 'bar', bar: 0 }; - configApi.setSiteConfig(config_0); - let config_1 = configApi.getSiteConfig(); - expect(config_1.foo).toEqual(config_0.foo); - let config_2 = configApi.getConfig(); - expect(config_2.foo).toEqual(config_0.foo); - configApi.setConfig({ foobar: 'bar0' }); - let config_3 = configApi.getConfig(); - expect(config_3.foobar).toEqual('bar0'); - configApi.reset(); - let config_4 = configApi.getConfig(); - expect(config_4.foobar).toBeUndefined(); - }); -}); diff --git a/packages/mermaid/src/config.spec.ts b/packages/mermaid/src/config.spec.ts new file mode 100644 index 0000000000..8dd4ff33c0 --- /dev/null +++ b/packages/mermaid/src/config.spec.ts @@ -0,0 +1,81 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import * as configApi from './config.js'; +import type { MermaidConfig } from './config.type.js'; + +describe('when working with site config', () => { + beforeEach(() => { + // Resets the site config to default config + configApi.setSiteConfig({}); + }); + it('should set site config and config properly', () => { + const config_0 = { fontFamily: 'foo-font', fontSize: 150 }; + configApi.setSiteConfig(config_0); + const config_1 = configApi.getSiteConfig(); + const config_2 = configApi.getConfig(); + expect(config_1.fontFamily).toEqual(config_0.fontFamily); + expect(config_1.fontSize).toEqual(config_0.fontSize); + expect(config_1).toEqual(config_2); + }); + it('should respect secure keys when applying directives', () => { + const config_0: MermaidConfig = { + fontFamily: 'foo-font', + securityLevel: 'strict', // can't be changed + fontSize: 12345, // can't be changed + secure: [...configApi.defaultConfig.secure!, 'fontSize'], + }; + configApi.setSiteConfig(config_0); + const directive: MermaidConfig = { + fontFamily: 'baf', + // fontSize and securityLevel shouldn't be changed + fontSize: 54321, + securityLevel: 'loose', + }; + const cfg: MermaidConfig = configApi.updateCurrentConfig(config_0, [directive]); + expect(cfg.fontFamily).toEqual(directive.fontFamily); + expect(cfg.fontSize).toBe(config_0.fontSize); + expect(cfg.securityLevel).toBe(config_0.securityLevel); + }); + it('should allow setting partial options', () => { + const defaultConfig = configApi.getConfig(); + + configApi.setConfig({ + quadrantChart: { + chartHeight: 600, + }, + }); + + const updatedConfig = configApi.getConfig(); + + // deep options we didn't update should remain the same + expect(defaultConfig.quadrantChart!.chartWidth).toEqual( + updatedConfig.quadrantChart!.chartWidth + ); + }); + it('should set reset config properly', () => { + const config_0 = { fontFamily: 'foo-font', fontSize: 150 }; + configApi.setSiteConfig(config_0); + const config_1 = { fontFamily: 'baf' }; + configApi.setConfig(config_1); + const config_2 = configApi.getConfig(); + expect(config_2.fontFamily).toEqual(config_1.fontFamily); + configApi.reset(); + const config_3 = configApi.getConfig(); + expect(config_3.fontFamily).toEqual(config_0.fontFamily); + const config_4 = configApi.getSiteConfig(); + expect(config_4.fontFamily).toEqual(config_0.fontFamily); + }); + it('should set global reset config properly', () => { + const config_0 = { fontFamily: 'foo-font', fontSize: 150 }; + configApi.setSiteConfig(config_0); + const config_1 = configApi.getSiteConfig(); + expect(config_1.fontFamily).toEqual(config_0.fontFamily); + const config_2 = configApi.getConfig(); + expect(config_2.fontFamily).toEqual(config_0.fontFamily); + configApi.setConfig({ altFontFamily: 'bar-font' }); + const config_3 = configApi.getConfig(); + expect(config_3.altFontFamily).toEqual('bar-font'); + configApi.reset(); + const config_4 = configApi.getConfig(); + expect(config_4.altFontFamily).toBeUndefined(); + }); +}); diff --git a/packages/mermaid/src/config.ts b/packages/mermaid/src/config.ts index 838716e2fb..eb24b6268f 100644 --- a/packages/mermaid/src/config.ts +++ b/packages/mermaid/src/config.ts @@ -3,15 +3,16 @@ import { log } from './logger.js'; import theme from './themes/index.js'; import config from './defaultConfig.js'; import type { MermaidConfig } from './config.type.js'; +import { sanitizeDirective } from './utils.js'; export const defaultConfig: MermaidConfig = Object.freeze(config); let siteConfig: MermaidConfig = assignWithDepth({}, defaultConfig); let configFromInitialize: MermaidConfig; -let directives: any[] = []; +let directives: MermaidConfig[] = []; let currentConfig: MermaidConfig = assignWithDepth({}, defaultConfig); -export const updateCurrentConfig = (siteCfg: MermaidConfig, _directives: any[]) => { +export const updateCurrentConfig = (siteCfg: MermaidConfig, _directives: MermaidConfig[]) => { // start with config being the siteConfig let cfg: MermaidConfig = assignWithDepth({}, siteCfg); // let sCfg = assignWithDepth(defaultConfig, siteConfigDelta); @@ -20,7 +21,6 @@ export const updateCurrentConfig = (siteCfg: MermaidConfig, _directives: any[]) let sumOfDirectives: MermaidConfig = {}; for (const d of _directives) { sanitize(d); - // Apply the data from the directive where the the overrides the themeVariables sumOfDirectives = assignWithDepth(sumOfDirectives, d); } @@ -111,12 +111,6 @@ export const getSiteConfig = (): MermaidConfig => { * @returns The currentConfig merged with the sanitized conf */ export const setConfig = (conf: MermaidConfig): MermaidConfig => { - // sanitize(conf); - // Object.keys(conf).forEach(key => { - // const manipulator = manipulators[key]; - // conf[key] = manipulator ? manipulator(conf[key]) : conf[key]; - // }); - checkConfig(conf); assignWithDepth(currentConfig, conf); @@ -150,9 +144,12 @@ export const getConfig = (): MermaidConfig => { * @param options - The potential setConfig parameter */ export const sanitize = (options: any) => { + if (!options) { + return; + } // Checking that options are not in the list of excluded options ['secure', ...(siteConfig.secure ?? [])].forEach((key) => { - if (options[key] !== undefined) { + if (Object.hasOwn(options, key)) { // DO NOT attempt to print options[key] within `${}` as a malicious script // can exploit the logger's attempt to stringify the value and execute arbitrary code log.debug(`Denied attempt to modify a secure key ${key}`, options[key]); @@ -162,7 +159,7 @@ export const sanitize = (options: any) => { // Check that there no attempts of prototype pollution Object.keys(options).forEach((key) => { - if (key.indexOf('__') === 0) { + if (key.startsWith('__')) { delete options[key]; } }); @@ -188,16 +185,14 @@ export const sanitize = (options: any) => { * * @param directive - The directive to push in */ -export const addDirective = (directive: any) => { - if (directive.fontFamily) { - if (!directive.themeVariables) { - directive.themeVariables = { fontFamily: directive.fontFamily }; - } else { - if (!directive.themeVariables.fontFamily) { - directive.themeVariables = { fontFamily: directive.fontFamily }; - } - } +export const addDirective = (directive: MermaidConfig) => { + sanitizeDirective(directive); + + // If the directive has a fontFamily, but no themeVariables, add the fontFamily to the themeVariables + if (directive.fontFamily && (!directive.themeVariables || !directive.themeVariables.fontFamily)) { + directive.themeVariables = { fontFamily: directive.fontFamily }; } + directives.push(directive); updateCurrentConfig(siteConfig, directives); }; @@ -226,9 +221,11 @@ export const reset = (config = siteConfig): void => { updateCurrentConfig(config, directives); }; -enum ConfigWarning { - 'LAZY_LOAD_DEPRECATED' = 'The configuration options lazyLoadedDiagrams and loadExternalDiagramsAtStartup are deprecated. Please use registerExternalDiagrams instead.', -} +const ConfigWarning = { + LAZY_LOAD_DEPRECATED: + 'The configuration options lazyLoadedDiagrams and loadExternalDiagramsAtStartup are deprecated. Please use registerExternalDiagrams instead.', +} as const; + type ConfigWarningStrings = keyof typeof ConfigWarning; const issuedWarnings: { [key in ConfigWarningStrings]?: boolean } = {}; const issueWarning = (warning: ConfigWarningStrings) => { diff --git a/packages/mermaid/src/config.type.ts b/packages/mermaid/src/config.type.ts index e9d21ba81b..9ae4079b98 100644 --- a/packages/mermaid/src/config.type.ts +++ b/packages/mermaid/src/config.type.ts @@ -1,22 +1,144 @@ -// TODO: This was auto generated from defaultConfig. Needs to be verified. +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ -import DOMPurify from 'dompurify'; +/** + * Configuration options to pass to the `dompurify` library. + */ +export type DOMPurifyConfiguration = import('dompurify').Config; +/** + * JavaScript function that returns a `FontConfig`. + * + * By default, these return the appropriate `*FontSize`, `*FontFamily`, `*FontWeight` + * values. + * + * For example, the font calculator called `boundaryFont` might be defined as: + * + * ```javascript + * boundaryFont: function () { + * return { + * fontFamily: this.boundaryFontFamily, + * fontSize: this.boundaryFontSize, + * fontWeight: this.boundaryFontWeight, + * }; + * } + * ``` + * + * + * This interface was referenced by `MermaidConfig`'s JSON-Schema + * via the `definition` "FontCalculator". + */ +export type FontCalculator = () => Partial; +/** + * Picks the color of the sankey diagram links, using the colors of the source and/or target of the links. + * + * + * This interface was referenced by `MermaidConfig`'s JSON-Schema + * via the `definition` "SankeyLinkColor". + */ +export type SankeyLinkColor = 'source' | 'target' | 'gradient'; +/** + * Controls the alignment of the Sankey diagrams. + * + * See . + * + * + * This interface was referenced by `MermaidConfig`'s JSON-Schema + * via the `definition` "SankeyNodeAlignment". + */ +export type SankeyNodeAlignment = 'left' | 'right' | 'center' | 'justify'; +/** + * The font size to use + */ +export type CSSFontSize = string | number; export interface MermaidConfig { - theme?: string; + /** + * Theme, the CSS style sheet. + * You may also use `themeCSS` to override this value. + * + */ + theme?: string | 'default' | 'forest' | 'dark' | 'neutral' | 'null'; themeVariables?: any; themeCSS?: string; + /** + * The maximum allowed size of the users text diagram + */ maxTextSize?: number; darkMode?: boolean; htmlLabels?: boolean; + /** + * Specifies the font to be used in the rendered diagrams. + * Can be any possible CSS `font-family`. + * See https://developer.mozilla.org/en-US/docs/Web/CSS/font-family + * + */ fontFamily?: string; altFontFamily?: string; - logLevel?: number; - securityLevel?: string; + /** + * This option decides the amount of logging to be used by mermaid. + * + */ + logLevel?: + | number + | string + | 0 + | 2 + | 1 + | 'trace' + | 'debug' + | 'info' + | 'warn' + | 'error' + | 'fatal' + | 3 + | 4 + | 5 + | undefined; + /** + * Level of trust for parsed diagram + */ + securityLevel?: string | 'strict' | 'loose' | 'antiscript' | 'sandbox' | undefined; + /** + * Dictates whether mermaid starts on Page load + */ startOnLoad?: boolean; + /** + * Controls whether or arrow markers in html code are absolute paths or anchors. + * This matters if you are using base tag settings. + * + */ arrowMarkerAbsolute?: boolean; + /** + * This option controls which `currentConfig` keys are considered secure and + * can only be changed via call to `mermaidAPI.initialize`. + * Calls to `mermaidAPI.reinitialize` cannot make changes to the secure keys + * in the current `currentConfig`. + * + * This prevents malicious graph directives from overriding a site's default security. + * + */ secure?: string[]; + /** + * This option controls if the generated ids of nodes in the SVG are + * generated randomly or based on a seed. + * If set to `false`, the IDs are generated based on the current date and + * thus are not deterministic. This is the default behavior. + * + * This matters if your files are checked into source control e.g. git and + * should not change unless content is changed. + * + */ deterministicIds?: boolean; + /** + * This option is the optional seed for deterministic ids. + * If set to `undefined` but deterministicIds is `true`, a simple number iterator is used. + * You can set this attribute to base the seed on a static string. + * + */ deterministicIDSeed?: string; flowchart?: FlowchartDiagramConfig; sequence?: SequenceDiagramConfig; @@ -32,96 +154,343 @@ export interface MermaidConfig { mindmap?: MindmapDiagramConfig; gitGraph?: GitGraphDiagramConfig; c4?: C4DiagramConfig; - dompurifyConfig?: DOMPurify.Config; + sankey?: SankeyDiagramConfig; + dompurifyConfig?: DOMPurifyConfiguration; wrap?: boolean; fontSize?: number; suppressErrorRendering?: boolean; } - -// TODO: More configs needs to be moved in here +/** + * This interface was referenced by `MermaidConfig`'s JSON-Schema + * via the `definition` "BaseDiagramConfig". + */ export interface BaseDiagramConfig { useWidth?: number; + /** + * When this flag is set to `true`, the height and width is set to 100% + * and is then scaled with the available space. + * If set to `false`, the absolute space required is used. + * + */ useMaxWidth?: boolean; } - +/** + * The object containing configurations specific for c4 diagrams + * + * This interface was referenced by `MermaidConfig`'s JSON-Schema + * via the `definition` "C4DiagramConfig". + */ export interface C4DiagramConfig extends BaseDiagramConfig { + /** + * Margin to the right and left of the c4 diagram, must be a positive value. + * + */ diagramMarginX?: number; + /** + * Margin to the over and under the c4 diagram, must be a positive value. + * + */ diagramMarginY?: number; + /** + * Margin between shapes + */ c4ShapeMargin?: number; + /** + * Padding between shapes + */ c4ShapePadding?: number; + /** + * Width of person boxes + */ width?: number; + /** + * Height of person boxes + */ height?: number; + /** + * Margin around boxes + */ boxMargin?: number; + /** + * How many shapes to place in each row. + */ c4ShapeInRow?: number; nextLinePaddingX?: number; + /** + * How many boundaries to place in each row. + */ c4BoundaryInRow?: number; + /** + * This sets the font size of Person shape for the diagram + */ personFontSize?: string | number; + /** + * This sets the font weight of Person shape for the diagram + */ personFontFamily?: string; + /** + * This sets the font weight of Person shape for the diagram + */ personFontWeight?: string | number; + /** + * This sets the font size of External Person shape for the diagram + */ external_personFontSize?: string | number; + /** + * This sets the font family of External Person shape for the diagram + */ external_personFontFamily?: string; + /** + * This sets the font weight of External Person shape for the diagram + */ external_personFontWeight?: string | number; + /** + * This sets the font size of System shape for the diagram + */ systemFontSize?: string | number; + /** + * This sets the font family of System shape for the diagram + */ systemFontFamily?: string; + /** + * This sets the font weight of System shape for the diagram + */ systemFontWeight?: string | number; + /** + * This sets the font size of External System shape for the diagram + */ external_systemFontSize?: string | number; + /** + * This sets the font family of External System shape for the diagram + */ external_systemFontFamily?: string; + /** + * This sets the font weight of External System shape for the diagram + */ external_systemFontWeight?: string | number; + /** + * This sets the font size of System DB shape for the diagram + */ system_dbFontSize?: string | number; + /** + * This sets the font family of System DB shape for the diagram + */ system_dbFontFamily?: string; + /** + * This sets the font weight of System DB shape for the diagram + */ system_dbFontWeight?: string | number; + /** + * This sets the font size of External System DB shape for the diagram + */ external_system_dbFontSize?: string | number; + /** + * This sets the font family of External System DB shape for the diagram + */ external_system_dbFontFamily?: string; + /** + * This sets the font weight of External System DB shape for the diagram + */ external_system_dbFontWeight?: string | number; + /** + * This sets the font size of System Queue shape for the diagram + */ system_queueFontSize?: string | number; + /** + * This sets the font family of System Queue shape for the diagram + */ system_queueFontFamily?: string; + /** + * This sets the font weight of System Queue shape for the diagram + */ system_queueFontWeight?: string | number; + /** + * This sets the font size of External System Queue shape for the diagram + */ external_system_queueFontSize?: string | number; + /** + * This sets the font family of External System Queue shape for the diagram + */ external_system_queueFontFamily?: string; + /** + * This sets the font weight of External System Queue shape for the diagram + */ external_system_queueFontWeight?: string | number; + /** + * This sets the font size of Boundary shape for the diagram + */ boundaryFontSize?: string | number; + /** + * This sets the font family of Boundary shape for the diagram + */ boundaryFontFamily?: string; + /** + * This sets the font weight of Boundary shape for the diagram + */ boundaryFontWeight?: string | number; + /** + * This sets the font size of Message shape for the diagram + */ messageFontSize?: string | number; + /** + * This sets the font family of Message shape for the diagram + */ messageFontFamily?: string; + /** + * This sets the font weight of Message shape for the diagram + */ messageFontWeight?: string | number; + /** + * This sets the font size of Container shape for the diagram + */ containerFontSize?: string | number; + /** + * This sets the font family of Container shape for the diagram + */ containerFontFamily?: string; + /** + * This sets the font weight of Container shape for the diagram + */ containerFontWeight?: string | number; + /** + * This sets the font size of External Container shape for the diagram + */ external_containerFontSize?: string | number; + /** + * This sets the font family of External Container shape for the diagram + */ external_containerFontFamily?: string; + /** + * This sets the font weight of External Container shape for the diagram + */ external_containerFontWeight?: string | number; + /** + * This sets the font size of Container DB shape for the diagram + */ container_dbFontSize?: string | number; + /** + * This sets the font family of Container DB shape for the diagram + */ container_dbFontFamily?: string; + /** + * This sets the font weight of Container DB shape for the diagram + */ container_dbFontWeight?: string | number; + /** + * This sets the font size of External Container DB shape for the diagram + */ external_container_dbFontSize?: string | number; + /** + * This sets the font family of External Container DB shape for the diagram + */ external_container_dbFontFamily?: string; + /** + * This sets the font weight of External Container DB shape for the diagram + */ external_container_dbFontWeight?: string | number; + /** + * This sets the font size of Container Queue shape for the diagram + */ container_queueFontSize?: string | number; + /** + * This sets the font family of Container Queue shape for the diagram + */ container_queueFontFamily?: string; + /** + * This sets the font weight of Container Queue shape for the diagram + */ container_queueFontWeight?: string | number; + /** + * This sets the font size of External Container Queue shape for the diagram + */ external_container_queueFontSize?: string | number; + /** + * This sets the font family of External Container Queue shape for the diagram + */ external_container_queueFontFamily?: string; + /** + * This sets the font weight of External Container Queue shape for the diagram + */ external_container_queueFontWeight?: string | number; + /** + * This sets the font size of Component shape for the diagram + */ componentFontSize?: string | number; + /** + * This sets the font family of Component shape for the diagram + */ componentFontFamily?: string; + /** + * This sets the font weight of Component shape for the diagram + */ componentFontWeight?: string | number; + /** + * This sets the font size of External Component shape for the diagram + */ external_componentFontSize?: string | number; + /** + * This sets the font family of External Component shape for the diagram + */ external_componentFontFamily?: string; + /** + * This sets the font weight of External Component shape for the diagram + */ external_componentFontWeight?: string | number; + /** + * This sets the font size of Component DB shape for the diagram + */ component_dbFontSize?: string | number; + /** + * This sets the font family of Component DB shape for the diagram + */ component_dbFontFamily?: string; + /** + * This sets the font weight of Component DB shape for the diagram + */ component_dbFontWeight?: string | number; + /** + * This sets the font size of External Component DB shape for the diagram + */ external_component_dbFontSize?: string | number; + /** + * This sets the font family of External Component DB shape for the diagram + */ external_component_dbFontFamily?: string; + /** + * This sets the font weight of External Component DB shape for the diagram + */ external_component_dbFontWeight?: string | number; + /** + * This sets the font size of Component Queue shape for the diagram + */ component_queueFontSize?: string | number; + /** + * This sets the font family of Component Queue shape for the diagram + */ component_queueFontFamily?: string; + /** + * This sets the font weight of Component Queue shape for the diagram + */ component_queueFontWeight?: string | number; + /** + * This sets the font size of External Component Queue shape for the diagram + */ external_component_queueFontSize?: string | number; + /** + * This sets the font family of External Component Queue shape for the diagram + */ external_component_queueFontFamily?: string; + /** + * This sets the font weight of External Component Queue shape for the diagram + */ external_component_queueFontWeight?: string | number; + /** + * This sets the auto-wrap state for the diagram + */ wrap?: boolean; + /** + * This sets the auto-wrap padding for the diagram (sides only) + */ wrapPadding?: number; person_bg_color?: string; person_border_color?: string; @@ -186,8 +555,14 @@ export interface C4DiagramConfig extends BaseDiagramConfig { boundaryFont?: FontCalculator; messageFont?: FontCalculator; } - +/** + * This interface was referenced by `MermaidConfig`'s JSON-Schema + * via the `definition` "GitGraphDiagramConfig". + */ export interface GitGraphDiagramConfig extends BaseDiagramConfig { + /** + * Margin top for the text over the diagram + */ titleTopMargin?: number; diagramPadding?: number; nodeLabel?: NodeLabel; @@ -196,16 +571,29 @@ export interface GitGraphDiagramConfig extends BaseDiagramConfig { showCommitLabel?: boolean; showBranches?: boolean; rotateCommitLabel?: boolean; + /** + * Controls whether or arrow markers in html code are absolute paths or anchors. + * This matters if you are using base tag settings. + * + */ arrowMarkerAbsolute?: boolean; } - +/** + * This interface was referenced by `MermaidConfig`'s JSON-Schema + * via the `definition` "NodeLabel". + */ export interface NodeLabel { width?: number; height?: number; x?: number; y?: number; } - +/** + * The object containing configurations specific for req diagrams + * + * This interface was referenced by `MermaidConfig`'s JSON-Schema + * via the `definition` "RequirementDiagramConfig". + */ export interface RequirementDiagramConfig extends BaseDiagramConfig { rect_fill?: string; text_color?: string; @@ -217,51 +605,163 @@ export interface RequirementDiagramConfig extends BaseDiagramConfig { rect_padding?: number; line_height?: number; } - +/** + * The object containing configurations specific for mindmap diagrams + * + * This interface was referenced by `MermaidConfig`'s JSON-Schema + * via the `definition` "MindmapDiagramConfig". + */ export interface MindmapDiagramConfig extends BaseDiagramConfig { - useMaxWidth: boolean; - padding: number; - maxNodeWidth: number; + padding?: number; + maxNodeWidth?: number; } - +/** + * This interface was referenced by `MermaidConfig`'s JSON-Schema + * via the `definition` "PieDiagramConfig". + */ export interface PieDiagramConfig extends BaseDiagramConfig { + /** + * Axial position of slice's label from zero at the center to 1 at the outside edges. + * + */ textPosition?: number; } - +/** + * This interface was referenced by `MermaidConfig`'s JSON-Schema + * via the `definition` "QuadrantChartConfig". + */ export interface QuadrantChartConfig extends BaseDiagramConfig { - chartWidth: number; - chartHeight: number; - titleFontSize: number; - titlePadding: number; - quadrantPadding: number; - xAxisLabelPadding: number; - yAxisLabelPadding: number; - xAxisLabelFontSize: number; - yAxisLabelFontSize: number; - quadrantLabelFontSize: number; - quadrantTextTopPadding: number; - pointTextPadding: number; - pointLabelFontSize: number; - pointRadius: number; - xAxisPosition: 'top' | 'bottom'; - yAxisPosition: 'left' | 'right'; - quadrantInternalBorderStrokeWidth: number; - quadrantExternalBorderStrokeWidth: number; + /** + * Width of the chart + */ + chartWidth?: number; + /** + * Height of the chart + */ + chartHeight?: number; + /** + * Chart title top and bottom padding + */ + titleFontSize?: number; + /** + * Padding around the quadrant square + */ + titlePadding?: number; + /** + * quadrant title padding from top if the quadrant is rendered on top + */ + quadrantPadding?: number; + /** + * Padding around x-axis labels + */ + xAxisLabelPadding?: number; + /** + * Padding around y-axis labels + */ + yAxisLabelPadding?: number; + /** + * x-axis label font size + */ + xAxisLabelFontSize?: number; + /** + * y-axis label font size + */ + yAxisLabelFontSize?: number; + /** + * quadrant title font size + */ + quadrantLabelFontSize?: number; + /** + * quadrant title padding from top if the quadrant is rendered on top + */ + quadrantTextTopPadding?: number; + /** + * padding between point and point label + */ + pointTextPadding?: number; + /** + * point title font size + */ + pointLabelFontSize?: number; + /** + * radius of the point to be drawn + */ + pointRadius?: number; + /** + * position of x-axis labels + */ + xAxisPosition?: 'top' | 'bottom'; + /** + * position of y-axis labels + */ + yAxisPosition?: 'left' | 'right'; + /** + * stroke width of edges of the box that are inside the quadrant + */ + quadrantInternalBorderStrokeWidth?: number; + /** + * stroke width of edges of the box that are outside the quadrant + */ + quadrantExternalBorderStrokeWidth?: number; } - +/** + * The object containing configurations specific for entity relationship diagrams + * + * This interface was referenced by `MermaidConfig`'s JSON-Schema + * via the `definition` "ErDiagramConfig". + */ export interface ErDiagramConfig extends BaseDiagramConfig { + /** + * Margin top for the text over the diagram + */ titleTopMargin?: number; + /** + * The amount of padding around the diagram as a whole so that embedded + * diagrams have margins, expressed in pixels. + * + */ diagramPadding?: number; - layoutDirection?: string; + /** + * Directional bias for layout of entities + */ + layoutDirection?: string | 'TB' | 'BT' | 'LR' | 'RL'; + /** + * The minimum width of an entity box. Expressed in pixels. + */ minEntityWidth?: number; + /** + * The minimum height of an entity box. Expressed in pixels. + */ minEntityHeight?: number; + /** + * The minimum internal padding between text in an entity box and the enclosing box borders. + * Expressed in pixels. + * + */ entityPadding?: number; + /** + * Stroke color of box edges and lines. + */ stroke?: string; + /** + * Fill color of entity boxes + */ fill?: string; + /** + * Font size (expressed as an integer representing a number of pixels) + */ fontSize?: number; } - +/** + * The object containing configurations specific for entity relationship diagrams + * + * This interface was referenced by `MermaidConfig`'s JSON-Schema + * via the `definition` "StateDiagramConfig". + */ export interface StateDiagramConfig extends BaseDiagramConfig { + /** + * Margin top for the text over the diagram + */ titleTopMargin?: number; arrowMarkerAbsolute?: boolean; dividerMargin?: number; @@ -273,151 +773,560 @@ export interface StateDiagramConfig extends BaseDiagramConfig { forkWidth?: number; forkHeight?: number; miniPadding?: number; + /** + * Font size factor, this is used to guess the width of the edges labels + * before rendering by dagre layout. + * This might need updating if/when switching font + * + */ fontSizeFactor?: number; fontSize?: number; labelHeight?: number; edgeLengthFactor?: string; compositTitleSize?: number; radius?: number; - defaultRenderer?: string; + /** + * Decides which rendering engine that is to be used for the rendering. + * + */ + defaultRenderer?: string | 'dagre-d3' | 'dagre-wrapper' | 'elk'; } - +/** + * This interface was referenced by `MermaidConfig`'s JSON-Schema + * via the `definition` "ClassDiagramConfig". + */ export interface ClassDiagramConfig extends BaseDiagramConfig { + /** + * Margin top for the text over the diagram + */ titleTopMargin?: number; + /** + * Controls whether or arrow markers in html code are absolute paths or anchors. + * This matters if you are using base tag settings. + * + */ arrowMarkerAbsolute?: boolean; dividerMargin?: number; padding?: number; textHeight?: number; - defaultRenderer?: string; + /** + * Decides which rendering engine that is to be used for the rendering. + * + */ + defaultRenderer?: string | 'dagre-d3' | 'dagre-wrapper' | 'elk'; nodeSpacing?: number; rankSpacing?: number; + /** + * The amount of padding around the diagram as a whole so that embedded + * diagrams have margins, expressed in pixels. + * + */ diagramPadding?: number; htmlLabels?: boolean; } - +/** + * The object containing configurations specific for journey diagrams + * + * + * This interface was referenced by `MermaidConfig`'s JSON-Schema + * via the `definition` "JourneyDiagramConfig". + */ export interface JourneyDiagramConfig extends BaseDiagramConfig { + /** + * Margin to the right and left of the c4 diagram, must be a positive value. + * + */ diagramMarginX?: number; + /** + * Margin to the over and under the c4 diagram, must be a positive value. + * + */ diagramMarginY?: number; + /** + * Margin between actors + */ leftMargin?: number; + /** + * Width of actor boxes + */ width?: number; + /** + * Height of actor boxes + */ height?: number; + /** + * Margin around loop boxes + */ boxMargin?: number; + /** + * Margin around the text in loop/alt/opt boxes + */ boxTextMargin?: number; + /** + * Margin around notes + */ noteMargin?: number; + /** + * Space between messages. + */ messageMargin?: number; - messageAlign?: string; + /** + * Multiline message alignment + */ + messageAlign?: string | 'left' | 'center' | 'right'; + /** + * Prolongs the edge of the diagram downwards. + * + * Depending on css styling this might need adjustment. + * + */ bottomMarginAdj?: number; + /** + * Curved Arrows become Right Angles + * + * This will display arrows that start and begin at the same node as + * right angles, rather than as curves. + * + */ rightAngles?: boolean; taskFontSize?: string | number; taskFontFamily?: string; taskMargin?: number; + /** + * Width of activation box + */ activationWidth?: number; + /** + * text placement as: tspan | fo | old only text as before + * + */ textPlacement?: string; actorColours?: string[]; sectionFills?: string[]; sectionColours?: string[]; } - +/** + * This interface was referenced by `MermaidConfig`'s JSON-Schema + * via the `definition` "TimelineDiagramConfig". + */ export interface TimelineDiagramConfig extends BaseDiagramConfig { + /** + * Margin to the right and left of the c4 diagram, must be a positive value. + * + */ diagramMarginX?: number; + /** + * Margin to the over and under the c4 diagram, must be a positive value. + * + */ diagramMarginY?: number; + /** + * Margin between actors + */ leftMargin?: number; + /** + * Width of actor boxes + */ width?: number; + /** + * Height of actor boxes + */ height?: number; padding?: number; + /** + * Margin around loop boxes + */ boxMargin?: number; + /** + * Margin around the text in loop/alt/opt boxes + */ boxTextMargin?: number; + /** + * Margin around notes + */ noteMargin?: number; + /** + * Space between messages. + */ messageMargin?: number; - messageAlign?: string; + /** + * Multiline message alignment + */ + messageAlign?: string | 'left' | 'center' | 'right'; + /** + * Prolongs the edge of the diagram downwards. + * + * Depending on css styling this might need adjustment. + * + */ bottomMarginAdj?: number; + /** + * Curved Arrows become Right Angles + * + * This will display arrows that start and begin at the same node as + * right angles, rather than as curves. + * + */ rightAngles?: boolean; taskFontSize?: string | number; taskFontFamily?: string; taskMargin?: number; + /** + * Width of activation box + */ activationWidth?: number; + /** + * text placement as: tspan | fo | old only text as before + * + */ textPlacement?: string; actorColours?: string[]; sectionFills?: string[]; sectionColours?: string[]; disableMulticolor?: boolean; - useMaxWidth?: boolean; } - +/** + * The object containing configurations specific for gantt diagrams + * + * + * This interface was referenced by `MermaidConfig`'s JSON-Schema + * via the `definition` "GanttDiagramConfig". + */ export interface GanttDiagramConfig extends BaseDiagramConfig { + /** + * Margin top for the text over the diagram + */ titleTopMargin?: number; + /** + * The height of the bars in the graph + */ barHeight?: number; + /** + * The margin between the different activities in the gantt diagram + */ barGap?: number; + /** + * Margin between title and gantt diagram and between axis and gantt diagram. + * + */ topPadding?: number; + /** + * The space allocated for the section name to the right of the activities + * + */ rightPadding?: number; + /** + * The space allocated for the section name to the left of the activities + * + */ leftPadding?: number; + /** + * Vertical starting position of the grid lines + */ gridLineStartPadding?: number; + /** + * Font size + */ fontSize?: number; + /** + * Font size for sections + */ sectionFontSize?: string | number; + /** + * The number of alternating section styles + */ numberSectionStyles?: number; + /** + * Date/time format of the axis + * + * This might need adjustment to match your locale and preferences. + * + */ axisFormat?: string; + /** + * axis ticks + * + * Pattern is: + * + * ```javascript + * /^([1-9][0-9]*)(minute|hour|day|week|month)$/ + * ``` + * + */ tickInterval?: string; + /** + * When this flag is set, date labels will be added to the top of the chart + * + */ topAxis?: boolean; - displayMode?: string; + /** + * Controls the display mode. + * + */ + displayMode?: string | 'compact'; + /** + * On which day a week-based interval should start + * + */ + weekday?: 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday'; } - +/** + * The object containing configurations specific for sequence diagrams + * + * This interface was referenced by `MermaidConfig`'s JSON-Schema + * via the `definition` "SequenceDiagramConfig". + */ export interface SequenceDiagramConfig extends BaseDiagramConfig { arrowMarkerAbsolute?: boolean; hideUnusedParticipants?: boolean; + /** + * Width of the activation rect + */ activationWidth?: number; + /** + * Margin to the right and left of the sequence diagram + */ diagramMarginX?: number; + /** + * Margin to the over and under the sequence diagram + */ diagramMarginY?: number; + /** + * Margin between actors + */ actorMargin?: number; + /** + * Width of actor boxes + */ width?: number; + /** + * Height of actor boxes + */ height?: number; + /** + * Margin around loop boxes + */ boxMargin?: number; + /** + * Margin around the text in loop/alt/opt boxes + */ boxTextMargin?: number; + /** + * Margin around notes + */ noteMargin?: number; + /** + * Space between messages. + */ messageMargin?: number; - messageAlign?: string; + /** + * Multiline message alignment + */ + messageAlign?: string | 'left' | 'center' | 'right'; + /** + * Mirror actors under diagram + * + */ mirrorActors?: boolean; + /** + * forces actor popup menus to always be visible (to support E2E testing). + * + */ forceMenus?: boolean; + /** + * Prolongs the edge of the diagram downwards. + * + * Depending on css styling this might need adjustment. + * + */ bottomMarginAdj?: number; + /** + * Curved Arrows become Right Angles + * + * This will display arrows that start and begin at the same node as + * right angles, rather than as curves. + * + */ rightAngles?: boolean; + /** + * This will show the node numbers + */ showSequenceNumbers?: boolean; + /** + * This sets the font size of the actor's description + */ actorFontSize?: string | number; + /** + * This sets the font family of the actor's description + */ actorFontFamily?: string; + /** + * This sets the font weight of the actor's description + */ actorFontWeight?: string | number; + /** + * This sets the font size of actor-attached notes + */ noteFontSize?: string | number; + /** + * This sets the font family of actor-attached notes + */ noteFontFamily?: string; + /** + * This sets the font weight of actor-attached notes + */ noteFontWeight?: string | number; - noteAlign?: string; + /** + * This sets the text alignment of actor-attached notes + */ + noteAlign?: string | 'left' | 'center' | 'right'; + /** + * This sets the font size of actor messages + */ messageFontSize?: string | number; + /** + * This sets the font family of actor messages + */ messageFontFamily?: string; + /** + * This sets the font weight of actor messages + */ messageFontWeight?: string | number; + /** + * This sets the auto-wrap state for the diagram + */ wrap?: boolean; + /** + * This sets the auto-wrap padding for the diagram (sides only) + */ wrapPadding?: number; + /** + * This sets the width of the loop-box (loop, alt, opt, par) + */ labelBoxWidth?: number; + /** + * This sets the height of the loop-box (loop, alt, opt, par) + */ labelBoxHeight?: number; messageFont?: FontCalculator; noteFont?: FontCalculator; actorFont?: FontCalculator; } - +/** + * The object containing configurations specific for flowcharts + * + * This interface was referenced by `MermaidConfig`'s JSON-Schema + * via the `definition` "FlowchartDiagramConfig". + */ export interface FlowchartDiagramConfig extends BaseDiagramConfig { + /** + * Margin top for the text over the diagram + */ titleTopMargin?: number; arrowMarkerAbsolute?: boolean; + /** + * The amount of padding around the diagram as a whole so that embedded + * diagrams have margins, expressed in pixels. + * + */ diagramPadding?: number; + /** + * Flag for setting whether or not a html tag should be used for rendering labels on the edges. + * + */ htmlLabels?: boolean; + /** + * Defines the spacing between nodes on the same level + * + * Pertains to horizontal spacing for TB (top to bottom) or BT (bottom to top) graphs, + * and the vertical spacing for LR as well as RL graphs. + * + */ nodeSpacing?: number; + /** + * Defines the spacing between nodes on different levels + * + * Pertains to horizontal spacing for TB (top to bottom) or BT (bottom to top) graphs, + * and the vertical spacing for LR as well as RL graphs. + * + */ rankSpacing?: number; - curve?: string; + /** + * Defines how mermaid renders curves for flowcharts. + * + */ + curve?: string | 'basis' | 'linear' | 'cardinal'; + /** + * Represents the padding between the labels and the shape + * + * **Only used in new experimental rendering.** + * + */ padding?: number; - defaultRenderer?: string; + /** + * Decides which rendering engine that is to be used for the rendering. + * + */ + defaultRenderer?: string | 'dagre-d3' | 'dagre-wrapper' | 'elk'; + /** + * Width of nodes where text is wrapped. + * + * When using markdown strings the text ius wrapped automatically, this + * value sets the max width of a text before it continues on a new line. + * + */ wrappingWidth?: number; } - +/** + * The object containing configurations specific for sankey diagrams. + * + * This interface was referenced by `MermaidConfig`'s JSON-Schema + * via the `definition` "SankeyDiagramConfig". + */ +export interface SankeyDiagramConfig extends BaseDiagramConfig { + width?: number; + height?: number; + /** + * The color of the links in the sankey diagram. + * + */ + linkColor?: SankeyLinkColor | string; + /** + * Controls the alignment of the Sankey diagrams. + * + * See . + * + */ + nodeAlignment?: 'left' | 'right' | 'center' | 'justify'; + useMaxWidth?: boolean; + /** + * Toggle to display or hide values along with title. + * + */ + showValues?: boolean; + /** + * The prefix to use for values + * + */ + prefix?: string; + /** + * The suffix to use for values + * + */ + suffix?: string; +} +/** + * This interface was referenced by `MermaidConfig`'s JSON-Schema + * via the `definition` "FontConfig". + */ export interface FontConfig { - fontSize?: string | number; + fontSize?: CSSFontSize; + /** + * The CSS [`font-family`](https://developer.mozilla.org/en-US/docs/Web/CSS/font-family) to use. + */ fontFamily?: string; + /** + * The font weight to use. + */ fontWeight?: string | number; } - -export type FontCalculator = () => Partial; - -export {}; diff --git a/packages/mermaid/src/dagre-wrapper/GraphObjects.md b/packages/mermaid/src/dagre-wrapper/GraphObjects.md index 184fc51372..840ddf8242 100644 --- a/packages/mermaid/src/dagre-wrapper/GraphObjects.md +++ b/packages/mermaid/src/dagre-wrapper/GraphObjects.md @@ -1,6 +1,6 @@ # Cluster handling -Dagre does not support edges between nodes and clusters or between clusters to other clusters. In order to remedy this shortcoming the dagre wrapper implements a few work-arounds. +Dagre does not support edges between nodes and clusters or between clusters to other clusters. In order to remedy this shortcoming the dagre wrapper implements a few workarounds. In the diagram below there are two clusters and there are no edges to nodes outside the own cluster. @@ -73,7 +73,7 @@ Sample object: } ``` -This is set by the renderer of the diagram and insert the data that the wrapper neds for rendering. +This is set by the renderer of the diagram and insert the data that the wrapper needs for rendering. | property | description | | ---------- | ------------------------------------------------------------------------------------------------ | @@ -114,7 +114,7 @@ Required edgeData for proper rendering: | label | overlap between label and labelText? | | labelPos | | | labelType | overlap between label and labelText? | -| thickness | Sets the thinkess of the edge. Can be \['normal', 'thick'\] | +| thickness | Sets the thickness of the edge. Can be \['normal', 'thick'\] | | pattern | Sets the pattern of the edge. Can be \['solid', 'dotted', 'dashed'\] | # Markers diff --git a/packages/mermaid/src/dagre-wrapper/edges.js b/packages/mermaid/src/dagre-wrapper/edges.js index 1581658b05..f89b4422be 100644 --- a/packages/mermaid/src/dagre-wrapper/edges.js +++ b/packages/mermaid/src/dagre-wrapper/edges.js @@ -368,7 +368,20 @@ const cutPathAtIntersect = (_points, boundryNode) => { return points; }; -//(edgePaths, e, edge, clusterDb, diagramtype, graph) +/** + * Calculate the deltas and angle between two points + * @param {{x: number, y:number}} point1 + * @param {{x: number, y:number}} point2 + * @returns {{angle: number, deltaX: number, deltaY: number}} + */ +function calculateDeltaAndAngle(point1, point2) { + const [x1, y1] = [point1.x, point1.y]; + const [x2, y2] = [point2.x, point2.y]; + const deltaX = x2 - x1; + const deltaY = y2 - y1; + return { angle: Math.atan(deltaY / deltaX), deltaX, deltaY }; +} + export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph) { let points = edge.points; let pointsHasChanged = false; @@ -435,22 +448,62 @@ export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph const lineData = points.filter((p) => !Number.isNaN(p.y)); // This is the accessor function we talked about above - let curve; + let curve = curveBasis; // Currently only flowcharts get the curve from the settings, perhaps this should // be expanded to a common setting? Restricting it for now in order not to cause side-effects that // have not been thought through - if (diagramType === 'graph' || diagramType === 'flowchart') { - curve = edge.curve || curveBasis; - } else { - curve = curveBasis; + if (edge.curve && (diagramType === 'graph' || diagramType === 'flowchart')) { + curve = edge.curve; } - // curve = curveLinear; + + // We need to draw the lines a bit shorter to avoid drawing + // under any transparent markers. + // The offsets are calculated from the markers' dimensions. + const markerOffsets = { + aggregation: 18, + extension: 18, + composition: 18, + dependency: 6, + lollipop: 13.5, + arrow_point: 5.3, + }; + const lineFunction = line() - .x(function (d) { - return d.x; + .x(function (d, i, data) { + let offset = 0; + if (i === 0 && Object.hasOwn(markerOffsets, edge.arrowTypeStart)) { + // Handle first point + // Calculate the angle and delta between the first two points + const { angle, deltaX } = calculateDeltaAndAngle(data[0], data[1]); + // Calculate the offset based on the angle and the marker's dimensions + offset = markerOffsets[edge.arrowTypeStart] * Math.cos(angle) * (deltaX >= 0 ? 1 : -1) || 0; + } else if (i === data.length - 1 && Object.hasOwn(markerOffsets, edge.arrowTypeEnd)) { + // Handle last point + // Calculate the angle and delta between the last two points + const { angle, deltaX } = calculateDeltaAndAngle( + data[data.length - 1], + data[data.length - 2] + ); + offset = markerOffsets[edge.arrowTypeEnd] * Math.cos(angle) * (deltaX >= 0 ? 1 : -1) || 0; + } + return d.x + offset; }) - .y(function (d) { - return d.y; + .y(function (d, i, data) { + // Same handling as X above + let offset = 0; + if (i === 0 && Object.hasOwn(markerOffsets, edge.arrowTypeStart)) { + const { angle, deltaY } = calculateDeltaAndAngle(data[0], data[1]); + offset = + markerOffsets[edge.arrowTypeStart] * Math.abs(Math.sin(angle)) * (deltaY >= 0 ? 1 : -1); + } else if (i === data.length - 1 && Object.hasOwn(markerOffsets, edge.arrowTypeEnd)) { + const { angle, deltaY } = calculateDeltaAndAngle( + data[data.length - 1], + data[data.length - 2] + ); + offset = + markerOffsets[edge.arrowTypeEnd] * Math.abs(Math.sin(angle)) * (deltaY >= 0 ? 1 : -1); + } + return d.y + offset; }) .curve(curve); diff --git a/packages/mermaid/src/dagre-wrapper/index.js b/packages/mermaid/src/dagre-wrapper/index.js index 590242b029..279c5d9ddd 100644 --- a/packages/mermaid/src/dagre-wrapper/index.js +++ b/packages/mermaid/src/dagre-wrapper/index.js @@ -155,9 +155,9 @@ export const render = async (elem, graph, markers, diagramtype, id) => { clearClusters(); clearGraphlib(); - log.warn('Graph at first:', graphlibJson.write(graph)); + log.warn('Graph at first:', JSON.stringify(graphlibJson.write(graph))); adjustClustersAndEdges(graph); - log.warn('Graph after:', graphlibJson.write(graph)); + log.warn('Graph after:', JSON.stringify(graphlibJson.write(graph))); // log.warn('Graph ever after:', graphlibJson.write(graph.node('A').graph)); await recursiveRender(elem, graph, diagramtype); }; diff --git a/packages/mermaid/src/dagre-wrapper/markers.js b/packages/mermaid/src/dagre-wrapper/markers.js index 57d092fdfe..051c987f62 100644 --- a/packages/mermaid/src/dagre-wrapper/markers.js +++ b/packages/mermaid/src/dagre-wrapper/markers.js @@ -16,7 +16,7 @@ const extension = (elem, type, id) => { .append('marker') .attr('id', type + '-extensionStart') .attr('class', 'marker extension ' + type) - .attr('refX', 0) + .attr('refX', 18) .attr('refY', 7) .attr('markerWidth', 190) .attr('markerHeight', 240) @@ -29,7 +29,7 @@ const extension = (elem, type, id) => { .append('marker') .attr('id', type + '-extensionEnd') .attr('class', 'marker extension ' + type) - .attr('refX', 19) + .attr('refX', 1) .attr('refY', 7) .attr('markerWidth', 20) .attr('markerHeight', 28) @@ -44,7 +44,7 @@ const composition = (elem, type) => { .append('marker') .attr('id', type + '-compositionStart') .attr('class', 'marker composition ' + type) - .attr('refX', 0) + .attr('refX', 18) .attr('refY', 7) .attr('markerWidth', 190) .attr('markerHeight', 240) @@ -57,7 +57,7 @@ const composition = (elem, type) => { .append('marker') .attr('id', type + '-compositionEnd') .attr('class', 'marker composition ' + type) - .attr('refX', 19) + .attr('refX', 1) .attr('refY', 7) .attr('markerWidth', 20) .attr('markerHeight', 28) @@ -71,7 +71,7 @@ const aggregation = (elem, type) => { .append('marker') .attr('id', type + '-aggregationStart') .attr('class', 'marker aggregation ' + type) - .attr('refX', 0) + .attr('refX', 18) .attr('refY', 7) .attr('markerWidth', 190) .attr('markerHeight', 240) @@ -84,7 +84,7 @@ const aggregation = (elem, type) => { .append('marker') .attr('id', type + '-aggregationEnd') .attr('class', 'marker aggregation ' + type) - .attr('refX', 19) + .attr('refX', 1) .attr('refY', 7) .attr('markerWidth', 20) .attr('markerHeight', 28) @@ -98,7 +98,7 @@ const dependency = (elem, type) => { .append('marker') .attr('id', type + '-dependencyStart') .attr('class', 'marker dependency ' + type) - .attr('refX', 0) + .attr('refX', 6) .attr('refY', 7) .attr('markerWidth', 190) .attr('markerHeight', 240) @@ -111,7 +111,7 @@ const dependency = (elem, type) => { .append('marker') .attr('id', type + '-dependencyEnd') .attr('class', 'marker dependency ' + type) - .attr('refX', 19) + .attr('refX', 13) .attr('refY', 7) .attr('markerWidth', 20) .attr('markerHeight', 28) @@ -125,15 +125,32 @@ const lollipop = (elem, type) => { .append('marker') .attr('id', type + '-lollipopStart') .attr('class', 'marker lollipop ' + type) - .attr('refX', 0) + .attr('refX', 13) + .attr('refY', 7) + .attr('markerWidth', 190) + .attr('markerHeight', 240) + .attr('orient', 'auto') + .append('circle') + .attr('stroke', 'black') + .attr('fill', 'transparent') + .attr('cx', 7) + .attr('cy', 7) + .attr('r', 6); + + elem + .append('defs') + .append('marker') + .attr('id', type + '-lollipopEnd') + .attr('class', 'marker lollipop ' + type) + .attr('refX', 1) .attr('refY', 7) .attr('markerWidth', 190) .attr('markerHeight', 240) .attr('orient', 'auto') .append('circle') .attr('stroke', 'black') - .attr('fill', 'white') - .attr('cx', 6) + .attr('fill', 'transparent') + .attr('cx', 7) .attr('cy', 7) .attr('r', 6); }; @@ -143,7 +160,7 @@ const point = (elem, type) => { .attr('id', type + '-pointEnd') .attr('class', 'marker ' + type) .attr('viewBox', '0 0 10 10') - .attr('refX', 10) + .attr('refX', 6) .attr('refY', 5) .attr('markerUnits', 'userSpaceOnUse') .attr('markerWidth', 12) diff --git a/packages/mermaid/src/dagre-wrapper/mermaid-graphlib.js b/packages/mermaid/src/dagre-wrapper/mermaid-graphlib.js index 72ef969653..1e376054dd 100644 --- a/packages/mermaid/src/dagre-wrapper/mermaid-graphlib.js +++ b/packages/mermaid/src/dagre-wrapper/mermaid-graphlib.js @@ -291,8 +291,8 @@ export const adjustClustersAndEdges = (graph, depth) => { shape: 'labelRect', style: '', }); - const edge1 = JSON.parse(JSON.stringify(edge)); - const edge2 = JSON.parse(JSON.stringify(edge)); + const edge1 = structuredClone(edge); + const edge2 = structuredClone(edge); edge1.label = ''; edge1.arrowTypeEnd = 'none'; edge2.label = ''; diff --git a/packages/mermaid/src/dagre-wrapper/nodes.js b/packages/mermaid/src/dagre-wrapper/nodes.js index b842fa9a52..51ff9ef116 100644 --- a/packages/mermaid/src/dagre-wrapper/nodes.js +++ b/packages/mermaid/src/dagre-wrapper/nodes.js @@ -5,11 +5,27 @@ import { getConfig } from '../config.js'; import intersect from './intersect/index.js'; import createLabel from './createLabel.js'; import note from './shapes/note.js'; -import { parseMember } from '../diagrams/class/svgDraw.js'; import { evaluate } from '../diagrams/common/common.js'; +const formatClass = (str) => { + if (str) { + return ' ' + str; + } + return ''; +}; +const getClassesFromNode = (node, otherClasses) => { + return `${otherClasses ? otherClasses : 'node default'}${formatClass(node.classes)} ${formatClass( + node.class + )}`; +}; + const question = async (parent, node) => { - const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true); + const { shapeSvg, bbox } = await labelHelper( + parent, + node, + getClassesFromNode(node, undefined), + true + ); const w = bbox.width + node.padding; const h = bbox.height + node.padding; @@ -70,7 +86,12 @@ const choice = (parent, node) => { }; const hexagon = async (parent, node) => { - const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true); + const { shapeSvg, bbox } = await labelHelper( + parent, + node, + getClassesFromNode(node, undefined), + true + ); const f = 4; const h = bbox.height + node.padding; @@ -97,7 +118,12 @@ const hexagon = async (parent, node) => { }; const rect_left_inv_arrow = async (parent, node) => { - const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true); + const { shapeSvg, bbox } = await labelHelper( + parent, + node, + getClassesFromNode(node, undefined), + true + ); const w = bbox.width + node.padding; const h = bbox.height + node.padding; @@ -123,7 +149,7 @@ const rect_left_inv_arrow = async (parent, node) => { }; const lean_right = async (parent, node) => { - const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true); + const { shapeSvg, bbox } = await labelHelper(parent, node, getClassesFromNode(node), true); const w = bbox.width + node.padding; const h = bbox.height + node.padding; @@ -146,7 +172,12 @@ const lean_right = async (parent, node) => { }; const lean_left = async (parent, node) => { - const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true); + const { shapeSvg, bbox } = await labelHelper( + parent, + node, + getClassesFromNode(node, undefined), + true + ); const w = bbox.width + node.padding; const h = bbox.height + node.padding; @@ -169,7 +200,12 @@ const lean_left = async (parent, node) => { }; const trapezoid = async (parent, node) => { - const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true); + const { shapeSvg, bbox } = await labelHelper( + parent, + node, + getClassesFromNode(node, undefined), + true + ); const w = bbox.width + node.padding; const h = bbox.height + node.padding; @@ -192,7 +228,12 @@ const trapezoid = async (parent, node) => { }; const inv_trapezoid = async (parent, node) => { - const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true); + const { shapeSvg, bbox } = await labelHelper( + parent, + node, + getClassesFromNode(node, undefined), + true + ); const w = bbox.width + node.padding; const h = bbox.height + node.padding; @@ -215,7 +256,12 @@ const inv_trapezoid = async (parent, node) => { }; const rect_right_inv_arrow = async (parent, node) => { - const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true); + const { shapeSvg, bbox } = await labelHelper( + parent, + node, + getClassesFromNode(node, undefined), + true + ); const w = bbox.width + node.padding; const h = bbox.height + node.padding; @@ -239,7 +285,12 @@ const rect_right_inv_arrow = async (parent, node) => { }; const cylinder = async (parent, node) => { - const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true); + const { shapeSvg, bbox } = await labelHelper( + parent, + node, + getClassesFromNode(node, undefined), + true + ); const w = bbox.width + node.padding; const rx = w / 2; @@ -314,7 +365,7 @@ const rect = async (parent, node) => { const { shapeSvg, bbox, halfPadding } = await labelHelper( parent, node, - 'node ' + node.classes, + 'node ' + node.classes + ' ' + node.class, true ); @@ -360,7 +411,7 @@ const rect = async (parent, node) => { const labelRect = async (parent, node) => { const { shapeSvg } = await labelHelper(parent, node, 'label', true); - log.trace('Classes = ', node.classes); + log.trace('Classes = ', node.class); // add the rect const rect = shapeSvg.insert('rect', ':first-child'); @@ -545,7 +596,12 @@ const rectWithTitle = (parent, node) => { }; const stadium = async (parent, node) => { - const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true); + const { shapeSvg, bbox } = await labelHelper( + parent, + node, + getClassesFromNode(node, undefined), + true + ); const h = bbox.height + node.padding; const w = bbox.width + h / 4 + node.padding; @@ -571,7 +627,12 @@ const stadium = async (parent, node) => { }; const circle = async (parent, node) => { - const { shapeSvg, bbox, halfPadding } = await labelHelper(parent, node, undefined, true); + const { shapeSvg, bbox, halfPadding } = await labelHelper( + parent, + node, + getClassesFromNode(node, undefined), + true + ); const circle = shapeSvg.insert('circle', ':first-child'); // center the circle around its coordinate @@ -596,12 +657,19 @@ const circle = async (parent, node) => { }; const doublecircle = async (parent, node) => { - const { shapeSvg, bbox, halfPadding } = await labelHelper(parent, node, undefined, true); + const { shapeSvg, bbox, halfPadding } = await labelHelper( + parent, + node, + getClassesFromNode(node, undefined), + true + ); const gap = 5; const circleGroup = shapeSvg.insert('g', ':first-child'); const outerCircle = circleGroup.insert('circle'); const innerCircle = circleGroup.insert('circle'); + circleGroup.attr('class', node.class); + // center the circle around its coordinate outerCircle .attr('style', node.style) @@ -632,7 +700,12 @@ const doublecircle = async (parent, node) => { }; const subroutine = async (parent, node) => { - const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true); + const { shapeSvg, bbox } = await labelHelper( + parent, + node, + getClassesFromNode(node, undefined), + true + ); const w = bbox.width + node.padding; const h = bbox.height + node.padding; @@ -806,8 +879,8 @@ const class_box = (parent, node) => { maxWidth = classTitleBBox.width; } const classAttributes = []; - node.classData.members.forEach((str) => { - const parsedInfo = parseMember(str); + node.classData.members.forEach((member) => { + const parsedInfo = member.getDisplayDetails(); let parsedText = parsedInfo.displayText; if (getConfig().flowchart.htmlLabels) { parsedText = parsedText.replace(//g, '>'); @@ -840,8 +913,8 @@ const class_box = (parent, node) => { maxHeight += lineHeight; const classMethods = []; - node.classData.methods.forEach((str) => { - const parsedInfo = parseMember(str); + node.classData.methods.forEach((member) => { + const parsedInfo = member.getDisplayDetails(); let displayText = parsedInfo.displayText; if (getConfig().flowchart.htmlLabels) { displayText = displayText.replace(//g, '>'); @@ -915,7 +988,9 @@ const class_box = (parent, node) => { ((-1 * maxHeight) / 2 + verticalPos + lineHeight / 2) + ')' ); - verticalPos += classTitleBBox.height + rowPadding; + //get the height of the bounding box of each member if exists + const memberBBox = lbl?.getBBox(); + verticalPos += (memberBBox?.height ?? 0) + rowPadding; }); verticalPos += lineHeight; @@ -933,7 +1008,8 @@ const class_box = (parent, node) => { 'transform', 'translate( ' + -maxWidth / 2 + ', ' + ((-1 * maxHeight) / 2 + verticalPos) + ')' ); - verticalPos += classTitleBBox.height + rowPadding; + const memberBBox = lbl?.getBBox(); + verticalPos += (memberBBox?.height ?? 0) + rowPadding; }); rect diff --git a/packages/mermaid/src/dagre-wrapper/shapes/util.js b/packages/mermaid/src/dagre-wrapper/shapes/util.js index 7ad412bdb7..95b82ddc07 100644 --- a/packages/mermaid/src/dagre-wrapper/shapes/util.js +++ b/packages/mermaid/src/dagre-wrapper/shapes/util.js @@ -13,6 +13,7 @@ export const labelHelper = async (parent, node, _classes, isNode) => { } else { classes = _classes; } + // Add outer g element const shapeSvg = parent .insert('g') @@ -49,7 +50,6 @@ export const labelHelper = async (parent, node, _classes, isNode) => { ) ); } - // Get the size of the label let bbox = text.getBBox(); const halfPadding = node.padding / 2; @@ -66,8 +66,11 @@ export const labelHelper = async (parent, node, _classes, isNode) => { await Promise.all( [...images].map( (img) => - new Promise((res) => - img.addEventListener('load', function () { + new Promise((res) => { + /** + * + */ + function setupImage() { img.style.display = 'flex'; img.style.flexDirection = 'column'; @@ -82,8 +85,15 @@ export const labelHelper = async (parent, node, _classes, isNode) => { img.style.width = '100%'; } res(img); - }) - ) + } + setTimeout(() => { + if (img.complete) { + setupImage(); + } + }); + img.addEventListener('error', setupImage); + img.addEventListener('load', setupImage); + }) ) ); } diff --git a/packages/mermaid/src/defaultConfig.ts b/packages/mermaid/src/defaultConfig.ts index f94d5b3fa8..f8bd9b0b53 100644 --- a/packages/mermaid/src/defaultConfig.ts +++ b/packages/mermaid/src/defaultConfig.ts @@ -1,567 +1,31 @@ +import type { RequiredDeep } from 'type-fest'; + import theme from './themes/index.js'; -import { MermaidConfig } from './config.type.js'; +import type { MermaidConfig } from './config.type.js'; + +// Uses our custom Vite jsonSchemaPlugin to load only the default values from +// our JSON Schema +// @ts-expect-error This file is automatically generated via a custom Vite plugin +import defaultConfigJson from './schemas/config.schema.yaml?only-defaults=true'; + /** - * **Configuration methods in Mermaid version 8.6.0 have been updated, to learn more[[click - * here](8.6.0_docs.md)].** - * - * ## **What follows are config instructions for older versions** - * - * These are the default options which can be overridden with the initialization call like so: - * - * **Example 1:** + * Default mermaid configuration options. * - * ```js - * mermaid.initialize({ flowchart:{ htmlLabels: false } }); - * ``` - * - * **Example 2:** - * - * ```html - * - * ``` - * - * A summary of all options and their defaults is found [here](#mermaidapi-configuration-defaults). - * A description of each option follows below. + * Please see the Mermaid config JSON Schema for the default JSON values. + * Non-JSON JS default values are listed in this file, e.g. functions, or + * `undefined` (explicitly set so that `configKeys` finds them). */ -const config: Partial = { - /** - * Theme , the CSS style sheet - * - * | Parameter | Description | Type | Required | Values | - * | --------- | --------------- | ------ | -------- | ---------------------------------------------- | - * | theme | Built in Themes | string | Optional | 'default', 'forest', 'dark', 'neutral', 'null' | - * - * **Notes:** To disable any pre-defined mermaid theme, use "null". - * - * @example - * - * ```js - * { - * "theme": "forest", - * "themeCSS": ".node rect { fill: red; }" - * } - * ``` - */ - theme: 'default', - themeVariables: theme['default'].getThemeVariables(), - themeCSS: undefined, - /* **maxTextSize** - The maximum allowed size of the users text diagram */ - maxTextSize: 50000, - darkMode: false, - - /** - * | Parameter | Description | Type | Required | Values | - * | ---------- | ------------------------------------------------------ | ------ | -------- | --------------------------- | - * | fontFamily | specifies the font to be used in the rendered diagrams | string | Required | Any Possible CSS FontFamily | - * - * **Notes:** Default value: '"trebuchet ms", verdana, arial, sans-serif;'. - */ - fontFamily: '"trebuchet ms", verdana, arial, sans-serif;', - - /** - * | Parameter | Description | Type | Required | Values | - * | --------- | ----------------------------------------------------- | ---------------- | -------- | --------------------------------------------- | - * | logLevel | This option decides the amount of logging to be used. | string \| number | Required | 'trace','debug','info','warn','error','fatal' | - * - * **Notes:** - * - * - Trace: 0 - * - Debug: 1 - * - Info: 2 - * - Warn: 3 - * - Error: 4 - * - Fatal: 5 (default) - */ - logLevel: 5, - - /** - * | Parameter | Description | Type | Required | Values | - * | ------------- | --------------------------------- | ------ | -------- | ------------------------------------------ | - * | securityLevel | Level of trust for parsed diagram | string | Required | 'sandbox', 'strict', 'loose', 'antiscript' | - * - * **Notes**: - * - * - **strict**: (**default**) HTML tags in the text are encoded and click functionality is disabled. - * - **antiscript**: HTML tags in text are allowed (only script elements are removed), and click - * functionality is enabled. - * - **loose**: HTML tags in text are allowed and click functionality is enabled. - * - **sandbox**: With this security level, all rendering takes place in a sandboxed iframe. This - * prevent any JavaScript from running in the context. This may hinder interactive functionality - * of the diagram, like scripts, popups in the sequence diagram, links to other tabs or targets, etc. - */ - securityLevel: 'strict', - - /** - * | Parameter | Description | Type | Required | Values | - * | ----------- | -------------------------------------------- | ------- | -------- | ----------- | - * | startOnLoad | Dictates whether mermaid starts on Page load | boolean | Required | true, false | - * - * **Notes:** Default value: true - */ - startOnLoad: true, - - /** - * | Parameter | Description | Type | Required | Values | - * | ------------------- | ---------------------------------------------------------------------------- | ------- | -------- | ----------- | - * | arrowMarkerAbsolute | Controls whether or arrow markers in html code are absolute paths or anchors | boolean | Required | true, false | - * - * **Notes**: - * - * This matters if you are using base tag settings. - * - * Default value: false - */ - arrowMarkerAbsolute: false, - - /** - * This option controls which currentConfig keys are considered _secure_ and can only be changed - * via call to mermaidAPI.initialize. Calls to mermaidAPI.reinitialize cannot make changes to the - * `secure` keys in the current currentConfig. This prevents malicious graph directives from - * overriding a site's default security. - * - * **Notes**: - * - * Default value: ['secure', 'securityLevel', 'startOnLoad', 'maxTextSize'] - */ - secure: ['secure', 'securityLevel', 'startOnLoad', 'maxTextSize'], - /** - * This option controls if the generated ids of nodes in the SVG are generated randomly or based - * on a seed. If set to false, the IDs are generated based on the current date and thus are not - * deterministic. This is the default behavior. - * - * **Notes**: - * - * This matters if your files are checked into source control e.g. git and should not change unless - * content is changed. - * - * Default value: false - */ - deterministicIds: false, - - /** - * This option is the optional seed for deterministic ids. if set to undefined but - * deterministicIds is true, a simple number iterator is used. You can set this attribute to base - * the seed on a static string. - */ +const config: RequiredDeep = { + ...defaultConfigJson, + // Set, even though they're `undefined` so that `configKeys` finds these keys + // TODO: Should we replace these with `null` so that they can go in the JSON Schema? deterministicIDSeed: undefined, + themeCSS: undefined, - /** - * This option suppresses inserting 'Syntax error' message in diagram. This option is useful when - * you want to control how to handle syntax error in your application. - * - * Default value: false - */ - suppressErrorRendering: false, - - /** The object containing configurations specific for flowcharts */ - flowchart: { - /** - * ### titleTopMargin - * - * | Parameter | Description | Type | Required | Values | - * | -------------- | ---------------------------------------------- | ------- | -------- | ------------------ | - * | titleTopMargin | Margin top for the text over the flowchart | Integer | Required | Any Positive Value | - * - * **Notes:** Default value: 25 - */ - titleTopMargin: 25, - - /** - * | Parameter | Description | Type | Required | Values | - * | -------------- | ----------------------------------------------- | ------- | -------- | ------------------ | - * | diagramPadding | Amount of padding around the diagram as a whole | Integer | Required | Any Positive Value | - * - * **Notes:** - * - * The amount of padding around the diagram as a whole so that embedded diagrams have margins, - * expressed in pixels - * - * Default value: 8 - */ - diagramPadding: 8, - - /** - * | Parameter | Description | Type | Required | Values | - * | ---------- | -------------------------------------------------------------------------------------------- | ------- | -------- | ----------- | - * | htmlLabels | Flag for setting whether or not a html tag should be used for rendering labels on the edges. | boolean | Required | true, false | - * - * **Notes:** Default value: true. - */ - htmlLabels: true, - - /** - * | Parameter | Description | Type | Required | Values | - * | ----------- | --------------------------------------------------- | ------- | -------- | ------------------- | - * | nodeSpacing | Defines the spacing between nodes on the same level | Integer | Required | Any positive Number | - * - * **Notes:** - * - * Pertains to horizontal spacing for TB (top to bottom) or BT (bottom to top) graphs, and the - * vertical spacing for LR as well as RL graphs.** - * - * Default value: 50 - */ - nodeSpacing: 50, - - /** - * | Parameter | Description | Type | Required | Values | - * | ----------- | ----------------------------------------------------- | ------- | -------- | ------------------- | - * | rankSpacing | Defines the spacing between nodes on different levels | Integer | Required | Any Positive Number | - * - * **Notes**: - * - * Pertains to vertical spacing for TB (top to bottom) or BT (bottom to top), and the horizontal - * spacing for LR as well as RL graphs. - * - * Default value 50 - */ - rankSpacing: 50, - - /** - * | Parameter | Description | Type | Required | Values | - * | --------- | -------------------------------------------------- | ------ | -------- | ----------------------------- | - * | curve | Defines how mermaid renders curves for flowcharts. | string | Required | 'basis', 'linear', 'cardinal' | - * - * **Notes:** - * - * Default Value: 'basis' - */ - curve: 'basis', - // Only used in new experimental rendering - // represents the padding between the labels and the shape - padding: 15, - - /** - * | 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 | - * | --------------- | ----------- | ------- | -------- | ----------------------- | - * | defaultRenderer | See notes | boolean | 4 | dagre-d3, dagre-wrapper, elk | - * - * **Notes:** - * - * Decides which rendering engine that is to be used for the rendering. Legal values are: - * dagre-d3 dagre-wrapper - wrapper for dagre implemented in mermaid, elk for layout using - * elkjs - * - * Default value: 'dagre-wrapper' - */ - defaultRenderer: 'dagre-wrapper', - /** - * | Parameter | Description | Type | Required | Values | - * | --------------- | ----------- | ------- | -------- | ----------------------- | - * | wrappingWidth | See notes | number | 4 | width of nodes where text is wrapped | - * - * **Notes:** - * - * When using markdown strings the text ius wrapped automatically, this - * value sets the max width of a text before it continues on a new line. - * Default value: 'dagre-wrapper' - */ - wrappingWidth: 200, - }, - - /** The object containing configurations specific for sequence diagrams */ + // add non-JSON default config values + themeVariables: theme['default'].getThemeVariables(), sequence: { - hideUnusedParticipants: false, - /** - * | Parameter | Description | Type | Required | Values | - * | --------------- | ---------------------------- | ------- | -------- | ------------------ | - * | activationWidth | Width of the activation rect | Integer | Required | Any Positive Value | - * - * **Notes:** Default value :10 - */ - activationWidth: 10, - - /** - * | 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 - */ - actorMargin: 50, - - /** - * | 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: 65, - - /** - * | 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:** Default value: 35 - */ - messageMargin: 35, - - /** - * | Parameter | Description | Type | Required | Values | - * | ------------ | --------------------------- | ------ | -------- | ------------------------- | - * | messageAlign | Multiline message alignment | string | Required | 'left', 'center', 'right' | - * - * **Notes:** Default value: 'center' - */ - messageAlign: 'center', - - /** - * | Parameter | Description | Type | Required | Values | - * | ------------ | --------------------------- | ------- | -------- | ----------- | - * | mirrorActors | Mirror actors under diagram | boolean | Required | true, false | - * - * **Notes:** Default value: true - */ - mirrorActors: true, - - /** - * | Parameter | Description | Type | Required | Values | - * | ---------- | ----------------------------------------------------------------------- | ------- | -------- | ----------- | - * | forceMenus | forces actor popup menus to always be visible (to support E2E testing). | Boolean | Required | True, False | - * - * **Notes:** - * - * Default value: false. - */ - forceMenus: false, - - /** - * | Parameter | Description | Type | Required | Values | - * | --------------- | ------------------------------------------ | ------- | -------- | ------------------ | - * | bottomMarginAdj | Prolongs the edge of the diagram downwards | Integer | Required | 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 | Required | true, false | - * - * **Notes:** When this flag is set to true, the height and width is set to 100% and is then - * scaling with the available space. If set to false, the absolute space required is used. - * - * Default value: true - */ - useMaxWidth: true, - - /** - * | Parameter | Description | Type | Required | Values | - * | ----------- | ------------------------------------ | ------- | -------- | ----------- | - * | rightAngles | display curve arrows as right angles | boolean | Required | true, false | - * - * **Notes:** - * - * This will display arrows that start and begin at the same node as right angles, rather than a - * curve - * - * Default value: false - */ - rightAngles: false, - - /** - * | Parameter | Description | Type | Required | Values | - * | ------------------- | ------------------------------- | ------- | -------- | ----------- | - * | showSequenceNumbers | This will show the node numbers | boolean | Required | true, false | - * - * **Notes:** Default value: false - */ - showSequenceNumbers: false, - - /** - * | Parameter | Description | Type | Required | Values | - * | ------------- | -------------------------------------------------- | ------- | -------- | ------------------ | - * | actorFontSize | This sets the font size of the actor's description | Integer | Require | Any Positive Value | - * - * **Notes:** **Default value 14**.. - */ - actorFontSize: 14, - - /** - * | Parameter | Description | Type | Required | Values | - * | --------------- | ---------------------------------------------------- | ------ | -------- | --------------------------- | - * | actorFontFamily | This sets the font family of the actor's description | string | Required | Any Possible CSS FontFamily | - * - * **Notes:** Default value: "'Open Sans", sans-serif' - */ - actorFontFamily: '"Open Sans", sans-serif', - - /** - * This sets the font weight of the actor's description - * - * **Notes:** Default value: 400. - */ - actorFontWeight: 400, - - /** - * | Parameter | Description | Type | Required | Values | - * | ------------ | ----------------------------------------------- | ------- | -------- | ------------------ | - * | noteFontSize | This sets the font size of actor-attached notes | Integer | Required | Any Positive Value | - * - * **Notes:** Default value: 14 - */ - noteFontSize: 14, - - /** - * | Parameter | Description | Type | Required | Values | - * | -------------- | -------------------------------------------------- | ------ | -------- | --------------------------- | - * | noteFontFamily | This sets the font family of actor-attached notes. | string | Required | Any Possible CSS FontFamily | - * - * **Notes:** Default value: ''"trebuchet ms", verdana, arial, sans-serif' - */ - noteFontFamily: '"trebuchet ms", verdana, arial, sans-serif', - - /** - * This sets the font weight of the note's description - * - * **Notes:** Default value: 400 - */ - noteFontWeight: 400, - - /** - * | Parameter | Description | Type | Required | Values | - * | --------- | ---------------------------------------------------- | ------ | -------- | ------------------------- | - * | noteAlign | This sets the text alignment of actor-attached notes | string | required | 'left', 'center', 'right' | - * - * **Notes:** Default value: 'center' - */ - noteAlign: 'center', - - /** - * | Parameter | Description | Type | Required | Values | - * | --------------- | ----------------------------------------- | ------- | -------- | ------------------- | - * | messageFontSize | This sets the font size of actor messages | Integer | Required | Any Positive Number | - * - * **Notes:** Default value: 16 - */ - messageFontSize: 16, - - /** - * | Parameter | Description | Type | Required | Values | - * | ----------------- | ------------------------------------------- | ------ | -------- | --------------------------- | - * | messageFontFamily | This sets the font family of actor messages | string | Required | Any Possible CSS FontFamily | - * - * **Notes:** Default value: '"trebuchet ms", verdana, arial, sans-serif' - */ - messageFontFamily: '"trebuchet ms", verdana, arial, sans-serif', - - /** - * This sets the font weight of the message's description - * - * **Notes:** Default value: 400. - */ - messageFontWeight: 400, - - /** - * This sets the auto-wrap state for the diagram - * - * **Notes:** Default value: false. - */ - wrap: false, - - /** - * This sets the auto-wrap padding for the diagram (sides only) - * - * **Notes:** Default value: 0. - */ - wrapPadding: 10, - - /** - * This sets the width of the loop-box (loop, alt, opt, par) - * - * **Notes:** Default value: 50. - */ - labelBoxWidth: 50, - - /** - * This sets the height of the loop-box (loop, alt, opt, par) - * - * **Notes:** Default value: 20. - */ - labelBoxHeight: 20, - + ...defaultConfigJson.sequence, messageFont: function () { return { fontFamily: this.messageFontFamily, @@ -584,1476 +48,14 @@ const config: Partial = { }; }, }, - - /** The object containing configurations specific for gantt diagrams */ gantt: { - /** - * ### titleTopMargin - * - * | Parameter | Description | Type | Required | Values | - * | -------------- | ---------------------------------------------- | ------- | -------- | ------------------ | - * | titleTopMargin | Margin top for the text over the gantt diagram | Integer | Required | Any Positive Value | - * - * **Notes:** Default value: 25 - */ - titleTopMargin: 25, - - /** - * | Parameter | Description | Type | Required | Values | - * | --------- | ----------------------------------- | ------- | -------- | ------------------ | - * | barHeight | The height of the bars in the graph | Integer | Required | Any Positive Value | - * - * **Notes:** Default value: 20 - */ - barHeight: 20, - - /** - * | Parameter | Description | Type | Required | Values | - * | --------- | ---------------------------------------------------------------- | ------- | -------- | ------------------ | - * | barGap | The margin between the different activities in the gantt diagram | Integer | Optional | Any Positive Value | - * - * **Notes:** Default value: 4 - */ - barGap: 4, - - /** - * | Parameter | Description | Type | Required | Values | - * | ---------- | -------------------------------------------------------------------------- | ------- | -------- | ------------------ | - * | topPadding | Margin between title and gantt diagram and between axis and gantt diagram. | Integer | Required | Any Positive Value | - * - * **Notes:** Default value: 50 - */ - topPadding: 50, - - /** - * | Parameter | Description | Type | Required | Values | - * | ------------ | ----------------------------------------------------------------------- | ------- | -------- | ------------------ | - * | rightPadding | The space allocated for the section name to the right of the activities | Integer | Required | Any Positive Value | - * - * **Notes:** Default value: 75 - */ - rightPadding: 75, - - /** - * | Parameter | Description | Type | Required | Values | - * | ----------- | ---------------------------------------------------------------------- | ------- | -------- | ------------------ | - * | leftPadding | The space allocated for the section name to the left of the activities | Integer | Required | Any Positive Value | - * - * **Notes:** Default value: 75 - */ - leftPadding: 75, - - /** - * | Parameter | Description | Type | Required | Values | - * | -------------------- | -------------------------------------------- | ------- | -------- | ------------------ | - * | gridLineStartPadding | Vertical starting position of the grid lines | Integer | Required | Any Positive Value | - * - * **Notes:** Default value: 35 - */ - gridLineStartPadding: 35, - - /** - * | Parameter | Description | Type | Required | Values | - * | --------- | ----------- | ------- | -------- | ------------------ | - * | fontSize | Font size | Integer | Required | Any Positive Value | - * - * **Notes:** Default value: 11 - */ - fontSize: 11, - - /** - * | Parameter | Description | Type | Required | Values | - * | --------------- | ---------------------- | ------- | -------- | ------------------ | - * | sectionFontSize | Font size for sections | Integer | Required | Any Positive Value | - * - * **Notes:** Default value: 11 - */ - sectionFontSize: 11, - - /** - * | Parameter | Description | Type | Required | Values | - * | ------------------- | ---------------------------------------- | ------- | -------- | ------------------ | - * | numberSectionStyles | The number of alternating section styles | Integer | 4 | Any Positive Value | - * - * **Notes:** Default value: 4 - */ - numberSectionStyles: 4, - - /** - * | Parameter | Description | Type | Required | Values | - * | ----------- | ------------------------- | ------ | -------- | --------- | - * | displayMode | Controls the display mode | string | 4 | 'compact' | - * - * **Notes**: - * - * - **compact**: Enables displaying multiple tasks on the same row. - */ - displayMode: '', - - /** - * | Parameter | Description | Type | Required | Values | - * | ---------- | ---------------------------- | ---- | -------- | ---------------- | - * | axisFormat | Date/time format of the axis | 3 | Required | Date in yy-mm-dd | - * - * **Notes:** - * - * This might need adjustment to match your locale and preferences - * - * Default value: '%Y-%m-%d'. - */ - axisFormat: '%Y-%m-%d', - - /** - * | Parameter | Description | Type | Required | Values | - * | ------------ | ------------| ------ | -------- | ------- | - * | tickInterval | axis ticks | string | Optional | string | - * - * **Notes:** - * - * Pattern is /^([1-9][0-9]*)(minute|hour|day|week|month)$/ - * - * Default value: undefined - */ + ...defaultConfigJson.gantt, tickInterval: undefined, - /** - * | 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 | - * | --------- | ----------- | ------- | -------- | ----------- | - * | topAxis | See notes | Boolean | 4 | True, False | - * - * **Notes:** when this flag is set date labels will be added to the top of the chart - * - * **Default value false**. - */ - topAxis: false, - - useWidth: undefined, - }, - - /** The object containing configurations specific for journey diagrams */ - journey: { - /** - * | 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'], + useWidth: undefined, // can probably be removed since `configKeys` already includes this }, - /** 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 - * - * | Parameter | Description | Type | Required | Values | - * | -------------- | ---------------------------------------------- | ------- | -------- | ------------------ | - * | titleTopMargin | Margin top for the text over the class diagram | Integer | Required | Any Positive Value | - * - * **Notes:** Default value: 25 - */ - titleTopMargin: 25, - arrowMarkerAbsolute: false, - dividerMargin: 10, - padding: 5, - textHeight: 10, - - /** - * | 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 | - * | --------------- | ----------- | ------- | -------- | ----------------------- | - * | defaultRenderer | See notes | boolean | 4 | dagre-d3, dagre-wrapper | - * - * **Notes**: - * - * Decides which rendering engine that is to be used for the rendering. Legal values are: - * dagre-d3 dagre-wrapper - wrapper for dagre implemented in mermaid - * - * Default value: 'dagre-d3' - */ - defaultRenderer: 'dagre-wrapper', - }, - state: { - /** - * ### titleTopMargin - * - * | Parameter | Description | Type | Required | Values | - * | -------------- | ---------------------------------------------- | ------- | -------- | ------------------ | - * | titleTopMargin | Margin top for the text over the state diagram | Integer | Required | Any Positive Value | - * - * **Notes:** Default value: 25 - */ - titleTopMargin: 25, - dividerMargin: 10, - sizeUnit: 5, - padding: 8, - textHeight: 10, - titleShift: -15, - noteMargin: 10, - forkWidth: 70, - forkHeight: 7, - // Used - miniPadding: 2, - // Font size factor, this is used to guess the width of the edges labels before rendering by dagre - // layout. This might need updating if/when switching font - fontSizeFactor: 5.02, - fontSize: 24, - labelHeight: 16, - edgeLengthFactor: '20', - compositTitleSize: 35, - radius: 5, - /** - * | 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 | - * | --------------- | ----------- | ------- | -------- | ----------------------- | - * | defaultRenderer | See notes | boolean | 4 | dagre-d3, dagre-wrapper | - * - * **Notes:** - * - * Decides which rendering engine that is to be used for the rendering. Legal values are: - * dagre-d3 dagre-wrapper - wrapper for dagre implemented in mermaid - * - * Default value: 'dagre-d3' - */ - defaultRenderer: 'dagre-wrapper', - }, - - /** The object containing configurations specific for entity relationship diagrams */ - er: { - /** - * ### titleTopMargin - * - * | Parameter | Description | Type | Required | Values | - * | -------------- | ---------------------------------------------- | ------- | -------- | ------------------ | - * | titleTopMargin | Margin top for the text over the diagram | Integer | Required | Any Positive Value | - * - * **Notes:** Default value: 25 - */ - titleTopMargin: 25, - - /** - * | Parameter | Description | Type | Required | Values | - * | -------------- | ----------------------------------------------- | ------- | -------- | ------------------ | - * | diagramPadding | Amount of padding around the diagram as a whole | Integer | Required | Any Positive Value | - * - * **Notes:** - * - * The amount of padding around the diagram as a whole so that embedded diagrams have margins, - * expressed in pixels - * - * Default value: 20 - */ - diagramPadding: 20, - - /** - * | Parameter | Description | Type | Required | Values | - * | --------------- | ---------------------------------------- | ------ | -------- | ---------------------- | - * | layoutDirection | Directional bias for layout of entities. | string | Required | "TB", "BT", "LR", "RL" | - * - * **Notes:** - * - * 'TB' for Top-Bottom, 'BT'for Bottom-Top, 'LR' for Left-Right, or 'RL' for Right to Left. - * - * T = top, B = bottom, L = left, and R = right. - * - * Default value: 'TB' - */ - layoutDirection: 'TB', - - /** - * | Parameter | Description | Type | Required | Values | - * | -------------- | ---------------------------------- | ------- | -------- | ------------------ | - * | minEntityWidth | The minimum width of an entity box | Integer | Required | Any Positive Value | - * - * **Notes:** Expressed in pixels. Default value: 100 - */ - minEntityWidth: 100, - - /** - * | Parameter | Description | Type | Required | Values | - * | --------------- | ----------------------------------- | ------- | -------- | ------------------ | - * | minEntityHeight | The minimum height of an entity box | Integer | 4 | Any Positive Value | - * - * **Notes:** Expressed in pixels Default value: 75 - */ - minEntityHeight: 75, - - /** - * | Parameter | Description | Type | Required | Values | - * | ------------- | ------------------------------------------------------------ | ------- | -------- | ------------------ | - * | entityPadding | Minimum internal padding between text in box and box borders | Integer | 4 | Any Positive Value | - * - * **Notes:** - * - * The minimum internal padding between text in an entity box and the enclosing box borders, - * expressed in pixels. - * - * Default value: 15 - */ - entityPadding: 15, - - /** - * | Parameter | Description | Type | Required | Values | - * | --------- | ----------------------------------- | ------ | -------- | -------------------- | - * | stroke | Stroke color of box edges and lines | string | 4 | Any recognized color | - * - * **Notes:** Default value: 'gray' - */ - stroke: 'gray', - - /** - * | Parameter | Description | Type | Required | Values | - * | --------- | -------------------------- | ------ | -------- | -------------------- | - * | fill | Fill color of entity boxes | string | 4 | Any recognized color | - * - * **Notes:** Default value: 'honeydew' - */ - fill: 'honeydew', - - /** - * | Parameter | Description | Type | Required | Values | - * | --------- | ------------------- | ------- | -------- | ------------------ | - * | fontSize | Font Size in pixels | Integer | | Any Positive Value | - * - * **Notes:** - * - * Font size (expressed as an integer representing a number of pixels) Default value: 12 - */ - fontSize: 12, - - /** - * | Parameter | Description | Type | Required | Values | - * | ----------- | ----------- | ------- | -------- | ----------- | - * | useMaxWidth | See Notes | boolean | Required | true, false | - * - * **Notes:** - * - * When this flag is set to true, the diagram width is locked to 100% and scaled based on - * available space. If set to false, the diagram reserves its absolute width. - * - * Default value: true - */ - useMaxWidth: true, - }, - - /** The object containing configurations specific for pie diagrams */ - pie: { - useWidth: undefined, - - /** - * | Parameter | Description | Type | Required | Values | - * | ----------- | ----------- | ------- | -------- | ----------- | - * | useMaxWidth | See Notes | boolean | Required | true, false | - * - * **Notes:** - * - * When this flag is set to true, the diagram width is locked to 100% and scaled based on - * available space. If set to false, the diagram reserves its absolute width. - * - * Default value: true - */ - useMaxWidth: true, - - /** - * | Parameter | Description | Type | Required | Values | - * | ------------ | -------------------------------------------------------------------------------- | ------- | -------- | ------------------- | - * | textPosition | Axial position of slice's label from zero at the center to 1 at the outside edge | Number | Optional | Decimal from 0 to 1 | - * - * **Notes:** Default value: 0.75 - */ - textPosition: 0.75, - }, - - quadrantChart: { - /** - * | Parameter | Description | Type | Required | Values | - * | --------------- | ---------------------------------- | ------- | -------- | ------------------- | - * | chartWidth | Width of the chart | number | Optional | Any positive number | - * - * **Notes:** - * Default value: 500 - */ - chartWidth: 500, - /** - * | Parameter | Description | Type | Required | Values | - * | --------------- | ---------------------------------- | ------- | -------- | ------------------- | - * | chartHeight | Height of the chart | number | Optional | Any positive number | - * - * **Notes:** - * Default value: 500 - */ - chartHeight: 500, - /** - * | Parameter | Description | Type | Required | Values | - * | ------------------ | ---------------------------------- | ------- | -------- | ------------------- | - * | titlePadding | Chart title top and bottom padding | number | Optional | Any positive number | - * - * **Notes:** - * Default value: 10 - */ - titlePadding: 10, - /** - * | Parameter | Description | Type | Required | Values | - * | ------------------ | ---------------------------------- | ------- | -------- | ------------------- | - * | titleFontSize | Chart title font size | number | Optional | Any positive number | - * - * **Notes:** - * Default value: 20 - */ - titleFontSize: 20, - /** - * | Parameter | Description | Type | Required | Values | - * | --------------- | ---------------------------------- | ------- | -------- | ------------------- | - * | quadrantPadding | Padding around the quadrant square | number | Optional | Any positive number | - * - * **Notes:** - * Default value: 5 - */ - quadrantPadding: 5, - /** - * | Parameter | Description | Type | Required | Values | - * | ---------------------- | -------------------------------------------------------------------------- | ------- | -------- | ------------------- | - * | quadrantTextTopPadding | quadrant title padding from top if the quadrant is rendered on top | number | Optional | Any positive number | - * - * **Notes:** - * Default value: 5 - */ - quadrantTextTopPadding: 5, - /** - * | Parameter | Description | Type | Required | Values | - * | ------------------ | ---------------------------------- | ------- | -------- | ------------------- | - * | quadrantLabelFontSize | quadrant title font size | number | Optional | Any positive number | - * - * **Notes:** - * Default value: 16 - */ - quadrantLabelFontSize: 16, - /** - * | Parameter | Description | Type | Required | Values | - * | --------------------------------- | ------------------------------------------------------------- | ------- | -------- | ------------------- | - * | quadrantInternalBorderStrokeWidth | stroke width of edges of the box that are inside the quadrant | number | Optional | Any positive number | - * - * **Notes:** - * Default value: 1 - */ - quadrantInternalBorderStrokeWidth: 1, - /** - * | Parameter | Description | Type | Required | Values | - * | --------------------------------- | -------------------------------------------------------------- | ------- | -------- | ------------------- | - * | quadrantExternalBorderStrokeWidth | stroke width of edges of the box that are outside the quadrant | number | Optional | Any positive number | - * - * **Notes:** - * Default value: 2 - */ - quadrantExternalBorderStrokeWidth: 2, - /** - * | Parameter | Description | Type | Required | Values | - * | --------------- | ---------------------------------- | ------- | -------- | ------------------- | - * | xAxisLabelPadding | Padding around x-axis labels | number | Optional | Any positive number | - * - * **Notes:** - * Default value: 5 - */ - xAxisLabelPadding: 5, - /** - * | Parameter | Description | Type | Required | Values | - * | ------------------ | ---------------------------------- | ------- | -------- | ------------------- | - * | xAxisLabelFontSize | x-axis label font size | number | Optional | Any positive number | - * - * **Notes:** - * Default value: 16 - */ - xAxisLabelFontSize: 16, - /** - * | Parameter | Description | Type | Required | Values | - * | ------------- | ------------------------------- | ------- | -------- | ------------------- | - * | xAxisPosition | position of x-axis labels | string | Optional | 'top' or 'bottom' | - * - * **Notes:** - * Default value: top - */ - xAxisPosition: 'top', - /** - * | Parameter | Description | Type | Required | Values | - * | --------------- | ---------------------------------- | ------- | -------- | ------------------- | - * | yAxisLabelPadding | Padding around y-axis labels | number | Optional | Any positive number | - * - * **Notes:** - * Default value: 5 - */ - yAxisLabelPadding: 5, - /** - * | Parameter | Description | Type | Required | Values | - * | ------------------ | ---------------------------------- | ------- | -------- | ------------------- | - * | yAxisLabelFontSize | y-axis label font size | number | Optional | Any positive number | - * - * **Notes:** - * Default value: 16 - */ - yAxisLabelFontSize: 16, - /** - * | Parameter | Description | Type | Required | Values | - * | ------------- | ------------------------------- | ------- | -------- | ------------------- | - * | yAxisPosition | position of y-axis labels | string | Optional | 'left' or 'right' | - * - * **Notes:** - * Default value: left - */ - yAxisPosition: 'left', - /** - * | Parameter | Description | Type | Required | Values | - * | ---------------------- | -------------------------------------- | ------- | -------- | ------------------- | - * | pointTextPadding | padding between point and point label | number | Optional | Any positive number | - * - * **Notes:** - * Default value: 5 - */ - pointTextPadding: 5, - /** - * | Parameter | Description | Type | Required | Values | - * | ---------------------- | ---------------------- | ------- | -------- | ------------------- | - * | pointTextPadding | point title font size | number | Optional | Any positive number | - * - * **Notes:** - * Default value: 12 - */ - pointLabelFontSize: 12, - /** - * | Parameter | Description | Type | Required | Values | - * | ------------- | ------------------------------- | ------- | -------- | ------------------- | - * | pointRadius | radius of the point to be drawn | number | Optional | Any positive number | - * - * **Notes:** - * Default value: 5 - */ - pointRadius: 5, - /** - * | Parameter | Description | Type | Required | Values | - * | ----------- | ----------- | ------- | -------- | ----------- | - * | useMaxWidth | See Notes | boolean | Required | true, false | - * - * **Notes:** - * - * When this flag is set to true, the diagram width is locked to 100% and scaled based on - * available space. If set to false, the diagram reserves its absolute width. - * - * Default value: true - */ - useMaxWidth: true, - }, - - /** The object containing configurations specific for req diagrams */ - requirement: { - useWidth: undefined, - - /** - * | Parameter | Description | Type | Required | Values | - * | ----------- | ----------- | ------- | -------- | ----------- | - * | useMaxWidth | See Notes | boolean | Required | true, false | - * - * **Notes:** - * - * When this flag is set to true, the diagram width is locked to 100% and scaled based on - * available space. If set to false, the diagram reserves its absolute width. - * - * Default value: true - */ - useMaxWidth: true, - - rect_fill: '#f9f9f9', - text_color: '#333', - rect_border_size: '0.5px', - rect_border_color: '#bbb', - rect_min_width: 200, - rect_min_height: 200, - fontSize: 14, - rect_padding: 10, - line_height: 20, - }, - gitGraph: { - /** - * ### titleTopMargin - * - * | Parameter | Description | Type | Required | Values | - * | -------------- | ---------------------------------------------- | ------- | -------- | ------------------ | - * | titleTopMargin | Margin top for the text over the Git diagram | Integer | Required | Any Positive Value | - * - * **Notes:** Default value: 25 - */ - titleTopMargin: 25, - diagramPadding: 8, - nodeLabel: { - width: 75, - height: 100, - x: -25, - y: 0, - }, - mainBranchName: 'main', - mainBranchOrder: 0, - showCommitLabel: true, - showBranches: true, - rotateCommitLabel: true, - }, - - /** The object containing configurations specific for c4 diagrams */ c4: { + ...defaultConfigJson.c4, useWidth: undefined, - - /** - * | Parameter | Description | Type | Required | Values | - * | -------------- | ---------------------------------------------- | ------- | -------- | ------------------ | - * | diagramMarginX | Margin to the right and left of the c4 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 c4 diagram | Integer | Required | Any Positive Value | - * - * **Notes:** Default value: 10 - */ - diagramMarginY: 10, - - /** - * | Parameter | Description | Type | Required | Values | - * | ------------- | --------------------- | ------- | -------- | ------------------ | - * | c4ShapeMargin | Margin between shapes | Integer | Required | Any Positive Value | - * - * **Notes:** Default value: 50 - */ - c4ShapeMargin: 50, - - /** - * | Parameter | Description | Type | Required | Values | - * | -------------- | ---------------------- | ------- | -------- | ------------------ | - * | c4ShapePadding | Padding between shapes | Integer | Required | Any Positive Value | - * - * **Notes:** Default value: 20 - */ - c4ShapePadding: 20, - - /** - * | Parameter | Description | Type | Required | Values | - * | --------- | --------------------- | ------- | -------- | ------------------ | - * | width | Width of person boxes | Integer | Required | Any Positive Value | - * - * **Notes:** Default value: 216 - */ - width: 216, - - /** - * | Parameter | Description | Type | Required | Values | - * | --------- | ---------------------- | ------- | -------- | ------------------ | - * | height | Height of person boxes | Integer | Required | Any Positive Value | - * - * **Notes:** Default value: 60 - */ - height: 60, - - /** - * | Parameter | Description | Type | Required | Values | - * | --------- | ------------------- | ------- | -------- | ------------------ | - * | boxMargin | Margin around boxes | Integer | Required | Any Positive Value | - * - * **Notes:** Default value: 10 - */ - boxMargin: 10, - - /** - * | Parameter | Description | Type | Required | Values | - * | ----------- | ----------- | ------- | -------- | ----------- | - * | useMaxWidth | See Notes | boolean | Required | true, false | - * - * **Notes:** When this flag is set to true, the height and width is set to 100% and is then - * scaling with the available space. If set to false, the absolute space required is used. - * - * Default value: true - */ - useMaxWidth: true, - - /** - * | Parameter | Description | Type | Required | Values | - * | ------------ | ----------- | ------- | -------- | ------------------ | - * | c4ShapeInRow | See Notes | Integer | Required | Any Positive Value | - * - * **Notes:** How many shapes to place in each row. - * - * Default value: 4 - */ - c4ShapeInRow: 4, - - nextLinePaddingX: 0, - - /** - * | Parameter | Description | Type | Required | Values | - * | --------------- | ----------- | ------- | -------- | ------------------ | - * | c4BoundaryInRow | See Notes | Integer | Required | Any Positive Value | - * - * **Notes:** How many boundaries to place in each row. - * - * Default value: 2 - */ - c4BoundaryInRow: 2, - - /** - * This sets the font size of Person shape for the diagram - * - * **Notes:** Default value: 14. - */ - personFontSize: 14, - /** - * This sets the font family of Person shape for the diagram - * - * **Notes:** Default value: "Open Sans", sans-serif. - */ - personFontFamily: '"Open Sans", sans-serif', - /** - * This sets the font weight of Person shape for the diagram - * - * **Notes:** Default value: normal. - */ - personFontWeight: 'normal', - - /** - * This sets the font size of External Person shape for the diagram - * - * **Notes:** Default value: 14. - */ - external_personFontSize: 14, - /** - * This sets the font family of External Person shape for the diagram - * - * **Notes:** Default value: "Open Sans", sans-serif. - */ - external_personFontFamily: '"Open Sans", sans-serif', - /** - * This sets the font weight of External Person shape for the diagram - * - * **Notes:** Default value: normal. - */ - external_personFontWeight: 'normal', - - /** - * This sets the font size of System shape for the diagram - * - * **Notes:** Default value: 14. - */ - systemFontSize: 14, - /** - * This sets the font family of System shape for the diagram - * - * **Notes:** Default value: "Open Sans", sans-serif. - */ - systemFontFamily: '"Open Sans", sans-serif', - /** - * This sets the font weight of System shape for the diagram - * - * **Notes:** Default value: normal. - */ - systemFontWeight: 'normal', - - /** - * This sets the font size of External System shape for the diagram - * - * **Notes:** Default value: 14. - */ - external_systemFontSize: 14, - /** - * This sets the font family of External System shape for the diagram - * - * **Notes:** Default value: "Open Sans", sans-serif. - */ - external_systemFontFamily: '"Open Sans", sans-serif', - /** - * This sets the font weight of External System shape for the diagram - * - * **Notes:** Default value: normal. - */ - external_systemFontWeight: 'normal', - - /** - * This sets the font size of System DB shape for the diagram - * - * **Notes:** Default value: 14. - */ - system_dbFontSize: 14, - /** - * This sets the font family of System DB shape for the diagram - * - * **Notes:** Default value: "Open Sans", sans-serif. - */ - system_dbFontFamily: '"Open Sans", sans-serif', - /** - * This sets the font weight of System DB shape for the diagram - * - * **Notes:** Default value: normal. - */ - system_dbFontWeight: 'normal', - - /** - * This sets the font size of External System DB shape for the diagram - * - * **Notes:** Default value: 14. - */ - external_system_dbFontSize: 14, - /** - * This sets the font family of External System DB shape for the diagram - * - * **Notes:** Default value: "Open Sans", sans-serif. - */ - external_system_dbFontFamily: '"Open Sans", sans-serif', - /** - * This sets the font weight of External System DB shape for the diagram - * - * **Notes:** Default value: normal. - */ - external_system_dbFontWeight: 'normal', - - /** - * This sets the font size of System Queue shape for the diagram - * - * **Notes:** Default value: 14. - */ - system_queueFontSize: 14, - /** - * This sets the font family of System Queue shape for the diagram - * - * **Notes:** Default value: "Open Sans", sans-serif. - */ - system_queueFontFamily: '"Open Sans", sans-serif', - /** - * This sets the font weight of System Queue shape for the diagram - * - * **Notes:** Default value: normal. - */ - system_queueFontWeight: 'normal', - - /** - * This sets the font size of External System Queue shape for the diagram - * - * **Notes:** Default value: 14. - */ - external_system_queueFontSize: 14, - /** - * This sets the font family of External System Queue shape for the diagram - * - * **Notes:** Default value: "Open Sans", sans-serif. - */ - external_system_queueFontFamily: '"Open Sans", sans-serif', - /** - * This sets the font weight of External System Queue shape for the diagram - * - * **Notes:** Default value: normal. - */ - external_system_queueFontWeight: 'normal', - - /** - * This sets the font size of Boundary shape for the diagram - * - * **Notes:** Default value: 14. - */ - boundaryFontSize: 14, - /** - * This sets the font family of Boundary shape for the diagram - * - * **Notes:** Default value: "Open Sans", sans-serif. - */ - boundaryFontFamily: '"Open Sans", sans-serif', - /** - * This sets the font weight of Boundary shape for the diagram - * - * **Notes:** Default value: normal. - */ - boundaryFontWeight: 'normal', - - /** - * This sets the font size of Message shape for the diagram - * - * **Notes:** Default value: 12. - */ - messageFontSize: 12, - /** - * This sets the font family of Message shape for the diagram - * - * **Notes:** Default value: "Open Sans", sans-serif. - */ - messageFontFamily: '"Open Sans", sans-serif', - /** - * This sets the font weight of Message shape for the diagram - * - * **Notes:** Default value: normal. - */ - messageFontWeight: 'normal', - - /** - * This sets the font size of Container shape for the diagram - * - * **Notes:** Default value: 14. - */ - containerFontSize: 14, - /** - * This sets the font family of Container shape for the diagram - * - * **Notes:** Default value: "Open Sans", sans-serif. - */ - containerFontFamily: '"Open Sans", sans-serif', - /** - * This sets the font weight of Container shape for the diagram - * - * **Notes:** Default value: normal. - */ - containerFontWeight: 'normal', - - /** - * This sets the font size of External Container shape for the diagram - * - * **Notes:** Default value: 14. - */ - external_containerFontSize: 14, - /** - * This sets the font family of External Container shape for the diagram - * - * **Notes:** Default value: "Open Sans", sans-serif. - */ - external_containerFontFamily: '"Open Sans", sans-serif', - /** - * This sets the font weight of External Container shape for the diagram - * - * **Notes:** Default value: normal. - */ - external_containerFontWeight: 'normal', - - /** - * This sets the font size of Container DB shape for the diagram - * - * **Notes:** Default value: 14. - */ - container_dbFontSize: 14, - /** - * This sets the font family of Container DB shape for the diagram - * - * **Notes:** Default value: "Open Sans", sans-serif. - */ - container_dbFontFamily: '"Open Sans", sans-serif', - /** - * This sets the font weight of Container DB shape for the diagram - * - * **Notes:** Default value: normal. - */ - container_dbFontWeight: 'normal', - - /** - * This sets the font size of External Container DB shape for the diagram - * - * **Notes:** Default value: 14. - */ - external_container_dbFontSize: 14, - /** - * This sets the font family of External Container DB shape for the diagram - * - * **Notes:** Default value: "Open Sans", sans-serif. - */ - external_container_dbFontFamily: '"Open Sans", sans-serif', - /** - * This sets the font weight of External Container DB shape for the diagram - * - * **Notes:** Default value: normal. - */ - external_container_dbFontWeight: 'normal', - - /** - * This sets the font size of Container Queue shape for the diagram - * - * **Notes:** Default value: 14. - */ - container_queueFontSize: 14, - /** - * This sets the font family of Container Queue shape for the diagram - * - * **Notes:** Default value: "Open Sans", sans-serif. - */ - container_queueFontFamily: '"Open Sans", sans-serif', - /** - * This sets the font weight of Container Queue shape for the diagram - * - * **Notes:** Default value: normal. - */ - container_queueFontWeight: 'normal', - - /** - * This sets the font size of External Container Queue shape for the diagram - * - * **Notes:** Default value: 14. - */ - external_container_queueFontSize: 14, - /** - * This sets the font family of External Container Queue shape for the diagram - * - * **Notes:** Default value: "Open Sans", sans-serif. - */ - external_container_queueFontFamily: '"Open Sans", sans-serif', - /** - * This sets the font weight of External Container Queue shape for the diagram - * - * **Notes:** Default value: normal. - */ - external_container_queueFontWeight: 'normal', - - /** - * This sets the font size of Component shape for the diagram - * - * **Notes:** Default value: 14. - */ - componentFontSize: 14, - /** - * This sets the font family of Component shape for the diagram - * - * **Notes:** Default value: "Open Sans", sans-serif. - */ - componentFontFamily: '"Open Sans", sans-serif', - /** - * This sets the font weight of Component shape for the diagram - * - * **Notes:** Default value: normal. - */ - componentFontWeight: 'normal', - - /** - * This sets the font size of External Component shape for the diagram - * - * **Notes:** Default value: 14. - */ - external_componentFontSize: 14, - /** - * This sets the font family of External Component shape for the diagram - * - * **Notes:** Default value: "Open Sans", sans-serif. - */ - external_componentFontFamily: '"Open Sans", sans-serif', - /** - * This sets the font weight of External Component shape for the diagram - * - * **Notes:** Default value: normal. - */ - external_componentFontWeight: 'normal', - - /** - * This sets the font size of Component DB shape for the diagram - * - * **Notes:** Default value: 14. - */ - component_dbFontSize: 14, - /** - * This sets the font family of Component DB shape for the diagram - * - * **Notes:** Default value: "Open Sans", sans-serif. - */ - component_dbFontFamily: '"Open Sans", sans-serif', - /** - * This sets the font weight of Component DB shape for the diagram - * - * **Notes:** Default value: normal. - */ - component_dbFontWeight: 'normal', - - /** - * This sets the font size of External Component DB shape for the diagram - * - * **Notes:** Default value: 14. - */ - external_component_dbFontSize: 14, - /** - * This sets the font family of External Component DB shape for the diagram - * - * **Notes:** Default value: "Open Sans", sans-serif. - */ - external_component_dbFontFamily: '"Open Sans", sans-serif', - /** - * This sets the font weight of External Component DB shape for the diagram - * - * **Notes:** Default value: normal. - */ - external_component_dbFontWeight: 'normal', - - /** - * This sets the font size of Component Queue shape for the diagram - * - * **Notes:** Default value: 14. - */ - component_queueFontSize: 14, - /** - * This sets the font family of Component Queue shape for the diagram - * - * **Notes:** Default value: "Open Sans", sans-serif. - */ - component_queueFontFamily: '"Open Sans", sans-serif', - /** - * This sets the font weight of Component Queue shape for the diagram - * - * **Notes:** Default value: normal. - */ - component_queueFontWeight: 'normal', - - /** - * This sets the font size of External Component Queue shape for the diagram - * - * **Notes:** Default value: 14. - */ - external_component_queueFontSize: 14, - /** - * This sets the font family of External Component Queue shape for the diagram - * - * **Notes:** Default value: "Open Sans", sans-serif. - */ - external_component_queueFontFamily: '"Open Sans", sans-serif', - /** - * This sets the font weight of External Component Queue shape for the diagram - * - * **Notes:** Default value: normal. - */ - external_component_queueFontWeight: 'normal', - - /** - * This sets the auto-wrap state for the diagram - * - * **Notes:** Default value: true. - */ - wrap: true, - - /** - * This sets the auto-wrap padding for the diagram (sides only) - * - * **Notes:** Default value: 0. - */ - wrapPadding: 10, - personFont: function () { return { fontFamily: this.personFontFamily, @@ -2229,65 +231,30 @@ const config: Partial = { fontWeight: this.messageFontWeight, }; }, - - // ' Colors - // ' ################################## - person_bg_color: '#08427B', - person_border_color: '#073B6F', - external_person_bg_color: '#686868', - external_person_border_color: '#8A8A8A', - system_bg_color: '#1168BD', - system_border_color: '#3C7FC0', - system_db_bg_color: '#1168BD', - system_db_border_color: '#3C7FC0', - system_queue_bg_color: '#1168BD', - system_queue_border_color: '#3C7FC0', - external_system_bg_color: '#999999', - external_system_border_color: '#8A8A8A', - external_system_db_bg_color: '#999999', - external_system_db_border_color: '#8A8A8A', - external_system_queue_bg_color: '#999999', - external_system_queue_border_color: '#8A8A8A', - container_bg_color: '#438DD5', - container_border_color: '#3C7FC0', - container_db_bg_color: '#438DD5', - container_db_border_color: '#3C7FC0', - container_queue_bg_color: '#438DD5', - container_queue_border_color: '#3C7FC0', - external_container_bg_color: '#B3B3B3', - external_container_border_color: '#A6A6A6', - external_container_db_bg_color: '#B3B3B3', - external_container_db_border_color: '#A6A6A6', - external_container_queue_bg_color: '#B3B3B3', - external_container_queue_border_color: '#A6A6A6', - component_bg_color: '#85BBF0', - component_border_color: '#78A8D8', - component_db_bg_color: '#85BBF0', - component_db_border_color: '#78A8D8', - component_queue_bg_color: '#85BBF0', - component_queue_border_color: '#78A8D8', - external_component_bg_color: '#CCCCCC', - external_component_border_color: '#BFBFBF', - external_component_db_bg_color: '#CCCCCC', - external_component_db_border_color: '#BFBFBF', - external_component_queue_bg_color: '#CCCCCC', - external_component_queue_border_color: '#BFBFBF', }, - mindmap: { - useMaxWidth: true, - padding: 10, - maxNodeWidth: 200, + pie: { + ...defaultConfigJson.pie, + useWidth: 984, + }, + requirement: { + ...defaultConfigJson.requirement, + useWidth: undefined, + }, + gitGraph: { + ...defaultConfigJson.gitGraph, + // TODO: This is a temporary override for `gitGraph`, since every other + // diagram does have `useMaxWidth`, but instead sets it to `true`. + // Should we set this to `true` instead? + useMaxWidth: false, + }, + sankey: { + ...defaultConfigJson.sankey, + // this is false, unlike every other diagram (other than gitGraph) + // TODO: can we make this default to `true` instead? + useMaxWidth: false, }, - fontSize: 16, }; -if (config.class) { - config.class.arrowMarkerAbsolute = config.arrowMarkerAbsolute; -} -if (config.gitGraph) { - config.gitGraph.arrowMarkerAbsolute = config.arrowMarkerAbsolute; -} - const keyify = (obj: any, prefix = ''): string[] => Object.keys(obj).reduce((res: string[], el): string[] => { if (Array.isArray(obj[el])) { @@ -2298,5 +265,5 @@ const keyify = (obj: any, prefix = ''): string[] => return [...res, prefix + el]; }, []); -export const configKeys: string[] = keyify(config, ''); +export const configKeys: Set = new Set(keyify(config, '')); export default config; diff --git a/packages/mermaid/src/diagram-api/detectType.ts b/packages/mermaid/src/diagram-api/detectType.ts index 8351a67dff..ba78dbe55e 100644 --- a/packages/mermaid/src/diagram-api/detectType.ts +++ b/packages/mermaid/src/diagram-api/detectType.ts @@ -1,4 +1,4 @@ -import { MermaidConfig } from '../config.type.js'; +import type { MermaidConfig } from '../config.type.js'; import { log } from '../logger.js'; import type { DetectorRecord, @@ -6,14 +6,10 @@ import type { DiagramLoader, ExternalDiagramDefinition, } from './types.js'; -import { frontMatterRegex } from './frontmatter.js'; -import { getDiagram, registerDiagram } from './diagramAPI.js'; +import { anyCommentRegex, directiveRegex, frontMatterRegex } from './regexes.js'; import { UnknownDiagramError } from '../errors.js'; -const directive = /%{2}{\s*(?:(\w+)\s*:|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi; -const anyComment = /\s*%%.*\n/gm; - -const detectors: Record = {}; +export const detectors: Record = {}; /** * Detects the type of the graph text. @@ -38,7 +34,10 @@ const detectors: Record = {}; * @returns A graph definition key */ export const detectType = function (text: string, config?: MermaidConfig): string { - text = text.replace(frontMatterRegex, '').replace(directive, '').replace(anyComment, '\n'); + text = text + .replace(frontMatterRegex, '') + .replace(directiveRegex, '') + .replace(anyCommentRegex, '\n'); for (const [key, { detector }] of Object.entries(detectors)) { const diagram = detector(text, config); if (diagram) { @@ -70,39 +69,6 @@ export const registerLazyLoadedDiagrams = (...diagrams: ExternalDiagramDefinitio } }; -export const loadRegisteredDiagrams = async () => { - log.debug(`Loading registered diagrams`); - // Load all lazy loaded diagrams in parallel - const results = await Promise.allSettled( - Object.entries(detectors).map(async ([key, { detector, loader }]) => { - if (loader) { - try { - getDiagram(key); - } catch (error) { - try { - // Register diagram if it is not already registered - const { diagram, id } = await loader(); - registerDiagram(id, diagram, detector); - } catch (err) { - // Remove failed diagram from detectors - log.error(`Failed to load external diagram with key ${key}. Removing from detectors.`); - delete detectors[key]; - throw err; - } - } - } - }) - ); - const failed = results.filter((result) => result.status === 'rejected'); - if (failed.length > 0) { - log.error(`Failed to load ${failed.length} external diagrams`); - for (const res of failed) { - log.error(res); - } - throw new Error(`Failed to load ${failed.length} external diagrams`); - } -}; - export const addDetector = (key: string, detector: DiagramDetector, loader?: DiagramLoader) => { if (detectors[key]) { log.error(`Detector with key ${key} already exists`); diff --git a/packages/mermaid/src/diagram-api/diagram-orchestration.ts b/packages/mermaid/src/diagram-api/diagram-orchestration.ts index 0253be45d5..80665cfa2e 100644 --- a/packages/mermaid/src/diagram-api/diagram-orchestration.ts +++ b/packages/mermaid/src/diagram-api/diagram-orchestration.ts @@ -5,7 +5,7 @@ import er from '../diagrams/er/erDetector.js'; import git from '../diagrams/git/gitGraphDetector.js'; import gantt from '../diagrams/gantt/ganttDetector.js'; import { info } from '../diagrams/info/infoDetector.js'; -import pie from '../diagrams/pie/pieDetector.js'; +import { pie } from '../diagrams/pie/pieDetector.js'; import quadrantChart from '../diagrams/quadrant-chart/quadrantDetector.js'; import requirement from '../diagrams/requirement/requirementDetector.js'; import sequence from '../diagrams/sequence/sequenceDetector.js'; @@ -18,6 +18,7 @@ import errorDiagram from '../diagrams/error/errorDiagram.js'; import flowchartElk from '../diagrams/flowchart/elk/detector.js'; import timeline from '../diagrams/timeline/detector.js'; import mindmap from '../diagrams/mindmap/detector.js'; +import sankey from '../diagrams/sankey/sankeyDetector.js'; import { registerLazyLoadedDiagrams } from './detectType.js'; import { registerDiagram } from './diagramAPI.js'; @@ -79,6 +80,7 @@ export const addDiagrams = () => { stateV2, state, journey, - quadrantChart + quadrantChart, + sankey ); }; diff --git a/packages/mermaid/src/diagram-api/diagramAPI.spec.ts b/packages/mermaid/src/diagram-api/diagramAPI.spec.ts index 49bde1a66f..b82011f8de 100644 --- a/packages/mermaid/src/diagram-api/diagramAPI.spec.ts +++ b/packages/mermaid/src/diagram-api/diagramAPI.spec.ts @@ -1,7 +1,7 @@ import { detectType } from './detectType.js'; import { getDiagram, registerDiagram } from './diagramAPI.js'; import { addDiagrams } from './diagram-orchestration.js'; -import { DiagramDetector } from './types.js'; +import type { DiagramDetector } from './types.js'; import { getDiagramFromText } from '../Diagram.js'; import { it, describe, expect, beforeAll } from 'vitest'; @@ -35,7 +35,12 @@ describe('DiagramAPI', () => { 'loki', { db: {}, - parser: {}, + parser: { + parse: (_text) => { + return; + }, + parser: { yy: {} }, + }, renderer: {}, styles: {}, }, diff --git a/packages/mermaid/src/diagram-api/diagramAPI.ts b/packages/mermaid/src/diagram-api/diagramAPI.ts index 457dd673ba..6f1421527a 100644 --- a/packages/mermaid/src/diagram-api/diagramAPI.ts +++ b/packages/mermaid/src/diagram-api/diagramAPI.ts @@ -4,10 +4,9 @@ import { getConfig as _getConfig } from '../config.js'; import { sanitizeText as _sanitizeText } from '../diagrams/common/common.js'; import { setupGraphViewbox as _setupGraphViewbox } from '../setupGraphViewbox.js'; import { addStylesForDiagram } from '../styles.js'; -import { DiagramDefinition, DiagramDetector } from './types.js'; -import * as _commonDb from '../commonDb.js'; +import type { DiagramDefinition, DiagramDetector } from './types.js'; +import * as _commonDb from '../diagrams/common/commonDb.js'; import { parseDirective as _parseDirective } from '../directiveUtils.js'; -import isEmpty from 'lodash-es/isEmpty.js'; /* Packaging and exposing resources for external diagrams so that they can import @@ -51,9 +50,7 @@ export const registerDiagram = ( if (detector) { addDetector(id, detector); } - if (!isEmpty(diagram.styles)) { - addStylesForDiagram(id, diagram.styles); - } + addStylesForDiagram(id, diagram.styles); if (diagram.injectUtils) { diagram.injectUtils( @@ -72,11 +69,11 @@ export const getDiagram = (name: string): DiagramDefinition => { if (name in diagrams) { return diagrams[name]; } - throw new Error(`Diagram ${name} not found.`); + throw new DiagramNotFoundError(name); }; export class DiagramNotFoundError extends Error { - constructor(message: string) { - super(`Diagram ${message} not found.`); + constructor(name: string) { + super(`Diagram ${name} not found.`); } } diff --git a/packages/mermaid/src/diagram-api/frontmatter.spec.ts b/packages/mermaid/src/diagram-api/frontmatter.spec.ts index ef05c8f149..03d46c300c 100644 --- a/packages/mermaid/src/diagram-api/frontmatter.spec.ts +++ b/packages/mermaid/src/diagram-api/frontmatter.spec.ts @@ -2,8 +2,13 @@ import { vi } from 'vitest'; import { extractFrontMatter } from './frontmatter.js'; const dbMock = () => ({ setDiagramTitle: vi.fn() }); +const setConfigMock = vi.fn(); describe('extractFrontmatter', () => { + beforeEach(() => { + setConfigMock.mockClear(); + }); + it('returns text unchanged if no frontmatter', () => { expect(extractFrontMatter('diagram', dbMock())).toEqual('diagram'); }); @@ -75,4 +80,21 @@ describe('extractFrontmatter', () => { 'tag suffix cannot contain exclamation marks' ); }); + + it('handles frontmatter with config', () => { + const text = `--- +title: hello +config: + graph: + string: hello + number: 14 + boolean: false + array: [1, 2, 3] +--- +diagram`; + expect(extractFrontMatter(text, {}, setConfigMock)).toEqual('diagram'); + expect(setConfigMock).toHaveBeenCalledWith({ + graph: { string: 'hello', number: 14, boolean: false, array: [1, 2, 3] }, + }); + }); }); diff --git a/packages/mermaid/src/diagram-api/frontmatter.ts b/packages/mermaid/src/diagram-api/frontmatter.ts index f8d2e9c415..0fd2917eae 100644 --- a/packages/mermaid/src/diagram-api/frontmatter.ts +++ b/packages/mermaid/src/diagram-api/frontmatter.ts @@ -1,46 +1,53 @@ -import { DiagramDB } from './types.js'; +import type { MermaidConfig } from '../config.type.js'; +import { frontMatterRegex } from './regexes.js'; +import type { DiagramDB } from './types.js'; // The "* as yaml" part is necessary for tree-shaking import * as yaml from 'js-yaml'; -// Match Jekyll-style front matter blocks (https://jekyllrb.com/docs/front-matter/). -// Based on regex used by Jekyll: https://github.com/jekyll/jekyll/blob/6dd3cc21c40b98054851846425af06c64f9fb466/lib/jekyll/document.rb#L10 -// Note that JS doesn't support the "\A" anchor, which means we can't use -// multiline mode. -// Relevant YAML spec: https://yaml.org/spec/1.2.2/#914-explicit-documents -export const frontMatterRegex = /^-{3}\s*[\n\r](.*?)[\n\r]-{3}\s*[\n\r]+/s; - -type FrontMatterMetadata = { +interface FrontMatterMetadata { title?: string; // Allows custom display modes. Currently used for compact mode in gantt charts. displayMode?: string; -}; + config?: MermaidConfig; +} /** * Extract and parse frontmatter from text, if present, and sets appropriate * properties in the provided db. * @param text - The text that may have a YAML frontmatter. * @param db - Diagram database, could be of any diagram. + * @param setDiagramConfig - Optional function to set diagram config. * @returns text with frontmatter stripped out */ -export function extractFrontMatter(text: string, db: DiagramDB): string { +export function extractFrontMatter( + text: string, + db: DiagramDB, + setDiagramConfig?: (config: MermaidConfig) => void +): string { const matches = text.match(frontMatterRegex); - if (matches) { - const parsed: FrontMatterMetadata = yaml.load(matches[1], { - // To keep things simple, only allow strings, arrays, and plain objects. - // https://www.yaml.org/spec/1.2/spec.html#id2802346 - schema: yaml.FAILSAFE_SCHEMA, - }) as FrontMatterMetadata; + if (!matches) { + return text; + } - if (parsed?.title) { - db.setDiagramTitle?.(parsed.title); - } + const parsed: FrontMatterMetadata = yaml.load(matches[1], { + // To support config, we need JSON schema. + // https://www.yaml.org/spec/1.2/spec.html#id2803231 + schema: yaml.JSON_SCHEMA, + }) as FrontMatterMetadata; - if (parsed?.displayMode) { - db.setDisplayMode?.(parsed.displayMode); - } + if (parsed?.title) { + // toString() is necessary because YAML could parse the title as a number/boolean + db.setDiagramTitle?.(parsed.title.toString()); + } - return text.slice(matches[0].length); - } else { - return text; + if (parsed?.displayMode) { + // toString() is necessary because YAML could parse the title as a number/boolean + db.setDisplayMode?.(parsed.displayMode.toString()); + } + + if (parsed?.config) { + setDiagramConfig?.(parsed.config); } + + return text.slice(matches[0].length); } diff --git a/packages/mermaid/src/diagram-api/loadDiagram.ts b/packages/mermaid/src/diagram-api/loadDiagram.ts new file mode 100644 index 0000000000..c1b445bf64 --- /dev/null +++ b/packages/mermaid/src/diagram-api/loadDiagram.ts @@ -0,0 +1,36 @@ +import { log } from '../logger.js'; +import { detectors } from './detectType.js'; +import { getDiagram, registerDiagram } from './diagramAPI.js'; + +export const loadRegisteredDiagrams = async () => { + log.debug(`Loading registered diagrams`); + // Load all lazy loaded diagrams in parallel + const results = await Promise.allSettled( + Object.entries(detectors).map(async ([key, { detector, loader }]) => { + if (loader) { + try { + getDiagram(key); + } catch (error) { + try { + // Register diagram if it is not already registered + const { diagram, id } = await loader(); + registerDiagram(id, diagram, detector); + } catch (err) { + // Remove failed diagram from detectors + log.error(`Failed to load external diagram with key ${key}. Removing from detectors.`); + delete detectors[key]; + throw err; + } + } + } + }) + ); + const failed = results.filter((result) => result.status === 'rejected'); + if (failed.length > 0) { + log.error(`Failed to load ${failed.length} external diagrams`); + for (const res of failed) { + log.error(res); + } + throw new Error(`Failed to load ${failed.length} external diagrams`); + } +}; diff --git a/packages/mermaid/src/diagram-api/regexes.ts b/packages/mermaid/src/diagram-api/regexes.ts new file mode 100644 index 0000000000..bb688b9c2a --- /dev/null +++ b/packages/mermaid/src/diagram-api/regexes.ts @@ -0,0 +1,11 @@ +// Match Jekyll-style front matter blocks (https://jekyllrb.com/docs/front-matter/). +// Based on regex used by Jekyll: https://github.com/jekyll/jekyll/blob/6dd3cc21c40b98054851846425af06c64f9fb466/lib/jekyll/document.rb#L10 +// Note that JS doesn't support the "\A" anchor, which means we can't use +// multiline mode. +// Relevant YAML spec: https://yaml.org/spec/1.2.2/#914-explicit-documents +export const frontMatterRegex = /^-{3}\s*[\n\r](.*?)[\n\r]-{3}\s*[\n\r]+/s; + +export const directiveRegex = + /%{2}{\s*(?:(\w+)\s*:|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi; + +export const anyCommentRegex = /\s*%%.*\n/gm; diff --git a/packages/mermaid/src/diagram-api/types.ts b/packages/mermaid/src/diagram-api/types.ts index 265af65874..2ac7fba127 100644 --- a/packages/mermaid/src/diagram-api/types.ts +++ b/packages/mermaid/src/diagram-api/types.ts @@ -1,5 +1,5 @@ -import { Diagram } from '../Diagram.js'; -import { MermaidConfig } from '../config.type.js'; +import type { Diagram } from '../Diagram.js'; +import type { BaseDiagramConfig, MermaidConfig } from '../config.type.js'; import type * as d3 from 'd3'; export interface InjectUtils { @@ -16,18 +16,26 @@ export interface InjectUtils { * Generic Diagram DB that may apply to any diagram type. */ export interface DiagramDB { + // config + getConfig?: () => BaseDiagramConfig | undefined; + + // db clear?: () => void; setDiagramTitle?: (title: string) => void; - setDisplayMode?: (title: string) => void; + getDiagramTitle?: () => string; + setAccTitle?: (title: string) => void; getAccTitle?: () => string; + setAccDescription?: (describetion: string) => void; getAccDescription?: () => string; + + setDisplayMode?: (title: string) => void; bindFunctions?: (element: Element) => void; } export interface DiagramDefinition { db: DiagramDB; renderer: any; - parser: any; + parser: ParserDefinition; styles?: any; init?: (config: MermaidConfig) => void; injectUtils?: ( @@ -70,6 +78,11 @@ export type DrawDefinition = ( diagramObject: Diagram ) => void; +export interface ParserDefinition { + parse: (text: string) => void; + parser: { yy: DiagramDB }; +} + /** * Type for function parse directive from diagram code. * @@ -79,6 +92,10 @@ export type DrawDefinition = ( */ export type ParseDirectiveDefinition = (statement: string, context: string, type: string) => void; -export type HTML = d3.Selection; +export type HTML = d3.Selection; + +export type SVG = d3.Selection; + +export type Group = d3.Selection; -export type SVG = d3.Selection; +export type DiagramStylesProvider = (options?: any) => string; diff --git a/packages/mermaid/src/diagram.spec.ts b/packages/mermaid/src/diagram.spec.ts index aa613d8e52..99ce4e2c66 100644 --- a/packages/mermaid/src/diagram.spec.ts +++ b/packages/mermaid/src/diagram.spec.ts @@ -49,7 +49,7 @@ describe('diagram detection', () => { "Parse error on line 2: graph TD; A--> --------------^ - Expecting 'AMP', 'ALPHA', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'MINUS', 'BRKT', 'DOT', 'PUNCTUATION', 'UNICODE_TEXT', 'PLUS', 'EQUALS', 'MULT', 'UNDERSCORE', got 'EOF'" + Expecting 'AMP', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'EOF'" `); await expect(getDiagramFromText('sequenceDiagram; A-->B')).rejects .toThrowErrorMatchingInlineSnapshot(` diff --git a/packages/mermaid/src/diagrams/c4/c4Db.js b/packages/mermaid/src/diagrams/c4/c4Db.js index 09dcc13cda..7c450940f6 100644 --- a/packages/mermaid/src/diagrams/c4/c4Db.js +++ b/packages/mermaid/src/diagrams/c4/c4Db.js @@ -1,7 +1,12 @@ import mermaidAPI from '../../mermaidAPI.js'; import * as configApi from '../../config.js'; import { sanitizeText } from '../common/common.js'; -import { setAccTitle, getAccTitle, getAccDescription, setAccDescription } from '../../commonDb.js'; +import { + setAccTitle, + getAccTitle, + getAccDescription, + setAccDescription, +} from '../common/commonDb.js'; let c4ShapeArray = []; let boundaryParseStack = ['']; diff --git a/packages/mermaid/src/diagrams/c4/c4Diagram.ts b/packages/mermaid/src/diagrams/c4/c4Diagram.ts index 04567fafa3..4c578b6246 100644 --- a/packages/mermaid/src/diagrams/c4/c4Diagram.ts +++ b/packages/mermaid/src/diagrams/c4/c4Diagram.ts @@ -1,10 +1,10 @@ -// @ts-ignore: TODO Fix ts errors +// @ts-ignore: JISON doesn't support types import c4Parser from './parser/c4Diagram.jison'; import c4Db from './c4Db.js'; import c4Renderer from './c4Renderer.js'; import c4Styles from './styles.js'; -import { MermaidConfig } from '../../config.type.js'; -import { DiagramDefinition } from '../../diagram-api/types.js'; +import type { MermaidConfig } from '../../config.type.js'; +import type { DiagramDefinition } from '../../diagram-api/types.js'; export const diagram: DiagramDefinition = { parser: c4Parser, diff --git a/packages/mermaid/src/diagrams/c4/parser/c4Container.spec.js b/packages/mermaid/src/diagrams/c4/parser/c4Container.spec.js new file mode 100644 index 0000000000..76dca3bc2a --- /dev/null +++ b/packages/mermaid/src/diagrams/c4/parser/c4Container.spec.js @@ -0,0 +1,135 @@ +import c4Db from '../c4Db.js'; +import c4 from './c4Diagram.jison'; +import { setConfig } from '../../../config.js'; + +setConfig({ + securityLevel: 'strict', +}); + +describe.each([ + ['Container', 'container'], + ['ContainerDb', 'container_db'], + ['ContainerQueue', 'container_queue'], + ['Container_Ext', 'external_container'], + ['ContainerDb_Ext', 'external_container_db'], + ['ContainerQueue_Ext', 'external_container_queue'], +])('parsing a C4 %s', function (macroName, elementName) { + beforeEach(function () { + c4.parser.yy = c4Db; + c4.parser.yy.clear(); + }); + + it('should parse a C4 diagram with one Container correctly', function () { + c4.parser.parse(`C4Context +title Container diagram for Internet Banking Container +${macroName}(ContainerAA, "Internet Banking Container", "Technology", "Allows customers to view information about their bank accounts, and make payments.")`); + + const yy = c4.parser.yy; + + const shapes = yy.getC4ShapeArray(); + expect(shapes.length).toBe(1); + const onlyShape = shapes[0]; + + expect(onlyShape).toEqual({ + alias: 'ContainerAA', + descr: { + text: 'Allows customers to view information about their bank accounts, and make payments.', + }, + label: { + text: 'Internet Banking Container', + }, + link: undefined, + sprite: undefined, + tags: undefined, + parentBoundary: 'global', + typeC4Shape: { + text: elementName, + }, + techn: { + text: 'Technology', + }, + wrap: false, + }); + }); + + it('should parse the alias', function () { + c4.parser.parse(`C4Context +${macroName}(ContainerAA, "Internet Banking Container")`); + + expect(c4.parser.yy.getC4ShapeArray()[0]).toMatchObject({ + alias: 'ContainerAA', + }); + }); + + it('should parse the label', function () { + c4.parser.parse(`C4Context +${macroName}(ContainerAA, "Internet Banking Container")`); + + expect(c4.parser.yy.getC4ShapeArray()[0]).toMatchObject({ + label: { + text: 'Internet Banking Container', + }, + }); + }); + + it('should parse the technology', function () { + c4.parser.parse(`C4Context +${macroName}(ContainerAA, "", "Java")`); + + expect(c4.parser.yy.getC4ShapeArray()[0]).toMatchObject({ + techn: { + text: 'Java', + }, + }); + }); + + it('should parse the description', function () { + c4.parser.parse(`C4Context +${macroName}(ContainerAA, "", "", "Allows customers to view information about their bank accounts, and make payments.")`); + + expect(c4.parser.yy.getC4ShapeArray()[0]).toMatchObject({ + descr: { + text: 'Allows customers to view information about their bank accounts, and make payments.', + }, + }); + }); + + it('should parse a sprite', function () { + c4.parser.parse(`C4Context +${macroName}(ContainerAA, $sprite="users")`); + + expect(c4.parser.yy.getC4ShapeArray()[0]).toMatchObject({ + label: { + text: { + sprite: 'users', + }, + }, + }); + }); + + it('should parse a link', function () { + c4.parser.parse(`C4Context +${macroName}(ContainerAA, $link="https://github.com/mermaidjs")`); + + expect(c4.parser.yy.getC4ShapeArray()[0]).toMatchObject({ + label: { + text: { + link: 'https://github.com/mermaidjs', + }, + }, + }); + }); + + it('should parse tags', function () { + c4.parser.parse(`C4Context +${macroName}(ContainerAA, $tags="tag1,tag2")`); + + expect(c4.parser.yy.getC4ShapeArray()[0]).toMatchObject({ + label: { + text: { + tags: 'tag1,tag2', + }, + }, + }); + }); +}); diff --git a/packages/mermaid/src/diagrams/c4/parser/c4Diagram.jison b/packages/mermaid/src/diagrams/c4/parser/c4Diagram.jison index 03b851458a..1dfa69ef1f 100644 --- a/packages/mermaid/src/diagrams/c4/parser/c4Diagram.jison +++ b/packages/mermaid/src/diagrams/c4/parser/c4Diagram.jison @@ -150,27 +150,27 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multiline");} "Node_R" { this.begin("node_r"); return 'NODE_R';} -"Rel" { this.begin("rel"); return 'REL';} -"BiRel" { this.begin("birel"); return 'BIREL';} -"Rel_Up" { this.begin("rel_u"); return 'REL_U';} -"Rel_U" { this.begin("rel_u"); return 'REL_U';} -"Rel_Down" { this.begin("rel_d"); return 'REL_D';} -"Rel_D" { this.begin("rel_d"); return 'REL_D';} -"Rel_Left" { this.begin("rel_l"); return 'REL_L';} -"Rel_L" { this.begin("rel_l"); return 'REL_L';} -"Rel_Right" { this.begin("rel_r"); return 'REL_R';} -"Rel_R" { this.begin("rel_r"); return 'REL_R';} -"Rel_Back" { this.begin("rel_b"); return 'REL_B';} -"RelIndex" { this.begin("rel_index"); return 'REL_INDEX';} - -"UpdateElementStyle" { this.begin("update_el_style"); return 'UPDATE_EL_STYLE';} -"UpdateRelStyle" { this.begin("update_rel_style"); return 'UPDATE_REL_STYLE';} -"UpdateLayoutConfig" { this.begin("update_layout_config"); return 'UPDATE_LAYOUT_CONFIG';} - -<> return "EOF_IN_STRUCT"; -[(][ ]*[,] { this.begin("attribute"); return "ATTRIBUTE_EMPTY";} -[(] { this.begin("attribute"); } -[)] { this.popState();this.popState();} +"Rel" { this.begin("rel"); return 'REL';} +"BiRel" { this.begin("birel"); return 'BIREL';} +"Rel_Up" { this.begin("rel_u"); return 'REL_U';} +"Rel_U" { this.begin("rel_u"); return 'REL_U';} +"Rel_Down" { this.begin("rel_d"); return 'REL_D';} +"Rel_D" { this.begin("rel_d"); return 'REL_D';} +"Rel_Left" { this.begin("rel_l"); return 'REL_L';} +"Rel_L" { this.begin("rel_l"); return 'REL_L';} +"Rel_Right" { this.begin("rel_r"); return 'REL_R';} +"Rel_R" { this.begin("rel_r"); return 'REL_R';} +"Rel_Back" { this.begin("rel_b"); return 'REL_B';} +"RelIndex" { this.begin("rel_index"); return 'REL_INDEX';} + +"UpdateElementStyle" { this.begin("update_el_style"); return 'UPDATE_EL_STYLE';} +"UpdateRelStyle" { this.begin("update_rel_style"); return 'UPDATE_REL_STYLE';} +"UpdateLayoutConfig" { this.begin("update_layout_config"); return 'UPDATE_LAYOUT_CONFIG';} + +<> return "EOF_IN_STRUCT"; +[(][ ]*[,] { this.begin("attribute"); return "ATTRIBUTE_EMPTY";} +[(] { this.begin("attribute"); } +[)] { this.popState();this.popState();} ",," { return 'ATTRIBUTE_EMPTY';} "," { } @@ -189,7 +189,7 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multiline");} '{' { /* this.begin("lbrace"); */ return "LBRACE";} '}' { /* this.popState(); */ return "RBRACE";} - + [\s]+ return 'SPACE'; [\n\r]+ return 'EOL'; <> return 'EOF'; @@ -257,7 +257,7 @@ graphConfig statements : otherStatements | diagramStatements - | otherStatements diagramStatements + | otherStatements diagramStatements ; otherStatements @@ -268,10 +268,10 @@ otherStatements otherStatement : title {yy.setTitle($1.substring(6));$$=$1.substring(6);} - | accDescription {yy.setAccDescription($1.substring(15));$$=$1.substring(15);} + | accDescription {yy.setAccDescription($1.substring(15));$$=$1.substring(15);} | acc_title acc_title_value { $$=$2.trim();yy.setTitle($$); } | acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); } - | acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); } + | acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); } ; boundaryStatement @@ -301,7 +301,7 @@ boundaryStopStatement diagramStatements : diagramStatement | diagramStatement NEWLINE - | diagramStatement NEWLINE statements + | diagramStatement NEWLINE statements ; diagramStatement @@ -312,19 +312,19 @@ diagramStatement | SYSTEM_QUEUE attributes {yy.addPersonOrSystem('system_queue', ...$2); $$=$2;} | SYSTEM_EXT attributes {yy.addPersonOrSystem('external_system', ...$2); $$=$2;} | SYSTEM_EXT_DB attributes {yy.addPersonOrSystem('external_system_db', ...$2); $$=$2;} - | SYSTEM_EXT_QUEUE attributes {yy.addPersonOrSystem('external_system_queue', ...$2); $$=$2;} + | SYSTEM_EXT_QUEUE attributes {yy.addPersonOrSystem('external_system_queue', ...$2); $$=$2;} | CONTAINER attributes {yy.addContainer('container', ...$2); $$=$2;} | CONTAINER_DB attributes {yy.addContainer('container_db', ...$2); $$=$2;} | CONTAINER_QUEUE attributes {yy.addContainer('container_queue', ...$2); $$=$2;} | CONTAINER_EXT attributes {yy.addContainer('external_container', ...$2); $$=$2;} | CONTAINER_EXT_DB attributes {yy.addContainer('external_container_db', ...$2); $$=$2;} - | CONTAINER_EXT_QUEUE attributes {yy.addContainer('external_container_queue', ...$2); $$=$2;} + | CONTAINER_EXT_QUEUE attributes {yy.addContainer('external_container_queue', ...$2); $$=$2;} | COMPONENT attributes {yy.addComponent('component', ...$2); $$=$2;} | COMPONENT_DB attributes {yy.addComponent('component_db', ...$2); $$=$2;} | COMPONENT_QUEUE attributes {yy.addComponent('component_queue', ...$2); $$=$2;} | COMPONENT_EXT attributes {yy.addComponent('external_component', ...$2); $$=$2;} | COMPONENT_EXT_DB attributes {yy.addComponent('external_component_db', ...$2); $$=$2;} - | COMPONENT_EXT_QUEUE attributes {yy.addComponent('external_component_queue', ...$2); $$=$2;} + | COMPONENT_EXT_QUEUE attributes {yy.addComponent('external_component_queue', ...$2); $$=$2;} | boundaryStatement | REL attributes {yy.addRel('rel', ...$2); $$=$2;} | BIREL attributes {yy.addRel('birel', ...$2); $$=$2;} diff --git a/packages/mermaid/src/diagrams/c4/svgDraw.js b/packages/mermaid/src/diagrams/c4/svgDraw.js index 5ca2f55f81..9ec7422613 100644 --- a/packages/mermaid/src/diagrams/c4/svgDraw.js +++ b/packages/mermaid/src/diagrams/c4/svgDraw.js @@ -1,5 +1,5 @@ import common from '../common/common.js'; -import * as svgDrawCommon from '../common/svgDrawCommon'; +import * as svgDrawCommon from '../common/svgDrawCommon.js'; import { sanitizeUrl } from '@braintree/sanitize-url'; export const drawRect = function (elem, rectData) { diff --git a/packages/mermaid/src/diagrams/class/classDb.ts b/packages/mermaid/src/diagrams/class/classDb.ts index 11a1eb37df..c9a202aa43 100644 --- a/packages/mermaid/src/diagrams/class/classDb.ts +++ b/packages/mermaid/src/diagrams/class/classDb.ts @@ -1,5 +1,5 @@ -// @ts-nocheck - don't check until handle it -import { select, Selection } from 'd3'; +import type { Selection } from 'd3'; +import { select } from 'd3'; import { log } from '../../logger.js'; import * as configApi from '../../config.js'; import common from '../common/common.js'; @@ -13,8 +13,9 @@ import { clear as commonClear, setDiagramTitle, getDiagramTitle, -} from '../../commonDb.js'; -import { +} from '../common/commonDb.js'; +import { ClassMember } from './classTypes.js'; +import type { ClassRelation, ClassNode, ClassNote, @@ -70,21 +71,21 @@ export const setClassLabel = function (id: string, label: string) { * @public */ export const addClass = function (id: string) { - const classId = splitClassNameAndType(id); + const { className, type } = splitClassNameAndType(id); // Only add class if not exists - if (classes[classId.className] !== undefined) { + if (Object.hasOwn(classes, className)) { return; } - classes[classId.className] = { - id: classId.className, - type: classId.type, - label: classId.className, + classes[className] = { + id: className, + type: type, + label: className, cssClasses: [], methods: [], members: [], annotations: [], - domId: MERMAID_DOM_ID_PREFIX + classId.className + '-' + classCounter, + domId: MERMAID_DOM_ID_PREFIX + className + '-' + classCounter, } as ClassNode; classCounter++; @@ -114,11 +115,11 @@ export const clear = function () { commonClear(); }; -export const getClass = function (id: string) { +export const getClass = function (id: string): ClassNode { return classes[id]; }; -export const getClasses = function () { +export const getClasses = function (): ClassMap { return classes; }; @@ -174,6 +175,8 @@ export const addAnnotation = function (className: string, annotation: string) { * @public */ export const addMember = function (className: string, member: string) { + addClass(className); + const validatedClassName = splitClassNameAndType(className).className; const theClass = classes[validatedClassName]; @@ -186,9 +189,9 @@ export const addMember = function (className: string, member: string) { theClass.annotations.push(sanitizeText(memberString.substring(2, memberString.length - 2))); } else if (memberString.indexOf(')') > 0) { //its a method - theClass.methods.push(sanitizeText(memberString)); + theClass.methods.push(new ClassMember(memberString, 'method')); } else if (memberString) { - theClass.members.push(sanitizeText(memberString)); + theClass.members.push(new ClassMember(memberString, 'attribute')); } } }; @@ -255,6 +258,7 @@ export const getTooltip = function (id: string, namespace?: string) { return classes[id].tooltip; }; + /** * Called by parser when a link is found. Adds the URL to the vertex data. * @@ -367,6 +371,7 @@ export const relationType = { const setupToolTips = function (element: Element) { let tooltipElem: Selection = select('.mermaidTooltip'); + // @ts-expect-error - Incorrect types if ((tooltipElem._groups || tooltipElem)[0][0] === null) { tooltipElem = select('body').append('div').attr('class', 'mermaidTooltip').style('opacity', 0); } @@ -376,7 +381,6 @@ const setupToolTips = function (element: Element) { const nodes = svg.selectAll('g.node'); nodes .on('mouseover', function () { - // @ts-expect-error - select is not part of the d3 type definition const el = select(this); const title = el.attr('title'); // Don't try to draw a tooltip if no data is provided @@ -386,6 +390,7 @@ const setupToolTips = function (element: Element) { // @ts-ignore - getBoundingClientRect is not part of the d3 type definition const rect = this.getBoundingClientRect(); + // @ts-expect-error - Incorrect types tooltipElem.transition().duration(200).style('opacity', '.9'); tooltipElem .text(el.attr('title')) @@ -395,8 +400,8 @@ const setupToolTips = function (element: Element) { el.classed('hover', true); }) .on('mouseout', function () { + // @ts-expect-error - Incorrect types tooltipElem.transition().duration(500).style('opacity', 0); - // @ts-expect-error - select is not part of the d3 type definition const el = select(this); el.classed('hover', false); }); @@ -448,9 +453,8 @@ const getNamespaces = function (): NamespaceMap { export const addClassesToNamespace = function (id: string, classNames: string[]) { if (namespaces[id] !== undefined) { classNames.map((className) => { + classes[className].parent = id; namespaces[id].classes[className] = classes[className]; - delete classes[className]; - classCounter--; }); } }; diff --git a/packages/mermaid/src/diagrams/class/classDiagram-v2.ts b/packages/mermaid/src/diagrams/class/classDiagram-v2.ts index 5b952627cf..ec5398d29d 100644 --- a/packages/mermaid/src/diagrams/class/classDiagram-v2.ts +++ b/packages/mermaid/src/diagrams/class/classDiagram-v2.ts @@ -1,5 +1,5 @@ -import { DiagramDefinition } from '../../diagram-api/types.js'; -// @ts-ignore: TODO Fix ts errors +import type { DiagramDefinition } from '../../diagram-api/types.js'; +// @ts-ignore: JISON doesn't support types import parser from './parser/classDiagram.jison'; import db from './classDb.js'; import styles from './styles.js'; diff --git a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts index a43ed2fcda..532c8aaa7a 100644 --- a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts +++ b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts @@ -4,6 +4,9 @@ import classDb from './classDb.js'; import { vi, describe, it, expect } from 'vitest'; const spyOn = vi.spyOn; +const staticCssStyle = 'text-decoration:underline;'; +const abstractCssStyle = 'font-style:italic;'; + describe('given a basic class diagram, ', function () { describe('when parsing class definition', function () { beforeEach(function () { @@ -127,7 +130,7 @@ describe('given a basic class diagram, ', function () { const c1 = classDb.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); expect(c1.members.length).toBe(1); - expect(c1.members[0]).toBe('member1'); + expect(c1.members[0].getDisplayDetails().displayText).toBe('member1'); }); it('should parse a class with a text label, member and annotation', () => { @@ -142,7 +145,7 @@ describe('given a basic class diagram, ', function () { const c1 = classDb.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); expect(c1.members.length).toBe(1); - expect(c1.members[0]).toBe('int member1'); + expect(c1.members[0].getDisplayDetails().displayText).toBe('int member1'); expect(c1.annotations.length).toBe(1); expect(c1.annotations[0]).toBe('interface'); }); @@ -168,7 +171,7 @@ describe('given a basic class diagram, ', function () { const c1 = classDb.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); - expect(c1.members[0]).toBe('int member1'); + expect(c1.members[0].getDisplayDetails().displayText).toBe('int member1'); expect(c1.cssClasses[0]).toBe('styleClass'); }); @@ -264,6 +267,181 @@ class C13["With Città foreign language"] const str = 'classDiagram\n' + 'note "test"\n'; parser.parse(str); }); + + const keywords = [ + 'direction', + 'classDiagram', + 'classDiagram-v2', + 'namespace', + '{}', + '{', + '}', + '()', + '(', + ')', + '[]', + '[', + ']', + 'class', + '\n', + 'cssClass', + 'callback', + 'link', + 'click', + 'note', + 'note for', + '<<', + '>>', + 'call ', + '~', + '~Generic~', + '_self', + '_blank', + '_parent', + '_top', + '<|', + '|>', + '>', + '<', + '*', + 'o', + '\\', + '--', + '..', + '-->', + '--|>', + ': label', + ':::', + '.', + '+', + 'alphaNum', + '!', + '0123', + 'function()', + 'function(arg1, arg2)', + ]; + + it.each(keywords)('should handle a note with %s in it', function (keyword: string) { + const str = `classDiagram + note "This is a keyword: ${keyword}. It truly is." + `; + parser.parse(str); + expect(classDb.getNotes()[0].text).toEqual(`This is a keyword: ${keyword}. It truly is.`); + }); + + it.each(keywords)( + 'should handle note with %s at beginning of string', + function (keyword: string) { + const str = `classDiagram + note "${keyword}"`; + + parser.parse(str); + expect(classDb.getNotes()[0].text).toEqual(`${keyword}`); + } + ); + + it.each(keywords)('should handle a "note for" with a %s in it', function (keyword: string) { + const str = `classDiagram + class Something { + int id + string name + } + note for Something "This is a keyword: ${keyword}. It truly is." + `; + + parser.parse(str); + expect(classDb.getNotes()[0].text).toEqual(`This is a keyword: ${keyword}. It truly is.`); + }); + + it.each(keywords)( + 'should handle a "note for" with a %s at beginning of string', + function (keyword: string) { + const str = `classDiagram + class Something { + int id + string name + } + note for Something "${keyword}" + `; + + parser.parse(str); + expect(classDb.getNotes()[0].text).toEqual(`${keyword}`); + } + ); + + it.each(keywords)('should elicit error for %s after NOTE token', function (keyword: string) { + const str = `classDiagram + note ${keyword}`; + expect(() => parser.parse(str)).toThrowError(/(Expecting\s'STR'|Unrecognized\stext)/); + }); + + it('should parse diagram with direction', () => { + parser.parse(`classDiagram + direction TB + class Student { + -idCard : IdCard + } + class IdCard{ + -id : int + -name : string + } + class Bike{ + -id : int + -name : string + } + Student "1" --o "1" IdCard : carries + Student "1" --o "1" Bike : rides`); + + expect(Object.keys(classDb.getClasses()).length).toBe(3); + expect(classDb.getClasses().Student).toMatchInlineSnapshot(` + { + "annotations": [], + "cssClasses": [], + "domId": "classId-Student-134", + "id": "Student", + "label": "Student", + "members": [ + ClassMember { + "classifier": "", + "id": "idCard : IdCard", + "memberType": "attribute", + "visibility": "-", + }, + ], + "methods": [], + "type": "", + } + `); + expect(classDb.getRelations().length).toBe(2); + expect(classDb.getRelations()).toMatchInlineSnapshot(` + [ + { + "id1": "Student", + "id2": "IdCard", + "relation": { + "lineType": 0, + "type1": "none", + "type2": 0, + }, + "relationTitle1": "1", + "relationTitle2": "1", + "title": "carries", + }, + { + "id1": "Student", + "id2": "Bike", + "relation": { + "lineType": 0, + "type1": "none", + "type2": 0, + }, + "relationTitle1": "1", + "relationTitle2": "1", + "title": "rides", + }, + ] + `); + }); }); describe('when parsing class defined in brackets', function () { @@ -315,8 +493,8 @@ class C13["With Città foreign language"] 'classDiagram\n' + 'class Class1 {\n' + 'int testMember\n' + - 'string fooMember\n' + 'test()\n' + + 'string fooMember\n' + 'foo()\n' + '}'; parser.parse(str); @@ -324,10 +502,10 @@ class C13["With Città foreign language"] const actual = parser.yy.getClass('Class1'); expect(actual.members.length).toBe(2); expect(actual.methods.length).toBe(2); - expect(actual.members[0]).toBe('int testMember'); - expect(actual.members[1]).toBe('string fooMember'); - expect(actual.methods[0]).toBe('test()'); - expect(actual.methods[1]).toBe('foo()'); + expect(actual.members[0].getDisplayDetails().displayText).toBe('int testMember'); + expect(actual.members[1].getDisplayDetails().displayText).toBe('string fooMember'); + expect(actual.methods[0].getDisplayDetails().displayText).toBe('test()'); + expect(actual.methods[1].getDisplayDetails().displayText).toBe('foo()'); }); it('should parse a class with a text label and members', () => { @@ -337,7 +515,7 @@ class C13["With Città foreign language"] const c1 = classDb.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); expect(c1.members.length).toBe(1); - expect(c1.members[0]).toBe('+member1'); + expect(c1.members[0].getDisplayDetails().displayText).toBe('+member1'); }); it('should parse a class with a text label, members and annotation', () => { @@ -352,7 +530,7 @@ class C13["With Città foreign language"] const c1 = classDb.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); expect(c1.members.length).toBe(1); - expect(c1.members[0]).toBe('+member1'); + expect(c1.members[0].getDisplayDetails().displayText).toBe('+member1'); expect(c1.annotations.length).toBe(1); expect(c1.annotations[0]).toBe('interface'); }); @@ -635,6 +813,20 @@ describe('given a class diagram with members and methods ', function () { parser.parse(str); }); + it('should handle direct member declaration', function () { + parser.parse('classDiagram\n' + 'Car : wheels'); + const car = classDb.getClass('Car'); + expect(car.members.length).toBe(1); + expect(car.members[0].id).toBe('wheels'); + }); + + it('should handle direct member declaration with type', function () { + parser.parse('classDiagram\n' + 'Car : int wheels'); + const car = classDb.getClass('Car'); + expect(car.members.length).toBe(1); + expect(car.members[0].id).toBe('int wheels'); + }); + it('should handle simple member declaration with type', function () { const str = 'classDiagram\n' + 'class Car\n' + 'Car : int wheels'; @@ -655,10 +847,10 @@ describe('given a class diagram with members and methods ', function () { const actual = parser.yy.getClass('actual'); expect(actual.members.length).toBe(4); expect(actual.methods.length).toBe(0); - expect(actual.members[0]).toBe('-int privateMember'); - expect(actual.members[1]).toBe('+int publicMember'); - expect(actual.members[2]).toBe('#int protectedMember'); - expect(actual.members[3]).toBe('~int privatePackage'); + expect(actual.members[0].getDisplayDetails().displayText).toBe('-int privateMember'); + expect(actual.members[1].getDisplayDetails().displayText).toBe('+int publicMember'); + expect(actual.members[2].getDisplayDetails().displayText).toBe('#int protectedMember'); + expect(actual.members[3].getDisplayDetails().displayText).toBe('~int privatePackage'); }); it('should handle generic types', function () { @@ -711,7 +903,9 @@ describe('given a class diagram with members and methods ', function () { expect(actual.annotations.length).toBe(0); expect(actual.members.length).toBe(0); expect(actual.methods.length).toBe(1); - expect(actual.methods[0]).toBe('someMethod()*'); + const method = actual.methods[0]; + expect(method.getDisplayDetails().displayText).toBe('someMethod()'); + expect(method.getDisplayDetails().cssStyle).toBe(abstractCssStyle); }); it('should handle static methods', function () { @@ -722,7 +916,9 @@ describe('given a class diagram with members and methods ', function () { expect(actual.annotations.length).toBe(0); expect(actual.members.length).toBe(0); expect(actual.methods.length).toBe(1); - expect(actual.methods[0]).toBe('someMethod()$'); + const method = actual.methods[0]; + expect(method.getDisplayDetails().displayText).toBe('someMethod()'); + expect(method.getDisplayDetails().cssStyle).toBe(staticCssStyle); }); it('should handle generic types in arguments', function () { @@ -848,53 +1044,6 @@ foo() parser.parse(str); }); }); - - describe('when parsing invalid generic classes', function () { - beforeEach(function () { - classDb.clear(); - parser.yy = classDb; - }); - - it('should break when another `{`is encountered before closing the first one while defining generic class with brackets', function () { - const str = - 'classDiagram\n' + - 'class Dummy_Class~T~ {\n' + - 'String data\n' + - ' void methods()\n' + - '}\n' + - '\n' + - 'class Dummy_Class {\n' + - 'class Flight {\n' + - ' flightNumber : Integer\n' + - ' departureTime : Date\n' + - '}'; - let testPassed = false; - try { - parser.parse(str); - } catch (error) { - testPassed = true; - } - expect(testPassed).toBe(true); - }); - - it('should break when EOF is encountered before closing the first `{` while defining generic class with brackets', function () { - const str = - 'classDiagram\n' + - 'class Dummy_Class~T~ {\n' + - 'String data\n' + - ' void methods()\n' + - '}\n' + - '\n' + - 'class Dummy_Class {\n'; - let testPassed = false; - try { - parser.parse(str); - } catch (error) { - testPassed = true; - } - expect(testPassed).toBe(true); - }); - }); }); describe('given a class diagram with relationships, ', function () { @@ -1167,10 +1316,10 @@ describe('given a class diagram with relationships, ', function () { const testClass = parser.yy.getClass('Class1'); expect(testClass.members.length).toBe(2); expect(testClass.methods.length).toBe(2); - expect(testClass.members[0]).toBe('int : test'); - expect(testClass.members[1]).toBe('string : foo'); - expect(testClass.methods[0]).toBe('test()'); - expect(testClass.methods[1]).toBe('foo()'); + expect(testClass.members[0].getDisplayDetails().displayText).toBe('int : test'); + expect(testClass.members[1].getDisplayDetails().displayText).toBe('string : foo'); + expect(testClass.methods[0].getDisplayDetails().displayText).toBe('test()'); + expect(testClass.methods[1].getDisplayDetails().displayText).toBe('foo()'); }); it('should handle abstract methods', function () { @@ -1181,7 +1330,9 @@ describe('given a class diagram with relationships, ', function () { expect(testClass.annotations.length).toBe(0); expect(testClass.members.length).toBe(0); expect(testClass.methods.length).toBe(1); - expect(testClass.methods[0]).toBe('someMethod()*'); + const method = testClass.methods[0]; + expect(method.getDisplayDetails().displayText).toBe('someMethod()'); + expect(method.getDisplayDetails().cssStyle).toBe(abstractCssStyle); }); it('should handle static methods', function () { @@ -1192,7 +1343,9 @@ describe('given a class diagram with relationships, ', function () { expect(testClass.annotations.length).toBe(0); expect(testClass.members.length).toBe(0); expect(testClass.methods.length).toBe(1); - expect(testClass.methods[0]).toBe('someMethod()$'); + const method = testClass.methods[0]; + expect(method.getDisplayDetails().displayText).toBe('someMethod()'); + expect(method.getDisplayDetails().cssStyle).toBe(staticCssStyle); }); it('should associate link and css appropriately', function () { @@ -1373,9 +1526,62 @@ class Class2 parser.parse(str); const testNamespace = parser.yy.getNamespace('Namespace1'); + const testClasses = parser.yy.getClasses(); expect(Object.keys(testNamespace.classes).length).toBe(2); expect(Object.keys(testNamespace.children).length).toBe(0); expect(testNamespace.classes['Class1'].id).toBe('Class1'); + expect(Object.keys(testClasses).length).toBe(2); + }); + + it('should add relations between classes of different namespaces', function () { + const str = `classDiagram + A1 --> B1 + namespace A { + class A1 { + +foo : string + } + class A2 { + +bar : int + } + } + namespace B { + class B1 { + +foo : bool + } + class B2 { + +bar : float + } + } + A2 --> B2`; + + parser.parse(str); + const testNamespaceA = parser.yy.getNamespace('A'); + const testNamespaceB = parser.yy.getNamespace('B'); + const testClasses = parser.yy.getClasses(); + const testRelations = parser.yy.getRelations(); + expect(Object.keys(testNamespaceA.classes).length).toBe(2); + expect(testNamespaceA.classes['A1'].members[0].getDisplayDetails().displayText).toBe( + '+foo : string' + ); + expect(testNamespaceA.classes['A2'].members[0].getDisplayDetails().displayText).toBe( + '+bar : int' + ); + expect(Object.keys(testNamespaceB.classes).length).toBe(2); + expect(testNamespaceB.classes['B1'].members[0].getDisplayDetails().displayText).toBe( + '+foo : bool' + ); + expect(testNamespaceB.classes['B2'].members[0].getDisplayDetails().displayText).toBe( + '+bar : float' + ); + expect(Object.keys(testClasses).length).toBe(4); + expect(testClasses['A1'].parent).toBe('A'); + expect(testClasses['A2'].parent).toBe('A'); + expect(testClasses['B1'].parent).toBe('B'); + expect(testClasses['B2'].parent).toBe('B'); + expect(testRelations[0].id1).toBe('A1'); + expect(testRelations[0].id2).toBe('B1'); + expect(testRelations[1].id1).toBe('A2'); + expect(testRelations[1].id2).toBe('B2'); }); }); @@ -1418,8 +1624,8 @@ class Class2 const c1 = classDb.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); expect(c1.members.length).toBe(1); - expect(c1.members[0]).toBe('+member1'); - + const member = c1.members[0]; + expect(member.getDisplayDetails().displayText).toBe('+member1'); const c2 = classDb.getClass('C2'); expect(c2.label).toBe('C2'); }); @@ -1435,9 +1641,10 @@ class Class2 const c1 = classDb.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); expect(c1.members.length).toBe(1); - expect(c1.members[0]).toBe('+member1'); expect(c1.annotations.length).toBe(1); expect(c1.annotations[0]).toBe('interface'); + const member = c1.members[0]; + expect(member.getDisplayDetails().displayText).toBe('+member1'); const c2 = classDb.getClass('C2'); expect(c2.label).toBe('C2'); @@ -1454,8 +1661,9 @@ C1 --> C2 const c1 = classDb.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); expect(c1.cssClasses.length).toBe(1); - expect(c1.members[0]).toBe('+member1'); expect(c1.cssClasses[0]).toBe('styleClass'); + const member = c1.members[0]; + expect(member.getDisplayDetails().displayText).toBe('+member1'); }); it('should parse a class with text label and css class', () => { @@ -1470,8 +1678,9 @@ cssClass "C1" styleClass const c1 = classDb.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); expect(c1.cssClasses.length).toBe(1); - expect(c1.members[0]).toBe('+member1'); expect(c1.cssClasses[0]).toBe('styleClass'); + const member = c1.members[0]; + expect(member.getDisplayDetails().displayText).toBe('+member1'); }); it('should parse two classes with text labels and css classes', () => { diff --git a/packages/mermaid/src/diagrams/class/classDiagram.ts b/packages/mermaid/src/diagrams/class/classDiagram.ts index 0d2a246b4e..7f027c186e 100644 --- a/packages/mermaid/src/diagrams/class/classDiagram.ts +++ b/packages/mermaid/src/diagrams/class/classDiagram.ts @@ -1,5 +1,5 @@ -import { DiagramDefinition } from '../../diagram-api/types.js'; -// @ts-ignore: TODO Fix ts errors +import type { DiagramDefinition } from '../../diagram-api/types.js'; +// @ts-ignore: JISON doesn't support types import parser from './parser/classDiagram.jison'; import db from './classDb.js'; import styles from './styles.js'; diff --git a/packages/mermaid/src/diagrams/class/classDiagramGrammar.spec.ts b/packages/mermaid/src/diagrams/class/classDiagramGrammar.spec.ts deleted file mode 100644 index f7d93741ab..0000000000 --- a/packages/mermaid/src/diagrams/class/classDiagramGrammar.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { readFile } from 'node:fs/promises'; -import { fileURLToPath } from 'node:url'; -// @ts-ignore - no types -import { LALRGenerator } from 'jison'; - -const getAbsolutePath = (relativePath: string) => { - return fileURLToPath(new URL(relativePath, import.meta.url)); -}; - -describe('class diagram grammar', function () { - it('should have no conflicts', async function () { - const grammarSource = await readFile(getAbsolutePath('./parser/classDiagram.jison'), 'utf8'); - const grammarParser = new LALRGenerator(grammarSource, {}); - expect(grammarParser.conflicts).toBe(0); - }); -}); diff --git a/packages/mermaid/src/diagrams/class/classParser.spec.ts b/packages/mermaid/src/diagrams/class/classParser.spec.ts deleted file mode 100644 index c479b82721..0000000000 --- a/packages/mermaid/src/diagrams/class/classParser.spec.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { setConfig } from '../../config.js'; -import classDB from './classDb.js'; -// @ts-ignore - no types in jison -import classDiagram from './parser/classDiagram.jison'; - -setConfig({ - securityLevel: 'strict', -}); - -describe('when parsing class diagram', function () { - beforeEach(function () { - classDiagram.parser.yy = classDB; - classDiagram.parser.yy.clear(); - }); - - it('should parse diagram with direction', () => { - classDiagram.parser.parse(`classDiagram - direction TB - class Student { - -idCard : IdCard - } - class IdCard{ - -id : int - -name : string - } - class Bike{ - -id : int - -name : string - } - Student "1" --o "1" IdCard : carries - Student "1" --o "1" Bike : rides`); - - expect(Object.keys(classDB.getClasses()).length).toBe(3); - expect(classDB.getClasses().Student).toMatchInlineSnapshot(` - { - "annotations": [], - "cssClasses": [], - "domId": "classId-Student-0", - "id": "Student", - "label": "Student", - "members": [ - "-idCard : IdCard", - ], - "methods": [], - "type": "", - } - `); - expect(classDB.getRelations().length).toBe(2); - expect(classDB.getRelations()).toMatchInlineSnapshot(` - [ - { - "id1": "Student", - "id2": "IdCard", - "relation": { - "lineType": 0, - "type1": "none", - "type2": 0, - }, - "relationTitle1": "1", - "relationTitle2": "1", - "title": "carries", - }, - { - "id1": "Student", - "id2": "Bike", - "relation": { - "lineType": 0, - "type1": "none", - "type2": 0, - }, - "relationTitle1": "1", - "relationTitle2": "1", - "title": "rides", - }, - ] - `); - }); -}); diff --git a/packages/mermaid/src/diagrams/class/classRenderer-v2.ts b/packages/mermaid/src/diagrams/class/classRenderer-v2.ts index e0cfea641d..b581252bfa 100644 --- a/packages/mermaid/src/diagrams/class/classRenderer-v2.ts +++ b/packages/mermaid/src/diagrams/class/classRenderer-v2.ts @@ -8,7 +8,7 @@ import utils from '../../utils.js'; import { interpolateToCurve, getStylesFromArray } from '../../utils.js'; import { setupGraphViewbox } from '../../setupGraphViewbox.js'; import common from '../common/common.js'; -import { ClassRelation, ClassNote, ClassMap, EdgeData, NamespaceMap } from './classTypes.js'; +import type { ClassRelation, ClassNote, ClassMap, EdgeData, NamespaceMap } from './classTypes.js'; const sanitizeText = (txt: string) => common.sanitizeText(txt, getConfig()); @@ -93,52 +93,51 @@ export const addClasses = function ( log.info(classes); // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition - keys.forEach(function (id) { - const vertex = classes[id]; - - /** - * Variable for storing the classes for the vertex - */ - let cssClassStr = ''; - if (vertex.cssClasses.length > 0) { - cssClassStr = cssClassStr + ' ' + vertex.cssClasses.join(' '); - } - - const styles = { labelStyle: '', style: '' }; //getStylesFromArray(vertex.styles); - - // Use vertex id as text in the box if no text is provided by the graph definition - const vertexText = vertex.label ?? vertex.id; - const radius = 0; - const shape = 'class_box'; - - // Add the node - const node = { - labelStyle: styles.labelStyle, - shape: shape, - labelText: sanitizeText(vertexText), - classData: vertex, - rx: radius, - ry: radius, - class: cssClassStr, - style: styles.style, - id: vertex.id, - domId: vertex.domId, - tooltip: diagObj.db.getTooltip(vertex.id, parent) || '', - haveCallback: vertex.haveCallback, - link: vertex.link, - width: vertex.type === 'group' ? 500 : undefined, - type: vertex.type, - // TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release - padding: getConfig().flowchart?.padding ?? getConfig().class?.padding, - }; - g.setNode(vertex.id, node); - - if (parent) { - g.setParent(vertex.id, parent); - } + keys + .filter((id) => classes[id].parent == parent) + .forEach(function (id) { + const vertex = classes[id]; + + /** + * Variable for storing the classes for the vertex + */ + const cssClassStr = vertex.cssClasses.join(' '); + + const styles = { labelStyle: '', style: '' }; //getStylesFromArray(vertex.styles); + + // Use vertex id as text in the box if no text is provided by the graph definition + const vertexText = vertex.label ?? vertex.id; + const radius = 0; + const shape = 'class_box'; + + // Add the node + const node = { + labelStyle: styles.labelStyle, + shape: shape, + labelText: sanitizeText(vertexText), + classData: vertex, + rx: radius, + ry: radius, + class: cssClassStr, + style: styles.style, + id: vertex.id, + domId: vertex.domId, + tooltip: diagObj.db.getTooltip(vertex.id, parent) || '', + haveCallback: vertex.haveCallback, + link: vertex.link, + width: vertex.type === 'group' ? 500 : undefined, + type: vertex.type, + // TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release + padding: getConfig().flowchart?.padding ?? getConfig().class?.padding, + }; + g.setNode(vertex.id, node); + + if (parent) { + g.setParent(vertex.id, parent); + } - log.info('setNode', node); - }); + log.info('setNode', node); + }); }; /** @@ -157,24 +156,17 @@ export const addNotes = function ( ) { log.info(notes); - // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition notes.forEach(function (note, i) { const vertex = note; - /** - * Variable for storing the classes for the vertex - * - */ const cssNoteStr = ''; const styles = { labelStyle: '', style: '' }; - // Use vertex id as text in the box if no text is provided by the graph definition const vertexText = vertex.text; const radius = 0; const shape = 'note'; - // Add the node const node = { labelStyle: styles.labelStyle, shape: shape, @@ -302,7 +294,7 @@ export const setConf = function (cnf: any) { }; /** - * Draws a flowchart in the tag with id: id based on the graph definition in text. + * Draws a class diagram in the tag with id: id based on the definition in text. * * @param text - * @param id - diff --git a/packages/mermaid/src/diagrams/class/classRenderer.js b/packages/mermaid/src/diagrams/class/classRenderer.js index 3774f7b8ca..58def16c2a 100644 --- a/packages/mermaid/src/diagrams/class/classRenderer.js +++ b/packages/mermaid/src/diagrams/class/classRenderer.js @@ -141,8 +141,6 @@ const insertMarkers = function (elem) { export const draw = function (text, id, _version, diagObj) { const conf = getConfig().class; idCache = {}; - // diagObj.db.clear(); - // diagObj.parser.parse(text); log.info('Rendering diagram ' + text); diff --git a/packages/mermaid/src/diagrams/class/classTypes.spec.ts b/packages/mermaid/src/diagrams/class/classTypes.spec.ts new file mode 100644 index 0000000000..2b360d4473 --- /dev/null +++ b/packages/mermaid/src/diagrams/class/classTypes.spec.ts @@ -0,0 +1,683 @@ +import { ClassMember } from './classTypes.js'; +import { vi, describe, it, expect } from 'vitest'; +const spyOn = vi.spyOn; + +const staticCssStyle = 'text-decoration:underline;'; +const abstractCssStyle = 'font-style:italic;'; + +describe('given text representing a method, ', function () { + describe('when method has no parameters', function () { + it('should parse correctly', function () { + const str = `getTime()`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe(str); + }); + + it('should handle public visibility', function () { + const str = `+getTime()`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('+getTime()'); + }); + + it('should handle private visibility', function () { + const str = `-getTime()`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('-getTime()'); + }); + + it('should handle protected visibility', function () { + const str = `#getTime()`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('#getTime()'); + }); + + it('should handle internal visibility', function () { + const str = `~getTime()`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('~getTime()'); + }); + + it('should return correct css for static classifier', function () { + const str = `getTime()$`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTime()'); + expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle); + }); + + it('should return correct css for abstract classifier', function () { + const str = `getTime()*`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTime()'); + expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle); + }); + }); + + describe('when method has single parameter value', function () { + it('should parse correctly', function () { + const str = `getTime(int)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe(str); + }); + + it('should handle public visibility', function () { + const str = `+getTime(int)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('+getTime(int)'); + }); + + it('should handle private visibility', function () { + const str = `-getTime(int)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('-getTime(int)'); + }); + + it('should handle protected visibility', function () { + const str = `#getTime(int)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('#getTime(int)'); + }); + + it('should handle internal visibility', function () { + const str = `~getTime(int)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('~getTime(int)'); + }); + + it('should return correct css for static classifier', function () { + const str = `getTime(int)$`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTime(int)'); + expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle); + }); + + it('should return correct css for abstract classifier', function () { + const str = `getTime(int)*`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTime(int)'); + expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle); + }); + }); + + describe('when method has single parameter type and name (type first)', function () { + it('should parse correctly', function () { + const str = `getTime(int count)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe(str); + }); + + it('should handle public visibility', function () { + const str = `+getTime(int count)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('+getTime(int count)'); + }); + + it('should handle private visibility', function () { + const str = `-getTime(int count)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('-getTime(int count)'); + }); + + it('should handle protected visibility', function () { + const str = `#getTime(int count)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('#getTime(int count)'); + }); + + it('should handle internal visibility', function () { + const str = `~getTime(int count)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('~getTime(int count)'); + }); + + it('should return correct css for static classifier', function () { + const str = `getTime(int count)$`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTime(int count)'); + expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle); + }); + + it('should return correct css for abstract classifier', function () { + const str = `getTime(int count)*`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTime(int count)'); + expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle); + }); + }); + + describe('when method has single parameter type and name (name first)', function () { + it('should parse correctly', function () { + const str = `getTime(count int)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe(str); + }); + + it('should handle public visibility', function () { + const str = `+getTime(count int)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('+getTime(count int)'); + }); + + it('should handle private visibility', function () { + const str = `-getTime(count int)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('-getTime(count int)'); + }); + + it('should handle protected visibility', function () { + const str = `#getTime(count int)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('#getTime(count int)'); + }); + + it('should handle internal visibility', function () { + const str = `~getTime(count int)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('~getTime(count int)'); + }); + + it('should return correct css for static classifier', function () { + const str = `getTime(count int)$`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTime(count int)'); + expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle); + }); + + it('should return correct css for abstract classifier', function () { + const str = `getTime(count int)*`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTime(count int)'); + expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle); + }); + }); + + describe('when method has multiple parameters', function () { + it('should parse correctly', function () { + const str = `getTime(string text, int count)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe(str); + }); + + it('should handle public visibility', function () { + const str = `+getTime(string text, int count)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe(str); + }); + + it('should handle private visibility', function () { + const str = `-getTime(string text, int count)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe(str); + }); + + it('should handle protected visibility', function () { + const str = `#getTime(string text, int count)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe(str); + }); + + it('should handle internal visibility', function () { + const str = `~getTime(string text, int count)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe(str); + }); + + it('should return correct css for static classifier', function () { + const str = `getTime(string text, int count)$`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTime(string text, int count)'); + expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle); + }); + + it('should return correct css for abstract classifier', function () { + const str = `getTime(string text, int count)*`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTime(string text, int count)'); + expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle); + }); + }); + + describe('when method has return type', function () { + it('should parse correctly', function () { + const str = `getTime() DateTime`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTime() : DateTime'); + }); + + it('should handle public visibility', function () { + const str = `+getTime() DateTime`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('+getTime() : DateTime'); + }); + + it('should handle private visibility', function () { + const str = `-getTime() DateTime`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('-getTime() : DateTime'); + }); + + it('should handle protected visibility', function () { + const str = `#getTime() DateTime`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('#getTime() : DateTime'); + }); + + it('should handle internal visibility', function () { + const str = `~getTime() DateTime`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('~getTime() : DateTime'); + }); + + it('should return correct css for static classifier', function () { + const str = `getTime() DateTime$`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTime() : DateTime'); + expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle); + }); + + it('should return correct css for abstract classifier', function () { + const str = `getTime() DateTime*`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTime() : DateTime'); + expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle); + }); + }); + + describe('when method parameter is generic', function () { + it('should parse correctly', function () { + const str = `getTimes(List~T~)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTimes(List)'); + }); + + it('should handle public visibility', function () { + const str = `+getTimes(List~T~)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('+getTimes(List)'); + }); + + it('should handle private visibility', function () { + const str = `-getTimes(List~T~)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('-getTimes(List)'); + }); + + it('should handle protected visibility', function () { + const str = `#getTimes(List~T~)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('#getTimes(List)'); + }); + + it('should handle internal visibility', function () { + const str = `~getTimes(List~T~)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('~getTimes(List)'); + }); + + it('should return correct css for static classifier', function () { + const str = `getTimes(List~T~)$`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTimes(List)'); + expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle); + }); + + it('should return correct css for abstract classifier', function () { + const str = `getTimes(List~T~)*`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTimes(List)'); + expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle); + }); + }); + + describe('when method parameter contains two generic', function () { + it('should parse correctly', function () { + const str = `getTimes(List~T~, List~OT~)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTimes(List, List)'); + }); + + it('should handle public visibility', function () { + const str = `+getTimes(List~T~, List~OT~)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('+getTimes(List, List)'); + }); + + it('should handle private visibility', function () { + const str = `-getTimes(List~T~, List~OT~)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('-getTimes(List, List)'); + }); + + it('should handle protected visibility', function () { + const str = `#getTimes(List~T~, List~OT~)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('#getTimes(List, List)'); + }); + + it('should handle internal visibility', function () { + const str = `~getTimes(List~T~, List~OT~)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('~getTimes(List, List)'); + }); + + it('should return correct css for static classifier', function () { + const str = `getTimes(List~T~, List~OT~)$`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTimes(List, List)'); + expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle); + }); + + it('should return correct css for abstract classifier', function () { + const str = `getTimes(List~T~, List~OT~)*`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTimes(List, List)'); + expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle); + }); + }); + + describe('when method parameter is a nested generic', function () { + it('should parse correctly', function () { + const str = `getTimetableList(List~List~T~~)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTimetableList(List>)'); + }); + + it('should handle public visibility', function () { + const str = `+getTimetableList(List~List~T~~)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('+getTimetableList(List>)'); + }); + + it('should handle private visibility', function () { + const str = `-getTimetableList(List~List~T~~)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('-getTimetableList(List>)'); + }); + + it('should handle protected visibility', function () { + const str = `#getTimetableList(List~List~T~~)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('#getTimetableList(List>)'); + }); + + it('should handle internal visibility', function () { + const str = `~getTimetableList(List~List~T~~)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('~getTimetableList(List>)'); + }); + + it('should return correct css for static classifier', function () { + const str = `getTimetableList(List~List~T~~)$`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTimetableList(List>)'); + expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle); + }); + + it('should return correct css for abstract classifier', function () { + const str = `getTimetableList(List~List~T~~)*`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTimetableList(List>)'); + expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle); + }); + }); + + describe('when method parameter is a composite generic', function () { + const methodNameAndParameters = 'getTimes(List~K, V~)'; + const expectedMethodNameAndParameters = 'getTimes(List)'; + it('should parse correctly', function () { + const str = methodNameAndParameters; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe(expectedMethodNameAndParameters); + }); + + it('should handle public visibility', function () { + const str = '+' + methodNameAndParameters; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe( + '+' + expectedMethodNameAndParameters + ); + }); + + it('should handle private visibility', function () { + const str = '-' + methodNameAndParameters; + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe( + '-' + expectedMethodNameAndParameters + ); + }); + + it('should handle protected visibility', function () { + const str = '#' + methodNameAndParameters; + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe( + '#' + expectedMethodNameAndParameters + ); + }); + + it('should handle internal visibility', function () { + const str = '~' + methodNameAndParameters; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe( + '~' + expectedMethodNameAndParameters + ); + }); + + it('should return correct css for static classifier', function () { + const str = methodNameAndParameters + '$'; + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe(expectedMethodNameAndParameters); + expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle); + }); + + it('should return correct css for abstract classifier', function () { + const str = methodNameAndParameters + '*'; + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe(expectedMethodNameAndParameters); + expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle); + }); + }); + + describe('when method return type is generic', function () { + it('should parse correctly', function () { + const str = `getTimes() List~T~`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTimes() : List'); + }); + + it('should handle public visibility', function () { + const str = `+getTimes() List~T~`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('+getTimes() : List'); + }); + + it('should handle private visibility', function () { + const str = `-getTimes() List~T~`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('-getTimes() : List'); + }); + + it('should handle protected visibility', function () { + const str = `#getTimes() List~T~`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('#getTimes() : List'); + }); + + it('should handle internal visibility', function () { + const str = `~getTimes() List~T~`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('~getTimes() : List'); + }); + + it('should return correct css for static classifier', function () { + const str = `getTimes() List~T~$`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTimes() : List'); + expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle); + }); + + it('should return correct css for abstract classifier', function () { + const str = `getTimes() List~T~*`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTimes() : List'); + expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle); + }); + }); + + describe('when method return type is a nested generic', function () { + it('should parse correctly', function () { + const str = `getTimetableList() List~List~T~~`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe( + 'getTimetableList() : List>' + ); + }); + + it('should handle public visibility', function () { + const str = `+getTimetableList() List~List~T~~`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe( + '+getTimetableList() : List>' + ); + }); + + it('should handle private visibility', function () { + const str = `-getTimetableList() List~List~T~~`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe( + '-getTimetableList() : List>' + ); + }); + + it('should handle protected visibility', function () { + const str = `#getTimetableList() List~List~T~~`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe( + '#getTimetableList() : List>' + ); + }); + + it('should handle internal visibility', function () { + const str = `~getTimetableList() List~List~T~~`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe( + '~getTimetableList() : List>' + ); + }); + + it('should return correct css for static classifier', function () { + const str = `getTimetableList() List~List~T~~$`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe( + 'getTimetableList() : List>' + ); + expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle); + }); + + it('should return correct css for abstract classifier', function () { + const str = `getTimetableList() List~List~T~~*`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe( + 'getTimetableList() : List>' + ); + expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle); + }); + }); + + describe('--uncategorized tests--', function () { + it('member name should handle double colons', function () { + const str = `std::map ~int,string~ pMap;`; + + const classMember = new ClassMember(str, 'attribute'); + expect(classMember.getDisplayDetails().displayText).toBe('std::map pMap;'); + }); + + it('member name should handle generic type', function () { + const str = `getTime~T~(this T, int seconds)$ DateTime`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe( + 'getTime(this T, int seconds) : DateTime' + ); + expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle); + }); + }); +}); diff --git a/packages/mermaid/src/diagrams/class/classTypes.ts b/packages/mermaid/src/diagrams/class/classTypes.ts index cf6f20f0b1..aa5ec7b70d 100644 --- a/packages/mermaid/src/diagrams/class/classTypes.ts +++ b/packages/mermaid/src/diagrams/class/classTypes.ts @@ -1,18 +1,136 @@ +import { getConfig } from '../../config.js'; +import { parseGenericTypes, sanitizeText } from '../common/common.js'; + export interface ClassNode { id: string; type: string; label: string; cssClasses: string[]; - methods: string[]; - members: string[]; + methods: ClassMember[]; + members: ClassMember[]; annotations: string[]; domId: string; + parent?: string; link?: string; linkTarget?: string; haveCallback?: boolean; tooltip?: string; } +export type Visibility = '#' | '+' | '~' | '-' | ''; +export const visibilityValues = ['#', '+', '~', '-', '']; + +/** + * Parses and stores class diagram member variables/methods. + * + */ +export class ClassMember { + id!: string; + cssStyle!: string; + memberType!: 'method' | 'attribute'; + visibility!: Visibility; + /** + * denote if static or to determine which css class to apply to the node + * @defaultValue '' + */ + classifier!: string; + /** + * parameters for method + * @defaultValue '' + */ + parameters!: string; + /** + * return type for method + * @defaultValue '' + */ + returnType!: string; + + constructor(input: string, memberType: 'method' | 'attribute') { + this.memberType = memberType; + this.visibility = ''; + this.classifier = ''; + const sanitizedInput = sanitizeText(input, getConfig()); + this.parseMember(sanitizedInput); + } + + getDisplayDetails() { + let displayText = this.visibility + parseGenericTypes(this.id); + if (this.memberType === 'method') { + displayText += `(${parseGenericTypes(this.parameters.trim())})`; + if (this.returnType) { + displayText += ' : ' + parseGenericTypes(this.returnType); + } + } + + displayText = displayText.trim(); + const cssStyle = this.parseClassifier(); + + return { + displayText, + cssStyle, + }; + } + + parseMember(input: string) { + let potentialClassifier = ''; + + if (this.memberType === 'method') { + const methodRegEx = /([#+~-])?(.+)\((.*)\)([\s$*])?(.*)([$*])?/; + const match = input.match(methodRegEx); + if (match) { + const detectedVisibility = match[1] ? match[1].trim() : ''; + + if (visibilityValues.includes(detectedVisibility)) { + this.visibility = detectedVisibility as Visibility; + } + + this.id = match[2].trim(); + this.parameters = match[3] ? match[3].trim() : ''; + potentialClassifier = match[4] ? match[4].trim() : ''; + this.returnType = match[5] ? match[5].trim() : ''; + + if (potentialClassifier === '') { + const lastChar = this.returnType.substring(this.returnType.length - 1); + if (lastChar.match(/[$*]/)) { + potentialClassifier = lastChar; + this.returnType = this.returnType.substring(0, this.returnType.length - 1); + } + } + } + } else { + const length = input.length; + const firstChar = input.substring(0, 1); + const lastChar = input.substring(length - 1); + + if (visibilityValues.includes(firstChar)) { + this.visibility = firstChar as Visibility; + } + + if (lastChar.match(/[*?]/)) { + potentialClassifier = lastChar; + } + + this.id = input.substring( + this.visibility === '' ? 0 : 1, + potentialClassifier === '' ? length : length - 1 + ); + } + + this.classifier = potentialClassifier; + } + + parseClassifier() { + switch (this.classifier) { + case '*': + return 'font-style:italic;'; + case '$': + return 'text-decoration:underline;'; + default: + return ''; + } + } +} + export interface ClassNote { id: string; class: string; diff --git a/packages/mermaid/src/diagrams/class/parser/classDiagram.jison b/packages/mermaid/src/diagrams/class/parser/classDiagram.jison index 7788fcc0c0..9c67e306ec 100644 --- a/packages/mermaid/src/diagrams/class/parser/classDiagram.jison +++ b/packages/mermaid/src/diagrams/class/parser/classDiagram.jison @@ -24,31 +24,50 @@ %x namespace %x namespace-body %% -\%\%\{ { this.begin('open_directive'); return 'open_directive'; } -.*direction\s+TB[^\n]* return 'direction_tb'; -.*direction\s+BT[^\n]* return 'direction_bt'; -.*direction\s+RL[^\n]* return 'direction_rl'; -.*direction\s+LR[^\n]* return 'direction_lr'; -((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; } -":" { this.popState(); this.begin('arg_directive'); return ':'; } -\}\%\% { this.popState(); this.popState(); return 'close_directive'; } -((?:(?!\}\%\%).|\n)*) return 'arg_directive'; -\%\%(?!\{)*[^\n]*(\r?\n?)+ /* skip comments */ -\%\%[^\n]*(\r?\n)* /* skip comments */ -accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; } -(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; } -accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; } -(?!\n|;|#)*[^\n]* { this.popState(); return "acc_descr_value"; } -accDescr\s*"{"\s* { this.begin("acc_descr_multiline");} -[\}] { this.popState(); } -[^\}]* return "acc_descr_multiline_value"; - -\s*(\r?\n)+ return 'NEWLINE'; -\s+ /* skip whitespace */ - -"classDiagram-v2" return 'CLASS_DIAGRAM'; -"classDiagram" return 'CLASS_DIAGRAM'; -"[*]" return 'EDGE_STATE'; +\%\%\{ { this.begin('open_directive'); return 'open_directive'; } +.*direction\s+TB[^\n]* return 'direction_tb'; +.*direction\s+BT[^\n]* return 'direction_bt'; +.*direction\s+RL[^\n]* return 'direction_rl'; +.*direction\s+LR[^\n]* return 'direction_lr'; +((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; } +":" { this.popState(); this.begin('arg_directive'); return ':'; } +\}\%\% { this.popState(); this.popState(); return 'close_directive'; } +((?:(?!\}\%\%).|\n)*) return 'arg_directive'; +\%\%(?!\{)*[^\n]*(\r?\n?)+ /* skip comments */ +\%\%[^\n]*(\r?\n)* /* skip comments */ +accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; } +(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; } +accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; } +(?!\n|;|#)*[^\n]* { this.popState(); return "acc_descr_value"; } +accDescr\s*"{"\s* { this.begin("acc_descr_multiline");} +[\}] { this.popState(); } +[^\}]* return "acc_descr_multiline_value"; + +\s*(\r?\n)+ return 'NEWLINE'; +\s+ /* skip whitespace */ + +"classDiagram-v2" return 'CLASS_DIAGRAM'; +"classDiagram" return 'CLASS_DIAGRAM'; +"[*]" return 'EDGE_STATE'; + +/* +---interactivity command--- +'call' adds a callback to the specified node. 'call' can only be specified when +the line was introduced with 'click'. +'call ()' attaches the function 'callback_name' with the specified +arguments to the node that was specified by 'click'. +Function arguments are optional: 'call ()' simply executes 'callback_name' without any arguments. +*/ +"call"[\s]+ this.begin("callback_name"); +\([\s]*\) this.popState(); +\( this.popState(); this.begin("callback_args"); +[^(]* return 'CALLBACK_NAME'; +\) this.popState(); +[^)]* return 'CALLBACK_ARGS'; + +["] this.popState(); +[^"]* return "STR"; +<*>["] this.begin("string"); "namespace" { this.begin('namespace'); return 'NAMESPACE'; } \s*(\r?\n)+ { this.popState(); return 'NEWLINE'; } @@ -60,26 +79,26 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili \s+ /* skip whitespace */ "[*]" return 'EDGE_STATE'; -"class" { this.begin('class'); return 'CLASS';} -\s*(\r?\n)+ { this.popState(); return 'NEWLINE'; } -\s+ /* skip whitespace */ -[}] { this.popState(); this.popState(); return 'STRUCT_STOP';} -[{] { this.begin("class-body"); return 'STRUCT_START';} -[}] { this.popState(); return 'STRUCT_STOP'; } -<> return "EOF_IN_STRUCT"; -"[*]" { return 'EDGE_STATE';} -[{] return "OPEN_IN_STRUCT"; -[\n] /* nothing */ -[^{}\n]* { return "MEMBER";} - -<*>"cssClass" return 'CSSCLASS'; -<*>"callback" return 'CALLBACK'; -<*>"link" return 'LINK'; -<*>"click" return 'CLICK'; -<*>"note for" return 'NOTE_FOR'; -<*>"note" return 'NOTE'; -<*>"<<" return 'ANNOTATION_START'; -<*>">>" return 'ANNOTATION_END'; +"class" { this.begin('class'); return 'CLASS';} +\s*(\r?\n)+ { this.popState(); return 'NEWLINE'; } +\s+ /* skip whitespace */ +[}] { this.popState(); this.popState(); return 'STRUCT_STOP';} +[{] { this.begin("class-body"); return 'STRUCT_START';} +[}] { this.popState(); return 'STRUCT_STOP'; } +<> return "EOF_IN_STRUCT"; +"[*]" { return 'EDGE_STATE';} +[{] return "OPEN_IN_STRUCT"; +[\n] /* nothing */ +[^{}\n]* { return "MEMBER";} + +<*>"cssClass" return 'CSSCLASS'; +<*>"callback" return 'CALLBACK'; +<*>"link" return 'LINK'; +<*>"click" return 'CLICK'; +<*>"note for" return 'NOTE_FOR'; +<*>"note" return 'NOTE'; +<*>"<<" return 'ANNOTATION_START'; +<*>">>" return 'ANNOTATION_END'; /* ---interactivity command--- @@ -87,64 +106,43 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili line was introduced with 'click'. 'href ""' attaches the specified link to the node that was specified by 'click'. */ -<*>"href"[\s]+["] this.begin("href"); -["] this.popState(); -[^"]* return 'HREF'; - -/* ----interactivity command--- -'call' adds a callback to the specified node. 'call' can only be specified when -the line was introduced with 'click'. -'call ()' attaches the function 'callback_name' with the specified -arguments to the node that was specified by 'click'. -Function arguments are optional: 'call ()' simply executes 'callback_name' without any arguments. -*/ -<*>"call"[\s]+ this.begin("callback_name"); -\([\s]*\) this.popState(); -\( this.popState(); this.begin("callback_args"); -[^(]* return 'CALLBACK_NAME'; -\) this.popState(); -[^)]* return 'CALLBACK_ARGS'; - -[~] this.popState(); -[^~]* return "GENERICTYPE"; -<*>[~] this.begin("generic"); - -["] this.popState(); -[^"]* return "STR"; -<*>["] this.begin("string"); - -[`] this.popState(); -[^`]+ return "BQUOTE_STR"; -<*>[`] this.begin("bqstring"); - -<*>"_self" return 'LINK_TARGET'; -<*>"_blank" return 'LINK_TARGET'; -<*>"_parent" return 'LINK_TARGET'; -<*>"_top" return 'LINK_TARGET'; - -<*>\s*\<\| return 'EXTENSION'; -<*>\s*\|\> return 'EXTENSION'; -<*>\s*\> return 'DEPENDENCY'; -<*>\s*\< return 'DEPENDENCY'; -<*>\s*\* return 'COMPOSITION'; -<*>\s*o return 'AGGREGATION'; -<*>\s*\(\) return 'LOLLIPOP'; -<*>\-\- return 'LINE'; -<*>\.\. return 'DOTTED_LINE'; -<*>":"{1}[^:\n;]+ return 'LABEL'; -<*>":"{3} return 'STYLE_SEPARATOR'; -<*>\- return 'MINUS'; -<*>"." return 'DOT'; -<*>\+ return 'PLUS'; -<*>\% return 'PCT'; -<*>"=" return 'EQUALS'; -<*>\= return 'EQUALS'; -<*>\w+ return 'ALPHA'; -<*>"[" return 'SQS'; -<*>"]" return 'SQE'; -<*>[!"#$%&'*+,-.`?\\/] return 'PUNCTUATION'; -<*>[0-9]+ return 'NUM'; +<*>"href" return 'HREF'; + +[~] this.popState(); +[^~]* return "GENERICTYPE"; +<*>"~" this.begin("generic"); + +[`] this.popState(); +[^`]+ return "BQUOTE_STR"; +<*>[`] this.begin("bqstring"); + +<*>"_self" return 'LINK_TARGET'; +<*>"_blank" return 'LINK_TARGET'; +<*>"_parent" return 'LINK_TARGET'; +<*>"_top" return 'LINK_TARGET'; + +<*>\s*\<\| return 'EXTENSION'; +<*>\s*\|\> return 'EXTENSION'; +<*>\s*\> return 'DEPENDENCY'; +<*>\s*\< return 'DEPENDENCY'; +<*>\s*\* return 'COMPOSITION'; +<*>\s*o return 'AGGREGATION'; +<*>\s*\(\) return 'LOLLIPOP'; +<*>\-\- return 'LINE'; +<*>\.\. return 'DOTTED_LINE'; +<*>":"{1}[^:\n;]+ return 'LABEL'; +<*>":"{3} return 'STYLE_SEPARATOR'; +<*>\- return 'MINUS'; +<*>"." return 'DOT'; +<*>\+ return 'PLUS'; +<*>\% return 'PCT'; +<*>"=" return 'EQUALS'; +<*>\= return 'EQUALS'; +<*>\w+ return 'ALPHA'; +<*>"[" return 'SQS'; +<*>"]" return 'SQE'; +<*>[!"#$%&'*+,-.`?\\/] return 'PUNCTUATION'; +<*>[0-9]+ return 'NUM'; <*>[\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]| [\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377]| [\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5]| @@ -206,9 +204,9 @@ Function arguments are optional: 'call ()' simply executes 'callb [\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC]| [\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF]| [\uFFD2-\uFFD7\uFFDA-\uFFDC] - return 'UNICODE_TEXT'; -<*>\s return 'SPACE'; -<*><> return 'EOF'; + return 'UNICODE_TEXT'; +<*>\s return 'SPACE'; +<*><> return 'EOF'; /lex @@ -226,21 +224,14 @@ start | statements ; -direction - : direction_tb - { yy.setDirection('TB');} - | direction_bt - { yy.setDirection('BT');} - | direction_rl - { yy.setDirection('RL');} - | direction_lr - { yy.setDirection('LR');} - ; - mermaidDoc : graphConfig ; +graphConfig + : CLASS_DIAGRAM NEWLINE statements EOF + ; + directive : openDirective typeDirective closeDirective NEWLINE | openDirective typeDirective ':' argDirective closeDirective NEWLINE @@ -262,10 +253,6 @@ closeDirective : close_directive { yy.parseDirective('}%%', 'close_directive', 'class'); } ; -graphConfig - : CLASS_DIAGRAM NEWLINE statements EOF - ; - statements : statement | statement NEWLINE @@ -294,7 +281,7 @@ statement | relationStatement LABEL { $1.title = yy.cleanupLabel($2); yy.addRelation($1); } | namespaceStatement | classStatement - | methodStatement + | memberStatement | annotationStatement | clickStatement | cssClassStatement @@ -321,7 +308,7 @@ classStatements ; classStatement - : classIdentifier + : classIdentifier | classIdentifier STYLE_SEPARATOR alphaNumToken {yy.setCssClass($1, $3);} | classIdentifier STRUCT_START members STRUCT_STOP {yy.addMembers($1,$3);} | classIdentifier STYLE_SEPARATOR alphaNumToken STRUCT_START members STRUCT_STOP {yy.setCssClass($1, $3);yy.addMembers($1,$5);} @@ -341,7 +328,7 @@ members | MEMBER members { $2.push($1);$$=$2;} ; -methodStatement +memberStatement : className {/*console.log('Rel found',$1);*/} | className LABEL {yy.addMember($1,yy.cleanupLabel($2));} | MEMBER {/*console.warn('Member',$1);*/} @@ -360,6 +347,17 @@ noteStatement | NOTE noteText { yy.addNote($2); } ; +direction + : direction_tb + { yy.setDirection('TB');} + | direction_bt + { yy.setDirection('BT');} + | direction_rl + { yy.setDirection('RL');} + | direction_lr + { yy.setDirection('LR');} + ; + relation : relationType lineType relationType { $$={type1:$1,type2:$3,lineType:$2}; } | lineType relationType { $$={type1:'none',type2:$2,lineType:$1}; } @@ -391,10 +389,10 @@ clickStatement | CLICK className CALLBACK_NAME STR {$$ = $1;yy.setClickEvent($2, $3);yy.setTooltip($2, $4);} | CLICK className CALLBACK_NAME CALLBACK_ARGS {$$ = $1;yy.setClickEvent($2, $3, $4);} | CLICK className CALLBACK_NAME CALLBACK_ARGS STR {$$ = $1;yy.setClickEvent($2, $3, $4);yy.setTooltip($2, $5);} - | CLICK className HREF {$$ = $1;yy.setLink($2, $3);} - | CLICK className HREF LINK_TARGET {$$ = $1;yy.setLink($2, $3, $4);} - | CLICK className HREF STR {$$ = $1;yy.setLink($2, $3);yy.setTooltip($2, $4);} - | CLICK className HREF STR LINK_TARGET {$$ = $1;yy.setLink($2, $3, $5);yy.setTooltip($2, $4);} + | CLICK className HREF STR {$$ = $1;yy.setLink($2, $4);} + | CLICK className HREF STR LINK_TARGET {$$ = $1;yy.setLink($2, $4, $5);} + | CLICK className HREF STR STR {$$ = $1;yy.setLink($2, $4);yy.setTooltip($2, $5);} + | CLICK className HREF STR STR LINK_TARGET {$$ = $1;yy.setLink($2, $4, $6);yy.setTooltip($2, $5);} ; cssClassStatement diff --git a/packages/mermaid/src/diagrams/class/styles.js b/packages/mermaid/src/diagrams/class/styles.js index 15386bf9ef..f12f609f91 100644 --- a/packages/mermaid/src/diagrams/class/styles.js +++ b/packages/mermaid/src/diagrams/class/styles.js @@ -109,25 +109,25 @@ g.classGroup line { } #extensionStart, .extension { - fill: ${options.mainBkg} !important; + fill: transparent !important; stroke: ${options.lineColor} !important; stroke-width: 1; } #extensionEnd, .extension { - fill: ${options.mainBkg} !important; + fill: transparent !important; stroke: ${options.lineColor} !important; stroke-width: 1; } #aggregationStart, .aggregation { - fill: ${options.mainBkg} !important; + fill: transparent !important; stroke: ${options.lineColor} !important; stroke-width: 1; } #aggregationEnd, .aggregation { - fill: ${options.mainBkg} !important; + fill: transparent !important; stroke: ${options.lineColor} !important; stroke-width: 1; } diff --git a/packages/mermaid/src/diagrams/class/svgDraw.js b/packages/mermaid/src/diagrams/class/svgDraw.js index e4afe21368..d6ed7bca4e 100644 --- a/packages/mermaid/src/diagrams/class/svgDraw.js +++ b/packages/mermaid/src/diagrams/class/svgDraw.js @@ -172,7 +172,6 @@ export const drawClass = function (elem, classDef, conf, diagObj) { // add class group const g = elem.append('g').attr('id', diagObj.db.lookUpDomId(id)).attr('class', 'classGroup'); - // add title let title; if (classDef.link) { title = g @@ -209,47 +208,56 @@ export const drawClass = function (elem, classDef, conf, diagObj) { } const titleHeight = title.node().getBBox().height; + let membersLine; + let membersBox; + let methodsLine; + + // don't draw box if no members + if (classDef.members.length > 0) { + membersLine = g + .append('line') // text label for the x axis + .attr('x1', 0) + .attr('y1', conf.padding + titleHeight + conf.dividerMargin / 2) + .attr('y2', conf.padding + titleHeight + conf.dividerMargin / 2); + + const members = g + .append('text') // text label for the x axis + .attr('x', conf.padding) + .attr('y', titleHeight + conf.dividerMargin + conf.textHeight) + .attr('fill', 'white') + .attr('class', 'classText'); + + isFirst = true; + classDef.members.forEach(function (member) { + addTspan(members, member, isFirst, conf); + isFirst = false; + }); + + membersBox = members.node().getBBox(); + } - const membersLine = g - .append('line') // text label for the x axis - .attr('x1', 0) - .attr('y1', conf.padding + titleHeight + conf.dividerMargin / 2) - .attr('y2', conf.padding + titleHeight + conf.dividerMargin / 2); - - const members = g - .append('text') // text label for the x axis - .attr('x', conf.padding) - .attr('y', titleHeight + conf.dividerMargin + conf.textHeight) - .attr('fill', 'white') - .attr('class', 'classText'); - - isFirst = true; - classDef.members.forEach(function (member) { - addTspan(members, member, isFirst, conf); - isFirst = false; - }); - - const membersBox = members.node().getBBox(); - - const methodsLine = g - .append('line') // text label for the x axis - .attr('x1', 0) - .attr('y1', conf.padding + titleHeight + conf.dividerMargin + membersBox.height) - .attr('y2', conf.padding + titleHeight + conf.dividerMargin + membersBox.height); - - const methods = g - .append('text') // text label for the x axis - .attr('x', conf.padding) - .attr('y', titleHeight + 2 * conf.dividerMargin + membersBox.height + conf.textHeight) - .attr('fill', 'white') - .attr('class', 'classText'); - - isFirst = true; - - classDef.methods.forEach(function (method) { - addTspan(methods, method, isFirst, conf); - isFirst = false; - }); + // don't draw box if no methods + if (classDef.methods.length > 0) { + methodsLine = g + .append('line') // text label for the x axis + .attr('x1', 0) + .attr('y1', conf.padding + titleHeight + conf.dividerMargin + membersBox.height) + .attr('y2', conf.padding + titleHeight + conf.dividerMargin + membersBox.height); + + const methods = g + .append('text') // text label for the x axis + .attr('x', conf.padding) + .attr('y', titleHeight + 2 * conf.dividerMargin + membersBox.height + conf.textHeight) + .attr('fill', 'white') + .attr('class', 'classText'); + + isFirst = true; + + classDef.methods.forEach(function (method) { + addTspan(methods, method, isFirst, conf); + isFirst = false; + }); + } const classBox = g.node().getBBox(); var cssClassStr = ' '; @@ -278,8 +286,12 @@ export const drawClass = function (elem, classDef, conf, diagObj) { title.insert('title').text(classDef.tooltip); } - membersLine.attr('x2', rectWidth); - methodsLine.attr('x2', rectWidth); + if (membersLine) { + membersLine.attr('x2', rectWidth); + } + if (methodsLine) { + methodsLine.attr('x2', rectWidth); + } classInfo.width = rectWidth; classInfo.height = classBox.height + conf.padding + 0.5 * conf.dividerMargin; @@ -291,7 +303,7 @@ export const getClassTitleString = function (classDef) { let classTitleString = classDef.id; if (classDef.type) { - classTitleString += '<' + classDef.type + '>'; + classTitleString += '<' + parseGenericTypes(classDef.type) + '>'; } return classTitleString; @@ -360,82 +372,19 @@ export const drawNote = function (elem, note, conf, diagObj) { return noteInfo; }; -export const parseMember = function (text) { - let displayText = ''; - let cssStyle = ''; - let returnType = ''; - - let visibility = ''; - let firstChar = text.substring(0, 1); - let lastChar = text.substring(text.length - 1, text.length); - - if (firstChar.match(/[#+~-]/)) { - visibility = firstChar; - } - - let noClassifierRe = /[\s\w)~]/; - if (!lastChar.match(noClassifierRe)) { - cssStyle = parseClassifier(lastChar); - } - - const startIndex = visibility === '' ? 0 : 1; - let endIndex = cssStyle === '' ? text.length : text.length - 1; - text = text.substring(startIndex, endIndex); - - const methodStart = text.indexOf('('); - const methodEnd = text.indexOf(')'); - const isMethod = methodStart > 1 && methodEnd > methodStart && methodEnd <= text.length; - - if (isMethod) { - let methodName = text.substring(0, methodStart).trim(); - - const parameters = text.substring(methodStart + 1, methodEnd); - - displayText = visibility + methodName + '(' + parseGenericTypes(parameters.trim()) + ')'; - - if (methodEnd < text.length) { - // special case: classifier after the closing parenthesis - let potentialClassifier = text.substring(methodEnd + 1, methodEnd + 2); - if (cssStyle === '' && !potentialClassifier.match(noClassifierRe)) { - cssStyle = parseClassifier(potentialClassifier); - returnType = text.substring(methodEnd + 2).trim(); - } else { - returnType = text.substring(methodEnd + 1).trim(); - } - - if (returnType !== '') { - if (returnType.charAt(0) === ':') { - returnType = returnType.substring(1).trim(); - } - returnType = ' : ' + parseGenericTypes(returnType); - displayText += returnType; - } - } - } else { - // finally - if all else fails, just send the text back as written (other than parsing for generic types) - displayText = visibility + parseGenericTypes(text); - } - - return { - displayText, - cssStyle, - }; -}; - /** * Adds a for a member in a diagram * * @param {SVGElement} textEl The element to append to - * @param {string} txt The member + * @param {string} member The member * @param {boolean} isFirst * @param {{ padding: string; textHeight: string }} conf The configuration for the member */ -const addTspan = function (textEl, txt, isFirst, conf) { - let member = parseMember(txt); - - const tSpan = textEl.append('tspan').attr('x', conf.padding).text(member.displayText); +const addTspan = function (textEl, member, isFirst, conf) { + const { displayText, cssStyle } = member.getDisplayDetails(); + const tSpan = textEl.append('tspan').attr('x', conf.padding).text(displayText); - if (member.cssStyle !== '') { + if (cssStyle !== '') { tSpan.attr('style', member.cssStyle); } @@ -444,27 +393,9 @@ const addTspan = function (textEl, txt, isFirst, conf) { } }; -/** - * Gives the styles for a classifier - * - * @param {'+' | '-' | '#' | '~' | '*' | '$'} classifier The classifier string - * @returns {string} Styling for the classifier - */ -const parseClassifier = function (classifier) { - switch (classifier) { - case '*': - return 'font-style:italic;'; - case '$': - return 'text-decoration:underline;'; - default: - return ''; - } -}; - export default { getClassTitleString, drawClass, drawEdge, drawNote, - parseMember, }; diff --git a/packages/mermaid/src/diagrams/class/svgDraw.spec.js b/packages/mermaid/src/diagrams/class/svgDraw.spec.js index e8ba9f7e1e..f068f8d626 100644 --- a/packages/mermaid/src/diagrams/class/svgDraw.spec.js +++ b/packages/mermaid/src/diagrams/class/svgDraw.spec.js @@ -1,330 +1,27 @@ import svgDraw from './svgDraw.js'; - -describe('given a string representing class method, ', function () { - it('should handle class names with generics', function () { - const classDef = { - id: 'Car', - type: 'T', - label: 'Car', - }; - - let actual = svgDraw.getClassTitleString(classDef); - expect(actual).toBe('Car'); - }); - - describe('when parsing base method declaration', function () { - it('should handle simple declaration', function () { - const str = 'foo()'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo()'); - expect(actual.cssStyle).toBe(''); - }); - - it('should handle declaration with parameters', function () { - const str = 'foo(int id)'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo(int id)'); - expect(actual.cssStyle).toBe(''); - }); - - it('should handle declaration with multiple parameters', function () { - const str = 'foo(int id, object thing)'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo(int id, object thing)'); - expect(actual.cssStyle).toBe(''); - }); - - it('should handle declaration with single item in parameters', function () { - const str = 'foo(id)'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo(id)'); - expect(actual.cssStyle).toBe(''); - }); - - it('should handle declaration with single item in parameters with extra spaces', function () { - const str = ' foo ( id) '; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo(id)'); - expect(actual.cssStyle).toBe(''); - }); - - it('should handle method declaration with generic parameter', function () { - const str = 'foo(List~int~)'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo(List)'); - expect(actual.cssStyle).toBe(''); - }); - - it('should handle method declaration with normal and generic parameter', function () { - const str = 'foo(int, List~int~)'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo(int, List)'); - expect(actual.cssStyle).toBe(''); - }); - - it('should handle declaration with return value', function () { - const str = 'foo(id) int'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo(id) : int'); - expect(actual.cssStyle).toBe(''); - }); - - it('should handle declaration with colon return value', function () { - const str = 'foo(id) : int'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo(id) : int'); - expect(actual.cssStyle).toBe(''); - }); - - it('should handle declaration with generic return value', function () { - const str = 'foo(id) List~int~'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo(id) : List'); - expect(actual.cssStyle).toBe(''); - }); - - it('should handle declaration with colon generic return value', function () { - const str = 'foo(id) : List~int~'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo(id) : List'); - expect(actual.cssStyle).toBe(''); - }); - - it('should handle method declaration with all possible markup', function () { - const str = '+foo ( List~int~ ids )* List~Item~'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('+foo(List ids) : List'); - expect(actual.cssStyle).toBe('font-style:italic;'); - }); - - it('should handle method declaration with nested generics', function () { - const str = '+foo ( List~List~int~~ ids )* List~List~Item~~'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('+foo(List> ids) : List>'); - expect(actual.cssStyle).toBe('font-style:italic;'); - }); - }); - - describe('when parsing method visibility', function () { - it('should correctly handle public', function () { - const str = '+foo()'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('+foo()'); - expect(actual.cssStyle).toBe(''); - }); - - it('should correctly handle private', function () { - const str = '-foo()'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('-foo()'); - expect(actual.cssStyle).toBe(''); - }); - - it('should correctly handle protected', function () { - const str = '#foo()'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('#foo()'); - expect(actual.cssStyle).toBe(''); - }); - - it('should correctly handle package/internal', function () { - const str = '~foo()'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('~foo()'); - expect(actual.cssStyle).toBe(''); - }); - }); - - describe('when parsing method classifier', function () { - it('should handle abstract method', function () { - const str = 'foo()*'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo()'); - expect(actual.cssStyle).toBe('font-style:italic;'); - }); - - it('should handle abstract method with return type', function () { - const str = 'foo(name: String) int*'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo(name: String) : int'); - expect(actual.cssStyle).toBe('font-style:italic;'); - }); - - it('should handle abstract method classifier after parenthesis with return type', function () { - const str = 'foo(name: String)* int'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo(name: String) : int'); - expect(actual.cssStyle).toBe('font-style:italic;'); - }); - - it('should handle static method classifier', function () { - const str = 'foo()$'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo()'); - expect(actual.cssStyle).toBe('text-decoration:underline;'); - }); - - it('should handle static method classifier with return type', function () { - const str = 'foo(name: String) int$'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo(name: String) : int'); - expect(actual.cssStyle).toBe('text-decoration:underline;'); - }); - - it('should handle static method classifier with colon and return type', function () { - const str = 'foo(name: String): int$'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo(name: String) : int'); - expect(actual.cssStyle).toBe('text-decoration:underline;'); - }); - - it('should handle static method classifier after parenthesis with return type', function () { - const str = 'foo(name: String)$ int'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo(name: String) : int'); - expect(actual.cssStyle).toBe('text-decoration:underline;'); - }); - - it('should ignore unknown character for classifier', function () { - const str = 'foo()!'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo()'); - expect(actual.cssStyle).toBe(''); - }); - }); -}); - -describe('given a string representing class member, ', function () { - describe('when parsing member declaration', function () { - it('should handle simple field', function () { - const str = 'id'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('id'); - expect(actual.cssStyle).toBe(''); - }); - - it('should handle field with type', function () { - const str = 'int id'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('int id'); - expect(actual.cssStyle).toBe(''); - }); - - it('should handle field with type (name first)', function () { - const str = 'id: int'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('id: int'); - expect(actual.cssStyle).toBe(''); - }); - - it('should handle array field', function () { - const str = 'int[] ids'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('int[] ids'); - expect(actual.cssStyle).toBe(''); - }); - - it('should handle array field (name first)', function () { - const str = 'ids: int[]'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('ids: int[]'); - expect(actual.cssStyle).toBe(''); - }); - - it('should handle field with generic type', function () { - const str = 'List~int~ ids'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('List ids'); - expect(actual.cssStyle).toBe(''); - }); - - it('should handle field with generic type (name first)', function () { - const str = 'ids: List~int~'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('ids: List'); - expect(actual.cssStyle).toBe(''); - }); - }); - - describe('when parsing classifiers', function () { - it('should handle static field', function () { - const str = 'String foo$'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('String foo'); - expect(actual.cssStyle).toBe('text-decoration:underline;'); - }); - - it('should handle static field (name first)', function () { - const str = 'foo: String$'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo: String'); - expect(actual.cssStyle).toBe('text-decoration:underline;'); - }); - - it('should handle static field with generic type', function () { - const str = 'List~String~ foo$'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('List foo'); - expect(actual.cssStyle).toBe('text-decoration:underline;'); - }); - - it('should handle static field with generic type (name first)', function () { - const str = 'foo: List~String~$'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo: List'); - expect(actual.cssStyle).toBe('text-decoration:underline;'); - }); - - it('should handle field with nested generic type', function () { - const str = 'List~List~int~~ idLists'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('List> idLists'); - expect(actual.cssStyle).toBe(''); - }); - - it('should handle field with nested generic type (name first)', function () { - const str = 'idLists: List~List~int~~'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('idLists: List>'); - expect(actual.cssStyle).toBe(''); +import { JSDOM } from 'jsdom'; + +describe('given a string representing a class, ', function () { + describe('when class name includes generic, ', function () { + it('should return correct text for generic', function () { + const classDef = { + id: 'Car', + type: 'T', + label: 'Car', + }; + + let actual = svgDraw.getClassTitleString(classDef); + expect(actual).toBe('Car'); + }); + it('should return correct text for nested generics', function () { + const classDef = { + id: 'Car', + type: 'T~T~', + label: 'Car', + }; + + let actual = svgDraw.getClassTitleString(classDef); + expect(actual).toBe('Car>'); }); }); }); diff --git a/packages/mermaid/src/diagrams/common/common.spec.js b/packages/mermaid/src/diagrams/common/common.spec.js deleted file mode 100644 index d1c68e8926..0000000000 --- a/packages/mermaid/src/diagrams/common/common.spec.js +++ /dev/null @@ -1,74 +0,0 @@ -import { sanitizeText, removeScript, parseGenericTypes } from './common.js'; - -describe('when securityLevel is antiscript, all script must be removed', function () { - /** - * @param {string} original The original text - * @param {string} result The expected sanitized text - */ - function compareRemoveScript(original, result) { - expect(removeScript(original).trim()).toEqual(result); - } - - it('should remove all script block, script inline.', function () { - const labelString = `1 - Act1: Hello 11 - Act2: - 11 - 1`; - const exactlyString = `1 - Act1: Hello 11 - Act2: - 11 - 1`; - compareRemoveScript(labelString, exactlyString); - }); - - it('should remove all javascript urls', function () { - compareRemoveScript( - `This is a clean link + clean link - and me too`, - `This is a clean link + clean link - and me too` - ); - }); - - it('should detect malicious images', function () { - compareRemoveScript(``, ``); - }); - - it('should detect iframes', function () { - compareRemoveScript( - ` - `, - '' - ); - }); -}); - -describe('Sanitize text', function () { - it('should remove script tag', function () { - const maliciousStr = 'javajavascript:script:alert(1)'; - const result = sanitizeText(maliciousStr, { - securityLevel: 'strict', - flowchart: { htmlLabels: true }, - }); - expect(result).not.toContain('javascript:alert(1)'); - }); -}); - -describe('generic parser', function () { - it('should parse generic types', function () { - expect(parseGenericTypes('test~T~')).toEqual('test'); - expect(parseGenericTypes('test~Array~Array~string~~~')).toEqual('test>>'); - expect(parseGenericTypes('test~Array~Array~string[]~~~')).toEqual( - 'test>>' - ); - expect(parseGenericTypes('test ~Array~Array~string[]~~~')).toEqual( - 'test >>' - ); - expect(parseGenericTypes('~test')).toEqual('~test'); - expect(parseGenericTypes('~test Array~string~')).toEqual('~test Array'); - }); -}); diff --git a/packages/mermaid/src/diagrams/common/common.spec.ts b/packages/mermaid/src/diagrams/common/common.spec.ts new file mode 100644 index 0000000000..4dac5b33c1 --- /dev/null +++ b/packages/mermaid/src/diagrams/common/common.spec.ts @@ -0,0 +1,87 @@ +import { sanitizeText, removeScript, parseGenericTypes, countOccurrence } from './common.js'; + +describe('when securityLevel is antiscript, all script must be removed', () => { + /** + * @param original - The original text + * @param result - The expected sanitized text + */ + function compareRemoveScript(original: string, result: string) { + expect(removeScript(original).trim()).toEqual(result); + } + + it('should remove all script block, script inline.', () => { + const labelString = `1 + Act1: Hello 11 + Act2: + 11 + 1`; + const exactlyString = `1 + Act1: Hello 11 + Act2: + 11 + 1`; + compareRemoveScript(labelString, exactlyString); + }); + + it('should remove all javascript urls', () => { + compareRemoveScript( + `This is a clean link + clean link + and me too`, + `This is a clean link + clean link + and me too` + ); + }); + + it('should detect malicious images', () => { + compareRemoveScript(``, ``); + }); + + it('should detect iframes', () => { + compareRemoveScript( + ` + `, + '' + ); + }); +}); + +describe('Sanitize text', () => { + it('should remove script tag', () => { + const maliciousStr = 'javajavascript:script:alert(1)'; + const result = sanitizeText(maliciousStr, { + securityLevel: 'strict', + flowchart: { htmlLabels: true }, + }); + expect(result).not.toContain('javascript:alert(1)'); + }); +}); + +describe('generic parser', () => { + it.each([ + ['test~T~', 'test'], + ['test~Array~Array~string~~~', 'test>>'], + ['test~Array~Array~string[]~~~', 'test>>'], + ['test ~Array~Array~string[]~~~', 'test >>'], + ['~test', '~test'], + ['~test~T~', '~test'], + ])('should parse generic types: %s to %s', (input: string, expected: string) => { + expect(parseGenericTypes(input)).toEqual(expected); + }); +}); + +it.each([ + ['', '', 0], + ['', 'x', 0], + ['test', 'x', 0], + ['test', 't', 2], + ['test', 'te', 1], + ['test~T~', '~', 2], + ['test~Array~Array~string~~~', '~', 6], +])( + 'should count `%s` to contain occurrences of `%s` to be `%i`', + (str: string, substring: string, count: number) => { + expect(countOccurrence(str, substring)).toEqual(count); + } +); diff --git a/packages/mermaid/src/diagrams/common/common.ts b/packages/mermaid/src/diagrams/common/common.ts index 243c0cbf25..e0ca2929db 100644 --- a/packages/mermaid/src/diagrams/common/common.ts +++ b/packages/mermaid/src/diagrams/common/common.ts @@ -1,6 +1,7 @@ import DOMPurify from 'dompurify'; -import { MermaidConfig } from '../../config.type.js'; +import type { MermaidConfig } from '../../config.type.js'; +// Remove and ignore br:s export const lineBreakRegex = //gi; /** @@ -177,23 +178,80 @@ export const getMin = function (...values: number[]): number { * @param text - The text to convert * @returns The converted string */ -export const parseGenericTypes = function (text: string): string { - let cleanedText = text; +export const parseGenericTypes = function (input: string): string { + const inputSets = input.split(/(,)/); + const output = []; - if (text.split('~').length - 1 >= 2) { - let newCleanedText = cleanedText; + for (let i = 0; i < inputSets.length; i++) { + let thisSet = inputSets[i]; - // use a do...while loop instead of replaceAll to detect recursion - // e.g. Array~Array~T~~ - do { - cleanedText = newCleanedText; - newCleanedText = cleanedText.replace(/~([^\s,:;]+)~/, '<$1>'); - } while (newCleanedText != cleanedText); + // if the original input included a value such as "~K, V~"", these will be split into + // an array of ["~K",","," V~"]. + // This means that on each call of processSet, there will only be 1 ~ present + // To account for this, if we encounter a ",", we are checking the previous and next sets in the array + // to see if they contain matching ~'s + // in which case we are assuming that they should be rejoined and sent to be processed + if (thisSet === ',' && i > 0 && i + 1 < inputSets.length) { + const previousSet = inputSets[i - 1]; + const nextSet = inputSets[i + 1]; - return parseGenericTypes(newCleanedText); - } else { - return cleanedText; + if (shouldCombineSets(previousSet, nextSet)) { + thisSet = previousSet + ',' + nextSet; + i++; // Move the index forward to skip the next iteration since we're combining sets + output.pop(); + } + } + + output.push(processSet(thisSet)); } + + return output.join(''); +}; + +export const countOccurrence = (string: string, substring: string): number => { + return Math.max(0, string.split(substring).length - 1); +}; + +const shouldCombineSets = (previousSet: string, nextSet: string): boolean => { + const prevCount = countOccurrence(previousSet, '~'); + const nextCount = countOccurrence(nextSet, '~'); + + return prevCount === 1 && nextCount === 1; +}; + +const processSet = (input: string): string => { + const tildeCount = countOccurrence(input, '~'); + let hasStartingTilde = false; + + if (tildeCount <= 1) { + return input; + } + + // If there is an odd number of tildes, and the input starts with a tilde, we need to remove it and add it back in later + if (tildeCount % 2 !== 0 && input.startsWith('~')) { + input = input.substring(1); + hasStartingTilde = true; + } + + const chars = [...input]; + + let first = chars.indexOf('~'); + let last = chars.lastIndexOf('~'); + + while (first !== -1 && last !== -1 && first !== last) { + chars[first] = '<'; + chars[last] = '>'; + + first = chars.indexOf('~'); + last = chars.lastIndexOf('~'); + } + + // Add the starting tilde back in if we removed it + if (hasStartingTilde) { + chars.unshift('~'); + } + + return chars.join(''); }; export default { diff --git a/packages/mermaid/src/diagrams/common/commonDb.ts b/packages/mermaid/src/diagrams/common/commonDb.ts new file mode 100644 index 0000000000..e4b9c3539f --- /dev/null +++ b/packages/mermaid/src/diagrams/common/commonDb.ts @@ -0,0 +1,32 @@ +import { sanitizeText as _sanitizeText } from './common.js'; +import { getConfig } from '../../config.js'; + +let accTitle = ''; +let diagramTitle = ''; +let accDescription = ''; + +const sanitizeText = (txt: string): string => _sanitizeText(txt, getConfig()); + +export const clear = (): void => { + accTitle = ''; + accDescription = ''; + diagramTitle = ''; +}; + +export const setAccTitle = (txt: string): void => { + accTitle = sanitizeText(txt).replace(/^\s+/g, ''); +}; + +export const getAccTitle = (): string => accTitle; + +export const setAccDescription = (txt: string): void => { + accDescription = sanitizeText(txt).replace(/\n\s+/g, '\n'); +}; + +export const getAccDescription = (): string => accDescription; + +export const setDiagramTitle = (txt: string): void => { + diagramTitle = sanitizeText(txt); +}; + +export const getDiagramTitle = (): string => diagramTitle; diff --git a/packages/mermaid/src/diagrams/common/commonTypes.ts b/packages/mermaid/src/diagrams/common/commonTypes.ts new file mode 100644 index 0000000000..84c26db6e1 --- /dev/null +++ b/packages/mermaid/src/diagrams/common/commonTypes.ts @@ -0,0 +1,58 @@ +export interface RectData { + x: number; + y: number; + fill: string; + width: number; + height: number; + stroke: string; + class?: string; + color?: string; + rx?: number; + ry?: number; + attrs?: Record; + anchor?: string; +} + +export interface Bound { + startx: number; + stopx: number; + starty: number; + stopy: number; + fill: string; + stroke: string; +} + +export interface TextData { + x: number; + y: number; + anchor: string; + text: string; + textMargin: number; + class?: string; +} + +export interface TextObject { + x: number; + y: number; + width: number; + height: number; + fill?: string; + anchor?: string; + 'text-anchor': string; + style: string; + textMargin: number; + rx: number; + ry: number; + tspan: boolean; + valign?: string; +} + +export type D3RectElement = d3.Selection; + +export type D3UseElement = d3.Selection; + +export type D3ImageElement = d3.Selection; + +export type D3TextElement = d3.Selection; + +export type D3TSpanElement = d3.Selection; diff --git a/packages/mermaid/src/diagrams/common/svgDrawCommon.js b/packages/mermaid/src/diagrams/common/svgDrawCommon.js deleted file mode 100644 index 9a4ce8aa2b..0000000000 --- a/packages/mermaid/src/diagrams/common/svgDrawCommon.js +++ /dev/null @@ -1,114 +0,0 @@ -import { sanitizeUrl } from '@braintree/sanitize-url'; - -export const drawRect = function (elem, rectData) { - const rectElem = elem.append('rect'); - rectElem.attr('x', rectData.x); - rectElem.attr('y', rectData.y); - rectElem.attr('fill', rectData.fill); - rectElem.attr('stroke', rectData.stroke); - rectElem.attr('width', rectData.width); - rectElem.attr('height', rectData.height); - rectElem.attr('rx', rectData.rx); - rectElem.attr('ry', rectData.ry); - - if (rectData.attrs !== 'undefined' && rectData.attrs !== null) { - for (let attrKey in rectData.attrs) { - rectElem.attr(attrKey, rectData.attrs[attrKey]); - } - } - - if (rectData.class !== 'undefined') { - rectElem.attr('class', rectData.class); - } - - return rectElem; -}; - -/** - * Draws a background rectangle - * - * @param {any} elem Diagram (reference for bounds) - * @param {any} bounds Shape of the rectangle - */ -export const drawBackgroundRect = function (elem, bounds) { - const rectElem = drawRect(elem, { - x: bounds.startx, - y: bounds.starty, - width: bounds.stopx - bounds.startx, - height: bounds.stopy - bounds.starty, - fill: bounds.fill, - stroke: bounds.stroke, - class: 'rect', - }); - rectElem.lower(); -}; - -export const drawText = function (elem, textData) { - // Remove and ignore br:s - const nText = textData.text.replace(//gi, ' '); - - const textElem = elem.append('text'); - textElem.attr('x', textData.x); - textElem.attr('y', textData.y); - textElem.attr('class', 'legend'); - - textElem.style('text-anchor', textData.anchor); - - if (textData.class !== undefined) { - textElem.attr('class', textData.class); - } - - const span = textElem.append('tspan'); - span.attr('x', textData.x + textData.textMargin * 2); - span.text(nText); - - return textElem; -}; - -export const drawImage = function (elem, x, y, link) { - const imageElem = elem.append('image'); - imageElem.attr('x', x); - imageElem.attr('y', y); - var sanitizedLink = sanitizeUrl(link); - imageElem.attr('xlink:href', sanitizedLink); -}; - -export const drawEmbeddedImage = function (elem, x, y, link) { - const imageElem = elem.append('use'); - imageElem.attr('x', x); - imageElem.attr('y', y); - const sanitizedLink = sanitizeUrl(link); - imageElem.attr('xlink:href', '#' + sanitizedLink); -}; - -export const getNoteRect = function () { - return { - x: 0, - y: 0, - width: 100, - height: 100, - fill: '#EDF2AE', - stroke: '#666', - anchor: 'start', - rx: 0, - ry: 0, - }; -}; - -export const getTextObj = function () { - return { - x: 0, - y: 0, - width: 100, - height: 100, - fill: undefined, - anchor: undefined, - 'text-anchor': 'start', - style: '#666', - textMargin: 0, - rx: 0, - ry: 0, - tspan: true, - valign: undefined, - }; -}; diff --git a/packages/mermaid/src/diagrams/common/svgDrawCommon.ts b/packages/mermaid/src/diagrams/common/svgDrawCommon.ts new file mode 100644 index 0000000000..706d43ab9d --- /dev/null +++ b/packages/mermaid/src/diagrams/common/svgDrawCommon.ts @@ -0,0 +1,126 @@ +import { sanitizeUrl } from '@braintree/sanitize-url'; +import type { Group, SVG } from '../../diagram-api/types.js'; +import type { + Bound, + D3ImageElement, + D3RectElement, + D3TSpanElement, + D3TextElement, + D3UseElement, + RectData, + TextData, + TextObject, +} from './commonTypes.js'; +import { lineBreakRegex } from './common.js'; + +export const drawRect = (element: SVG | Group, rectData: RectData): D3RectElement => { + const rectElement: D3RectElement = element.append('rect'); + rectElement.attr('x', rectData.x); + rectElement.attr('y', rectData.y); + rectElement.attr('fill', rectData.fill); + rectElement.attr('stroke', rectData.stroke); + rectElement.attr('width', rectData.width); + rectElement.attr('height', rectData.height); + rectData.rx !== undefined && rectElement.attr('rx', rectData.rx); + rectData.ry !== undefined && rectElement.attr('ry', rectData.ry); + + if (rectData.attrs !== undefined) { + for (const attrKey in rectData.attrs) { + rectElement.attr(attrKey, rectData.attrs[attrKey]); + } + } + + rectData.class !== undefined && rectElement.attr('class', rectData.class); + + return rectElement; +}; + +/** + * Draws a background rectangle + * + * @param element - Diagram (reference for bounds) + * @param bounds - Shape of the rectangle + */ +export const drawBackgroundRect = (element: SVG | Group, bounds: Bound): void => { + const rectData: RectData = { + x: bounds.startx, + y: bounds.starty, + width: bounds.stopx - bounds.startx, + height: bounds.stopy - bounds.starty, + fill: bounds.fill, + stroke: bounds.stroke, + class: 'rect', + }; + const rectElement: D3RectElement = drawRect(element, rectData); + rectElement.lower(); +}; + +export const drawText = (element: SVG | Group, textData: TextData): D3TextElement => { + const nText: string = textData.text.replace(lineBreakRegex, ' '); + + const textElem: D3TextElement = element.append('text'); + textElem.attr('x', textData.x); + textElem.attr('y', textData.y); + textElem.attr('class', 'legend'); + + textElem.style('text-anchor', textData.anchor); + textData.class !== undefined && textElem.attr('class', textData.class); + + const tspan: D3TSpanElement = textElem.append('tspan'); + tspan.attr('x', textData.x + textData.textMargin * 2); + tspan.text(nText); + + return textElem; +}; + +export const drawImage = (elem: SVG | Group, x: number, y: number, link: string): void => { + const imageElement: D3ImageElement = elem.append('image'); + imageElement.attr('x', x); + imageElement.attr('y', y); + const sanitizedLink: string = sanitizeUrl(link); + imageElement.attr('xlink:href', sanitizedLink); +}; + +export const drawEmbeddedImage = ( + element: SVG | Group, + x: number, + y: number, + link: string +): void => { + const imageElement: D3UseElement = element.append('use'); + imageElement.attr('x', x); + imageElement.attr('y', y); + const sanitizedLink: string = sanitizeUrl(link); + imageElement.attr('xlink:href', `#${sanitizedLink}`); +}; + +export const getNoteRect = (): RectData => { + const noteRectData: RectData = { + x: 0, + y: 0, + width: 100, + height: 100, + fill: '#EDF2AE', + stroke: '#666', + anchor: 'start', + rx: 0, + ry: 0, + }; + return noteRectData; +}; + +export const getTextObj = (): TextObject => { + const testObject: TextObject = { + x: 0, + y: 0, + width: 100, + height: 100, + 'text-anchor': 'start', + style: '#666', + textMargin: 0, + rx: 0, + ry: 0, + tspan: true, + }; + return testObject; +}; diff --git a/packages/mermaid/src/diagrams/er/erDb.js b/packages/mermaid/src/diagrams/er/erDb.js index 2f5116cbfe..01bbb585cd 100644 --- a/packages/mermaid/src/diagrams/er/erDb.js +++ b/packages/mermaid/src/diagrams/er/erDb.js @@ -10,7 +10,7 @@ import { clear as commonClear, setDiagramTitle, getDiagramTitle, -} from '../../commonDb.js'; +} from '../common/commonDb.js'; let entities = {}; let relationships = []; @@ -32,10 +32,13 @@ export const parseDirective = function (statement, context, type) { mermaidAPI.parseDirective(this, statement, context, type); }; -const addEntity = function (name) { +const addEntity = function (name, alias = undefined) { if (entities[name] === undefined) { - entities[name] = { attributes: [] }; + entities[name] = { attributes: [], alias: alias }; log.info('Added new entity :', name); + } else if (entities[name] && !entities[name].alias && alias) { + entities[name].alias = alias; + log.info(`Add alias '${alias}' to entity '${name}'`); } return entities[name]; diff --git a/packages/mermaid/src/diagrams/er/erRenderer.js b/packages/mermaid/src/diagrams/er/erRenderer.js index 93e22732a1..0c19d491b6 100644 --- a/packages/mermaid/src/diagrams/er/erRenderer.js +++ b/packages/mermaid/src/diagrams/er/erRenderer.js @@ -326,7 +326,7 @@ const drawEntities = function (svgNode, entities, graph) { .style('text-anchor', 'middle') .style('font-family', getConfig().fontFamily) .style('font-size', conf.fontSize + 'px') - .text(entityName); + .text(entities[entityName].alias ?? entityName); const { width: entityWidth, height: entityHeight } = drawAttributes( groupNode, @@ -555,7 +555,6 @@ const drawRelationshipFromLayout = function (svg, rel, g, insert, diagObj) { export const draw = function (text, id, _version, diagObj) { conf = getConfig().er; log.info('Drawing ER diagram'); - // diag.db.clear(); const securityLevel = getConfig().securityLevel; // Handle root and Document for when rendering in sandbox mode let sandboxElement; @@ -568,13 +567,6 @@ export const draw = function (text, id, _version, diagObj) { : select('body'); // const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document; - // Parse the text to populate erDb - // try { - // parser.parse(text); - // } catch (err) { - // log.debug('Parsing failed'); - // } - // Get a reference to the svg node that contains the text const svg = root.select(`[id='${id}']`); diff --git a/packages/mermaid/src/diagrams/er/parser/erDiagram.jison b/packages/mermaid/src/diagrams/er/parser/erDiagram.jison index 0c6820b495..f391346077 100644 --- a/packages/mermaid/src/diagrams/er/parser/erDiagram.jison +++ b/packages/mermaid/src/diagrams/er/parser/erDiagram.jison @@ -30,11 +30,13 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili \s+ /* skip whitespace in block */ \b((?:PK)|(?:FK)|(?:UK))\b return 'ATTRIBUTE_KEY' (.*?)[~](.*?)*[~] return 'ATTRIBUTE_WORD'; -[A-Za-z_][A-Za-z0-9\-_\[\]\(\)]* return 'ATTRIBUTE_WORD' +[\*A-Za-z_][A-Za-z0-9\-_\[\]\(\)]* return 'ATTRIBUTE_WORD' \"[^"]*\" return 'COMMENT'; [\n]+ /* nothing */ "}" { this.popState(); return 'BLOCK_STOP'; } . return yytext[0]; +"[" return 'SQS'; +"]" return 'SQE'; "one or zero" return 'ZERO_OR_ONE'; "one or more" return 'ONE_OR_MORE'; @@ -64,7 +66,7 @@ o\{ return 'ZERO_OR_MORE'; "optionally to" return 'NON_IDENTIFYING'; \.\- return 'NON_IDENTIFYING'; \-\. return 'NON_IDENTIFYING'; -[A-Za-z][A-Za-z0-9\-_]* return 'ALPHANUM'; +[A-Za-z_][A-Za-z0-9\-_]* return 'ALPHANUM'; . return yytext[0]; <> return 'EOF'; @@ -102,17 +104,21 @@ statement yy.addEntity($1); yy.addEntity($3); yy.addRelationship($1, $5, $3, $2); - /*console.log($1 + $2 + $3 + ':' + $5);*/ } | entityName BLOCK_START attributes BLOCK_STOP { - /* console.log('detected block'); */ yy.addEntity($1); yy.addAttributes($1, $3); - /* console.log('handled block'); */ } | entityName BLOCK_START BLOCK_STOP { yy.addEntity($1); } | entityName { yy.addEntity($1); } + | entityName SQS entityName SQE BLOCK_START attributes BLOCK_STOP + { + yy.addEntity($1, $3); + yy.addAttributes($1, $6); + } + | entityName SQS entityName SQE BLOCK_START BLOCK_STOP { yy.addEntity($1, $3); } + | entityName SQS entityName SQE { yy.addEntity($1, $3); } | title title_value { $$=$2.trim();yy.setAccTitle($$); } | acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); } | acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); } diff --git a/packages/mermaid/src/diagrams/er/parser/erDiagram.spec.js b/packages/mermaid/src/diagrams/er/parser/erDiagram.spec.js index 6fdc9eaa73..825af737a6 100644 --- a/packages/mermaid/src/diagrams/er/parser/erDiagram.spec.js +++ b/packages/mermaid/src/diagrams/er/parser/erDiagram.spec.js @@ -33,7 +33,7 @@ describe('when parsing ER diagram it...', function () { describe('has non A-Za-z0-9_- chars', function () { // these were entered using the Mac keyboard utility. const chars = - "~ ` ! @ # $ ^ & * ( ) - _ = + [ ] { } | / ; : ' . ? ¡ ⁄ ™ € £ ‹ ¢ › ∞ fi § ‡ • ° ª · º ‚ ≠ ± œ Œ ∑ „ ® † ˇ ¥ Á ¨ ˆ ˆ Ø π ∏ “ « » å Å ß Í ∂ Î ƒ Ï © ˙ Ó ∆ Ô ˚  ¬ Ò … Ú æ Æ Ω ¸ ≈ π ˛ ç Ç √ ◊ ∫ ı ˜ µ  ≤ ¯ ≥ ˘ ÷ ¿"; + "~ ` ! @ # $ ^ & * ( ) - = + [ ] { } | / ; : ' . ? ¡ ⁄ ™ € £ ‹ ¢ › ∞ fi § ‡ • ° ª · º ‚ ≠ ± œ Œ ∑ „ ® † ˇ ¥ Á ¨ ˆ ˆ Ø π ∏ “ « » å Å ß Í ∂ Î ƒ Ï © ˙ Ó ∆ Ô ˚  ¬ Ò … Ú æ Æ Ω ¸ ≈ π ˛ ç Ç √ ◊ ∫ ı ˜ µ  ≤ ¯ ≥ ˘ ÷ ¿"; const allowed = chars.split(' '); allowed.forEach((allowedChar) => { @@ -133,6 +133,50 @@ describe('when parsing ER diagram it...', function () { const entities = erDb.getEntities(); expect(entities.hasOwnProperty(hyphensUnderscore)).toBe(true); }); + + it('can have an alias', function () { + const entity = 'foo'; + const alias = 'bar'; + erDiagram.parser.parse(`erDiagram\n${entity}["${alias}"]\n`); + const entities = erDb.getEntities(); + expect(entities.hasOwnProperty(entity)).toBe(true); + expect(entities[entity].alias).toBe(alias); + }); + + it('can have an alias even if the relationship is defined before class', function () { + const firstEntity = 'foo'; + const secondEntity = 'bar'; + const alias = 'batman'; + erDiagram.parser.parse( + `erDiagram\n${firstEntity} ||--o| ${secondEntity} : rel\nclass ${firstEntity}["${alias}"]\n` + ); + const entities = erDb.getEntities(); + expect(entities.hasOwnProperty(firstEntity)).toBe(true); + expect(entities.hasOwnProperty(secondEntity)).toBe(true); + expect(entities[firstEntity].alias).toBe(alias); + expect(entities[secondEntity].alias).toBeUndefined(); + }); + + it('can have an alias even if the relationship is defined after class', function () { + const firstEntity = 'foo'; + const secondEntity = 'bar'; + const alias = 'batman'; + erDiagram.parser.parse( + `erDiagram\nclass ${firstEntity}["${alias}"]\n${firstEntity} ||--o| ${secondEntity} : rel\n` + ); + const entities = erDb.getEntities(); + expect(entities.hasOwnProperty(firstEntity)).toBe(true); + expect(entities.hasOwnProperty(secondEntity)).toBe(true); + expect(entities[firstEntity].alias).toBe(alias); + expect(entities[secondEntity].alias).toBeUndefined(); + }); + + it('can start with an underscore', function () { + const entity = '_foo'; + erDiagram.parser.parse(`erDiagram\n${entity}\n`); + const entities = erDb.getEntities(); + expect(entities.hasOwnProperty(entity)).toBe(true); + }); }); describe('attribute name', () => { @@ -154,6 +198,26 @@ describe('when parsing ER diagram it...', function () { expect(entities[entity].attributes[2].attributeName).toBe('author-ref[name](1)'); }); + it('should allow asterisk at the start of attribute name', function () { + const entity = 'BOOK'; + const attribute = 'string *title'; + + erDiagram.parser.parse(`erDiagram\n${entity}{\n${attribute}}`); + const entities = erDb.getEntities(); + expect(Object.keys(entities).length).toBe(1); + expect(entities[entity].attributes.length).toBe(1); + }); + + it('should allow asterisks at the start of attribute declared with type and name', () => { + const entity = 'BOOK'; + const attribute = 'id *the_Primary_Key'; + + erDiagram.parser.parse(`erDiagram\n${entity} {\n${attribute}}`); + const entities = erDb.getEntities(); + expect(Object.keys(entities).length).toBe(1); + expect(entities[entity].attributes.length).toBe(1); + }); + it('should not allow leading numbers, dashes or brackets', function () { const entity = 'BOOK'; const nonLeadingChars = '0-[]()'; diff --git a/packages/mermaid/src/diagrams/error/errorDiagram.ts b/packages/mermaid/src/diagrams/error/errorDiagram.ts index 76efdb0ae5..284dfd7447 100644 --- a/packages/mermaid/src/diagrams/error/errorDiagram.ts +++ b/packages/mermaid/src/diagrams/error/errorDiagram.ts @@ -1,23 +1,15 @@ -import { DiagramDefinition } from '../../diagram-api/types.js'; -import styles from './styles.js'; -import renderer from './errorRenderer.js'; -export const diagram: DiagramDefinition = { - db: { - clear: () => { - // Quite ok, clear needs to be there for error to work as a regular diagram - }, - }, - styles, +import type { DiagramDefinition } from '../../diagram-api/types.js'; +import { renderer } from './errorRenderer.js'; + +const diagram: DiagramDefinition = { + db: {}, renderer, parser: { parser: { yy: {} }, - parse: () => { - // no op + parse: (): void => { + return; }, }, - init: () => { - // no op - }, }; export default diagram; diff --git a/packages/mermaid/src/diagrams/error/errorRenderer.ts b/packages/mermaid/src/diagrams/error/errorRenderer.ts index aa0e9e8164..a8e738e5fd 100644 --- a/packages/mermaid/src/diagrams/error/errorRenderer.ts +++ b/packages/mermaid/src/diagrams/error/errorRenderer.ts @@ -1,100 +1,81 @@ -/** Created by knut on 14-12-11. */ -// @ts-ignore TODO: Investigate D3 issue -import { select } from 'd3'; import { log } from '../../logger.js'; -import { getErrorMessage } from '../../utils.js'; - -/** - * Merges the value of `conf` with the passed `cnf` - * - * @param cnf - Config to merge - */ -export const setConf = function () { - // no-op -}; +import type { Group, SVG } from '../../diagram-api/types.js'; +import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; +import { configureSvgSize } from '../../setupGraphViewbox.js'; /** * Draws a an info picture in the tag with id: id based on the graph definition in text. * * @param _text - Mermaid graph definition. * @param id - The text for the error - * @param mermaidVersion - The version + * @param version - The version */ -export const draw = (_text: string, id: string, mermaidVersion: string) => { - try { - log.debug('Renering svg for syntax error\n'); +export const draw = (_text: string, id: string, version: string) => { + log.debug('renering svg for syntax error\n'); - const svg = select('#' + id); + const svg: SVG = selectSvgElement(id); + svg.attr('viewBox', '0 0 2412 512'); + configureSvgSize(svg, 100, 512, true); - const g = svg.append('g'); + const g: Group = svg.append('g'); + g.append('path') + .attr('class', 'error-icon') + .attr( + 'd', + 'm411.313,123.313c6.25-6.25 6.25-16.375 0-22.625s-16.375-6.25-22.625,0l-32,32-9.375,9.375-20.688-20.688c-12.484-12.5-32.766-12.5-45.25,0l-16,16c-1.261,1.261-2.304,2.648-3.31,4.051-21.739-8.561-45.324-13.426-70.065-13.426-105.867,0-192,86.133-192,192s86.133,192 192,192 192-86.133 192-192c0-24.741-4.864-48.327-13.426-70.065 1.402-1.007 2.79-2.049 4.051-3.31l16-16c12.5-12.492 12.5-32.758 0-45.25l-20.688-20.688 9.375-9.375 32.001-31.999zm-219.313,100.687c-52.938,0-96,43.063-96,96 0,8.836-7.164,16-16,16s-16-7.164-16-16c0-70.578 57.422-128 128-128 8.836,0 16,7.164 16,16s-7.164,16-16,16z' + ); - g.append('path') - .attr('class', 'error-icon') - .attr( - 'd', - 'm411.313,123.313c6.25-6.25 6.25-16.375 0-22.625s-16.375-6.25-22.625,0l-32,32-9.375,9.375-20.688-20.688c-12.484-12.5-32.766-12.5-45.25,0l-16,16c-1.261,1.261-2.304,2.648-3.31,4.051-21.739-8.561-45.324-13.426-70.065-13.426-105.867,0-192,86.133-192,192s86.133,192 192,192 192-86.133 192-192c0-24.741-4.864-48.327-13.426-70.065 1.402-1.007 2.79-2.049 4.051-3.31l16-16c12.5-12.492 12.5-32.758 0-45.25l-20.688-20.688 9.375-9.375 32.001-31.999zm-219.313,100.687c-52.938,0-96,43.063-96,96 0,8.836-7.164,16-16,16s-16-7.164-16-16c0-70.578 57.422-128 128-128 8.836,0 16,7.164 16,16s-7.164,16-16,16z' - ); + g.append('path') + .attr('class', 'error-icon') + .attr( + 'd', + 'm459.02,148.98c-6.25-6.25-16.375-6.25-22.625,0s-6.25,16.375 0,22.625l16,16c3.125,3.125 7.219,4.688 11.313,4.688 4.094,0 8.188-1.563 11.313-4.688 6.25-6.25 6.25-16.375 0-22.625l-16.001-16z' + ); - g.append('path') - .attr('class', 'error-icon') - .attr( - 'd', - 'm459.02,148.98c-6.25-6.25-16.375-6.25-22.625,0s-6.25,16.375 0,22.625l16,16c3.125,3.125 7.219,4.688 11.313,4.688 4.094,0 8.188-1.563 11.313-4.688 6.25-6.25 6.25-16.375 0-22.625l-16.001-16z' - ); + g.append('path') + .attr('class', 'error-icon') + .attr( + 'd', + 'm340.395,75.605c3.125,3.125 7.219,4.688 11.313,4.688 4.094,0 8.188-1.563 11.313-4.688 6.25-6.25 6.25-16.375 0-22.625l-16-16c-6.25-6.25-16.375-6.25-22.625,0s-6.25,16.375 0,22.625l15.999,16z' + ); - g.append('path') - .attr('class', 'error-icon') - .attr( - 'd', - 'm340.395,75.605c3.125,3.125 7.219,4.688 11.313,4.688 4.094,0 8.188-1.563 11.313-4.688 6.25-6.25 6.25-16.375 0-22.625l-16-16c-6.25-6.25-16.375-6.25-22.625,0s-6.25,16.375 0,22.625l15.999,16z' - ); + g.append('path') + .attr('class', 'error-icon') + .attr( + 'd', + 'm400,64c8.844,0 16-7.164 16-16v-32c0-8.836-7.156-16-16-16-8.844,0-16,7.164-16,16v32c0,8.836 7.156,16 16,16z' + ); - g.append('path') - .attr('class', 'error-icon') - .attr( - 'd', - 'm400,64c8.844,0 16-7.164 16-16v-32c0-8.836-7.156-16-16-16-8.844,0-16,7.164-16,16v32c0,8.836 7.156,16 16,16z' - ); + g.append('path') + .attr('class', 'error-icon') + .attr( + 'd', + 'm496,96.586h-32c-8.844,0-16,7.164-16,16 0,8.836 7.156,16 16,16h32c8.844,0 16-7.164 16-16 0-8.836-7.156-16-16-16z' + ); - g.append('path') - .attr('class', 'error-icon') - .attr( - 'd', - 'm496,96.586h-32c-8.844,0-16,7.164-16,16 0,8.836 7.156,16 16,16h32c8.844,0 16-7.164 16-16 0-8.836-7.156-16-16-16z' - ); + g.append('path') + .attr('class', 'error-icon') + .attr( + 'd', + 'm436.98,75.605c3.125,3.125 7.219,4.688 11.313,4.688 4.094,0 8.188-1.563 11.313-4.688l32-32c6.25-6.25 6.25-16.375 0-22.625s-16.375-6.25-22.625,0l-32,32c-6.251,6.25-6.251,16.375-0.001,22.625z' + ); - g.append('path') - .attr('class', 'error-icon') - .attr( - 'd', - 'm436.98,75.605c3.125,3.125 7.219,4.688 11.313,4.688 4.094,0 8.188-1.563 11.313-4.688l32-32c6.25-6.25 6.25-16.375 0-22.625s-16.375-6.25-22.625,0l-32,32c-6.251,6.25-6.251,16.375-0.001,22.625z' - ); - - g.append('text') // text label for the x axis - .attr('class', 'error-text') - .attr('x', 1440) - .attr('y', 250) - .attr('font-size', '150px') - .style('text-anchor', 'middle') - .text('Syntax error in text'); - g.append('text') // text label for the x axis - .attr('class', 'error-text') - .attr('x', 1250) - .attr('y', 400) - .attr('font-size', '100px') - .style('text-anchor', 'middle') - .text('mermaid version ' + mermaidVersion); - - svg.attr('height', 100); - svg.attr('width', 500); - svg.attr('viewBox', '768 0 912 512'); - } catch (e) { - log.error('Error while rendering info diagram'); - log.error(getErrorMessage(e)); - } + g.append('text') // text label for the x axis + .attr('class', 'error-text') + .attr('x', 1440) + .attr('y', 250) + .attr('font-size', '150px') + .style('text-anchor', 'middle') + .text('Syntax error in text'); + g.append('text') // text label for the x axis + .attr('class', 'error-text') + .attr('x', 1250) + .attr('y', 400) + .attr('font-size', '100px') + .style('text-anchor', 'middle') + .text(`mermaid version ${version}`); }; -export default { - setConf, - draw, -}; +export const renderer = { draw }; + +export default renderer; diff --git a/packages/mermaid/src/diagrams/error/styles.js b/packages/mermaid/src/diagrams/error/styles.js deleted file mode 100644 index 0b0729813d..0000000000 --- a/packages/mermaid/src/diagrams/error/styles.js +++ /dev/null @@ -1,3 +0,0 @@ -const getStyles = () => ``; - -export default getStyles; diff --git a/packages/mermaid/src/diagrams/flowchart/elk/flowRenderer-elk.js b/packages/mermaid/src/diagrams/flowchart/elk/flowRenderer-elk.js index 5ed06723e9..c7bfdf5246 100644 --- a/packages/mermaid/src/diagrams/flowchart/elk/flowRenderer-elk.js +++ b/packages/mermaid/src/diagrams/flowchart/elk/flowRenderer-elk.js @@ -655,14 +655,7 @@ const addMarkersToEdge = function (svgPath, edgeData, diagramType, arrowMarkerAb */ 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 {}; - } + return diagObj.db.getClasses(); }; const addSubGraphs = function (db) { @@ -766,14 +759,8 @@ const insertChildren = (nodeArray, parentLookupDb) => { */ export const draw = async function (text, id, _version, diagObj) { - // Add temporary render element - diagObj.db.clear(); nodeDb = {}; portPos = {}; - diagObj.db.setGen('gen-2'); - // Parse the graph definition - diagObj.parser.parse(text); - const renderEl = select('body').append('div').attr('style', 'height:400px').attr('id', 'cy'); let graph = { id: 'root', diff --git a/packages/mermaid/src/diagrams/flowchart/elk/render-utils.spec.ts b/packages/mermaid/src/diagrams/flowchart/elk/render-utils.spec.ts index d048b07a37..046ed43c1b 100644 --- a/packages/mermaid/src/diagrams/flowchart/elk/render-utils.spec.ts +++ b/packages/mermaid/src/diagrams/flowchart/elk/render-utils.spec.ts @@ -1,4 +1,5 @@ -import { findCommonAncestor, TreeData } from './render-utils.js'; +import type { TreeData } from './render-utils.js'; +import { findCommonAncestor } from './render-utils.js'; describe('when rendering a flowchart using elk ', () => { let lookupDb: TreeData; beforeEach(() => { diff --git a/packages/mermaid/src/diagrams/flowchart/flowDb.js b/packages/mermaid/src/diagrams/flowchart/flowDb.js index f7e1a38db5..2bb50abee1 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDb.js +++ b/packages/mermaid/src/diagrams/flowchart/flowDb.js @@ -12,7 +12,7 @@ import { clear as commonClear, setDiagramTitle, getDiagramTitle, -} from '../../commonDb.js'; +} from '../common/commonDb.js'; const MERMAID_DOM_ID_PREFIX = 'flowchart-'; let vertexCounter = 0; @@ -208,21 +208,22 @@ export const updateLink = function (positions, style) { }); }; -export const addClass = function (id, style) { - if (classes[id] === undefined) { - classes[id] = { id: id, styles: [], textStyles: [] }; - } +export const addClass = function (ids, style) { + ids.split(',').forEach(function (id) { + if (classes[id] === undefined) { + classes[id] = { id, styles: [], textStyles: [] }; + } - if (style !== undefined && style !== null) { - style.forEach(function (s) { - if (s.match('color')) { - const newStyle1 = s.replace('fill', 'bgFill'); - const newStyle2 = newStyle1.replace('color', 'fill'); - classes[id].textStyles.push(newStyle2); - } - classes[id].styles.push(s); - }); - } + if (style !== undefined && style !== null) { + style.forEach(function (s) { + if (s.match('color')) { + const newStyle = s.replace('fill', 'bgFill').replace('color', 'fill'); + classes[id].textStyles.push(newStyle); + } + classes[id].styles.push(s); + }); + } + }); }; /** @@ -341,7 +342,10 @@ export const setLink = function (ids, linkStr, target) { setClass(ids, 'clickable'); }; export const getTooltip = function (id) { - return tooltips[id]; + if (tooltips.hasOwnProperty(id)) { + return tooltips[id]; + } + return undefined; }; /** @@ -442,7 +446,7 @@ export const clear = function (ver = 'gen-1') { subGraphs = []; subGraphLookup = {}; subCount = 0; - tooltips = []; + tooltips = {}; firstGraphFlag = true; version = ver; commonClear(); diff --git a/packages/mermaid/src/diagrams/flowchart/flowDb.spec.js b/packages/mermaid/src/diagrams/flowchart/flowDb.spec.js index 09f8c8678d..6f04ca70a1 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDb.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/flowDb.spec.js @@ -41,3 +41,26 @@ describe('flow db subgraphs', () => { }); }); }); + +describe('flow db addClass', () => { + beforeEach(() => { + flowDb.clear(); + }); + it('should detect many classes', () => { + flowDb.addClass('a,b', ['stroke-width: 8px']); + const classes = flowDb.getClasses(); + + expect(classes.hasOwnProperty('a')).toBe(true); + expect(classes.hasOwnProperty('b')).toBe(true); + expect(classes['a']['styles']).toEqual(['stroke-width: 8px']); + expect(classes['b']['styles']).toEqual(['stroke-width: 8px']); + }); + + it('should detect single class', () => { + flowDb.addClass('a', ['stroke-width: 8px']); + const classes = flowDb.getClasses(); + + expect(classes.hasOwnProperty('a')).toBe(true); + expect(classes['a']['styles']).toEqual(['stroke-width: 8px']); + }); +}); diff --git a/packages/mermaid/src/diagrams/flowchart/flowDiagram-v2.ts b/packages/mermaid/src/diagrams/flowchart/flowDiagram-v2.ts index 7a2c0e0bc5..c3de4b6854 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDiagram-v2.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowDiagram-v2.ts @@ -1,9 +1,9 @@ -// @ts-ignore: TODO Fix ts errors +// @ts-ignore: JISON doesn't support types import flowParser from './parser/flow.jison'; import flowDb from './flowDb.js'; import flowRendererV2 from './flowRenderer-v2.js'; import flowStyles from './styles.js'; -import { MermaidConfig } from '../../config.type.js'; +import type { MermaidConfig } from '../../config.type.js'; import { setConfig } from '../../config.js'; export const diagram = { diff --git a/packages/mermaid/src/diagrams/flowchart/flowDiagram.ts b/packages/mermaid/src/diagrams/flowchart/flowDiagram.ts index 7018e4890d..ca4f8fba8a 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDiagram.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowDiagram.ts @@ -1,10 +1,10 @@ -// @ts-ignore: TODO Fix ts errors +// @ts-ignore: JISON doesn't support types import flowParser from './parser/flow.jison'; import flowDb from './flowDb.js'; import flowRenderer from './flowRenderer.js'; import flowRendererV2 from './flowRenderer-v2.js'; import flowStyles from './styles.js'; -import { MermaidConfig } from '../../config.type.js'; +import type { MermaidConfig } from '../../config.type.js'; export const diagram = { parser: flowParser, diff --git a/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js b/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js index 23f94942c2..4a3b7a8ce5 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js +++ b/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js @@ -1,10 +1,7 @@ import * as graphlib from 'dagre-d3-es/src/graphlib/index.js'; import { select, curveLinear, selectAll } from 'd3'; - -import flowDb from './flowDb.js'; import { getConfig } from '../../config.js'; import utils from '../../utils.js'; - import { render } from '../../dagre-wrapper/index.js'; import { addHtmlLabel } from 'dagre-d3-es/src/dagre-js/label/add-html-label.js'; import { log } from '../../logger.js'; @@ -344,15 +341,7 @@ export const addEdges = function (edges, g, diagObj) { * @returns {object} ClassDef styles */ export const getClasses = function (text, diagObj) { - log.info('Extracting classes'); - diagObj.db.clear(); - try { - // Parse the graph definition - diagObj.parse(text); - return diagObj.db.getClasses(); - } catch (e) { - return; - } + return diagObj.db.getClasses(); }; /** @@ -364,10 +353,6 @@ export const getClasses = function (text, diagObj) { export const draw = async function (text, id, _version, diagObj) { log.info('Drawing flowchart'); - diagObj.db.clear(); - flowDb.setGen('gen-2'); - // Parse the graph definition - diagObj.parser.parse(text); // Fetch the default direction, use TD if none was found let dir = diagObj.db.getDirection(); diff --git a/packages/mermaid/src/diagrams/flowchart/flowRenderer.js b/packages/mermaid/src/diagrams/flowchart/flowRenderer.js index f69fe93ebc..fc06cacd4d 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowRenderer.js +++ b/packages/mermaid/src/diagrams/flowchart/flowRenderer.js @@ -273,15 +273,7 @@ export const addEdges = function (edges, g, diagObj) { */ export const getClasses = function (text, diagObj) { log.info('Extracting classes'); - diagObj.db.clear(); - try { - // Parse the graph definition - diagObj.parse(text); - return diagObj.db.getClasses(); - } catch (e) { - log.error(e); - return {}; - } + return diagObj.db.getClasses(); }; /** @@ -294,7 +286,6 @@ export const getClasses = function (text, diagObj) { */ export const draw = function (text, id, _version, diagObj) { log.info('Drawing flowchart'); - diagObj.db.clear(); const { securityLevel, flowchart: conf } = getConfig(); let sandboxElement; if (securityLevel === 'sandbox') { @@ -306,13 +297,6 @@ export const draw = function (text, id, _version, diagObj) { : select('body'); const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document; - // Parse the graph definition - try { - diagObj.parser.parse(text); - } catch (err) { - log.debug('Parsing failed'); - } - // Fetch the default direction, use TD if none was found let dir = diagObj.db.getDirection(); if (dir === undefined) { diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-edges.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-edges.spec.js index dcac21ee75..21f3a43555 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-edges.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-edges.spec.js @@ -6,6 +6,40 @@ setConfig({ securityLevel: 'strict', }); +const keywords = [ + 'graph', + 'flowchart', + 'flowchart-elk', + 'style', + 'default', + 'linkStyle', + 'interpolate', + 'classDef', + 'class', + 'href', + 'call', + 'click', + '_self', + '_blank', + '_parent', + '_top', + 'end', + 'subgraph', + 'kitty', +]; + +const doubleEndedEdges = [ + { edgeStart: 'x--', edgeEnd: '--x', stroke: 'normal', type: 'double_arrow_cross' }, + { edgeStart: 'x==', edgeEnd: '==x', stroke: 'thick', type: 'double_arrow_cross' }, + { edgeStart: 'x-.', edgeEnd: '.-x', stroke: 'dotted', type: 'double_arrow_cross' }, + { edgeStart: 'o--', edgeEnd: '--o', stroke: 'normal', type: 'double_arrow_circle' }, + { edgeStart: 'o==', edgeEnd: '==o', stroke: 'thick', type: 'double_arrow_circle' }, + { edgeStart: 'o-.', edgeEnd: '.-o', stroke: 'dotted', type: 'double_arrow_circle' }, + { edgeStart: '<--', edgeEnd: '-->', stroke: 'normal', type: 'double_arrow_point' }, + { edgeStart: '<==', edgeEnd: '==>', stroke: 'thick', type: 'double_arrow_point' }, + { edgeStart: '<-.', edgeEnd: '.->', stroke: 'dotted', type: 'double_arrow_point' }, +]; + describe('[Edges] when parsing', () => { beforeEach(function () { flow.parser.yy = flowDb; @@ -39,211 +73,62 @@ describe('[Edges] when parsing', () => { expect(edges[0].type).toBe('arrow_circle'); }); - describe('cross', function () { - it('should handle double edged nodes and edges', function () { - const res = flow.parser.parse('graph TD;\nA x--x B;'); - - const vert = flow.parser.yy.getVertices(); - const edges = flow.parser.yy.getEdges(); - - expect(vert['A'].id).toBe('A'); - expect(vert['B'].id).toBe('B'); - expect(edges.length).toBe(1); - expect(edges[0].start).toBe('A'); - expect(edges[0].end).toBe('B'); - expect(edges[0].type).toBe('double_arrow_cross'); - expect(edges[0].text).toBe(''); - expect(edges[0].stroke).toBe('normal'); - expect(edges[0].length).toBe(1); - }); - - it('should handle double edged nodes with text', function () { - const res = flow.parser.parse('graph TD;\nA x-- text --x B;'); - - const vert = flow.parser.yy.getVertices(); - const edges = flow.parser.yy.getEdges(); - - expect(vert['A'].id).toBe('A'); - expect(vert['B'].id).toBe('B'); - expect(edges.length).toBe(1); - expect(edges[0].start).toBe('A'); - expect(edges[0].end).toBe('B'); - expect(edges[0].type).toBe('double_arrow_cross'); - expect(edges[0].text).toBe('text'); - expect(edges[0].stroke).toBe('normal'); - expect(edges[0].length).toBe(1); - }); - - it('should handle double edged nodes and edges on thick arrows', function () { - const res = flow.parser.parse('graph TD;\nA x==x B;'); - - const vert = flow.parser.yy.getVertices(); - const edges = flow.parser.yy.getEdges(); - - expect(vert['A'].id).toBe('A'); - expect(vert['B'].id).toBe('B'); - expect(edges.length).toBe(1); - expect(edges[0].start).toBe('A'); - expect(edges[0].end).toBe('B'); - expect(edges[0].type).toBe('double_arrow_cross'); - expect(edges[0].text).toBe(''); - expect(edges[0].stroke).toBe('thick'); - expect(edges[0].length).toBe(1); - }); - - it('should handle double edged nodes with text on thick arrows', function () { - const res = flow.parser.parse('graph TD;\nA x== text ==x B;'); - - const vert = flow.parser.yy.getVertices(); - const edges = flow.parser.yy.getEdges(); - - expect(vert['A'].id).toBe('A'); - expect(vert['B'].id).toBe('B'); - expect(edges.length).toBe(1); - expect(edges[0].start).toBe('A'); - expect(edges[0].end).toBe('B'); - expect(edges[0].type).toBe('double_arrow_cross'); - expect(edges[0].text).toBe('text'); - expect(edges[0].stroke).toBe('thick'); - expect(edges[0].length).toBe(1); - }); - - it('should handle double edged nodes and edges on dotted arrows', function () { - const res = flow.parser.parse('graph TD;\nA x-.-x B;'); - - const vert = flow.parser.yy.getVertices(); - const edges = flow.parser.yy.getEdges(); - - expect(vert['A'].id).toBe('A'); - expect(vert['B'].id).toBe('B'); - expect(edges.length).toBe(1); - expect(edges[0].start).toBe('A'); - expect(edges[0].end).toBe('B'); - expect(edges[0].type).toBe('double_arrow_cross'); - expect(edges[0].text).toBe(''); - expect(edges[0].stroke).toBe('dotted'); - expect(edges[0].length).toBe(1); - }); + describe('edges', function () { + doubleEndedEdges.forEach((edgeType) => { + it(`should handle ${edgeType.stroke} ${edgeType.type} with no text`, function () { + const res = flow.parser.parse(`graph TD;\nA ${edgeType.edgeStart}${edgeType.edgeEnd} B;`); - it('should handle double edged nodes with text on dotted arrows', function () { - const res = flow.parser.parse('graph TD;\nA x-. text .-x B;'); - - const vert = flow.parser.yy.getVertices(); - const edges = flow.parser.yy.getEdges(); - - expect(vert['A'].id).toBe('A'); - expect(vert['B'].id).toBe('B'); - expect(edges.length).toBe(1); - expect(edges[0].start).toBe('A'); - expect(edges[0].end).toBe('B'); - expect(edges[0].type).toBe('double_arrow_cross'); - expect(edges[0].text).toBe('text'); - expect(edges[0].stroke).toBe('dotted'); - expect(edges[0].length).toBe(1); - }); - }); - - describe('circle', function () { - it('should handle double edged nodes and edges', function () { - const res = flow.parser.parse('graph TD;\nA o--o B;'); - - const vert = flow.parser.yy.getVertices(); - const edges = flow.parser.yy.getEdges(); - - expect(vert['A'].id).toBe('A'); - expect(vert['B'].id).toBe('B'); - expect(edges.length).toBe(1); - expect(edges[0].start).toBe('A'); - expect(edges[0].end).toBe('B'); - expect(edges[0].type).toBe('double_arrow_circle'); - expect(edges[0].text).toBe(''); - expect(edges[0].stroke).toBe('normal'); - expect(edges[0].length).toBe(1); - }); + const vert = flow.parser.yy.getVertices(); + const edges = flow.parser.yy.getEdges(); - it('should handle double edged nodes with text', function () { - const res = flow.parser.parse('graph TD;\nA o-- text --o B;'); - - const vert = flow.parser.yy.getVertices(); - const edges = flow.parser.yy.getEdges(); - - expect(vert['A'].id).toBe('A'); - expect(vert['B'].id).toBe('B'); - expect(edges.length).toBe(1); - expect(edges[0].start).toBe('A'); - expect(edges[0].end).toBe('B'); - expect(edges[0].type).toBe('double_arrow_circle'); - expect(edges[0].text).toBe('text'); - expect(edges[0].stroke).toBe('normal'); - expect(edges[0].length).toBe(1); - }); + expect(vert['A'].id).toBe('A'); + expect(vert['B'].id).toBe('B'); + expect(edges.length).toBe(1); + expect(edges[0].start).toBe('A'); + expect(edges[0].end).toBe('B'); + expect(edges[0].type).toBe(`${edgeType.type}`); + expect(edges[0].text).toBe(''); + expect(edges[0].stroke).toBe(`${edgeType.stroke}`); + }); - it('should handle double edged nodes and edges on thick arrows', function () { - const res = flow.parser.parse('graph TD;\nA o==o B;'); - - const vert = flow.parser.yy.getVertices(); - const edges = flow.parser.yy.getEdges(); - - expect(vert['A'].id).toBe('A'); - expect(vert['B'].id).toBe('B'); - expect(edges.length).toBe(1); - expect(edges[0].start).toBe('A'); - expect(edges[0].end).toBe('B'); - expect(edges[0].type).toBe('double_arrow_circle'); - expect(edges[0].text).toBe(''); - expect(edges[0].stroke).toBe('thick'); - expect(edges[0].length).toBe(1); - }); + it(`should handle ${edgeType.stroke} ${edgeType.type} with text`, function () { + const res = flow.parser.parse( + `graph TD;\nA ${edgeType.edgeStart} text ${edgeType.edgeEnd} B;` + ); - it('should handle double edged nodes with text on thick arrows', function () { - const res = flow.parser.parse('graph TD;\nA o== text ==o B;'); - - const vert = flow.parser.yy.getVertices(); - const edges = flow.parser.yy.getEdges(); - - expect(vert['A'].id).toBe('A'); - expect(vert['B'].id).toBe('B'); - expect(edges.length).toBe(1); - expect(edges[0].start).toBe('A'); - expect(edges[0].end).toBe('B'); - expect(edges[0].type).toBe('double_arrow_circle'); - expect(edges[0].text).toBe('text'); - expect(edges[0].stroke).toBe('thick'); - expect(edges[0].length).toBe(1); - }); + const vert = flow.parser.yy.getVertices(); + const edges = flow.parser.yy.getEdges(); - it('should handle double edged nodes and edges on dotted arrows', function () { - const res = flow.parser.parse('graph TD;\nA o-.-o B;'); - - const vert = flow.parser.yy.getVertices(); - const edges = flow.parser.yy.getEdges(); - - expect(vert['A'].id).toBe('A'); - expect(vert['B'].id).toBe('B'); - expect(edges.length).toBe(1); - expect(edges[0].start).toBe('A'); - expect(edges[0].end).toBe('B'); - expect(edges[0].type).toBe('double_arrow_circle'); - expect(edges[0].text).toBe(''); - expect(edges[0].stroke).toBe('dotted'); - expect(edges[0].length).toBe(1); - }); + expect(vert['A'].id).toBe('A'); + expect(vert['B'].id).toBe('B'); + expect(edges.length).toBe(1); + expect(edges[0].start).toBe('A'); + expect(edges[0].end).toBe('B'); + expect(edges[0].type).toBe(`${edgeType.type}`); + expect(edges[0].text).toBe('text'); + expect(edges[0].stroke).toBe(`${edgeType.stroke}`); + }); - it('should handle double edged nodes with text on dotted arrows', function () { - const res = flow.parser.parse('graph TD;\nA o-. text .-o B;'); - - const vert = flow.parser.yy.getVertices(); - const edges = flow.parser.yy.getEdges(); - - expect(vert['A'].id).toBe('A'); - expect(vert['B'].id).toBe('B'); - expect(edges.length).toBe(1); - expect(edges[0].start).toBe('A'); - expect(edges[0].end).toBe('B'); - expect(edges[0].type).toBe('double_arrow_circle'); - expect(edges[0].text).toBe('text'); - expect(edges[0].stroke).toBe('dotted'); - expect(edges[0].length).toBe(1); + it.each(keywords)( + `should handle ${edgeType.stroke} ${edgeType.type} with %s text`, + function (keyword) { + const res = flow.parser.parse( + `graph TD;\nA ${edgeType.edgeStart} ${keyword} ${edgeType.edgeEnd} B;` + ); + + const vert = flow.parser.yy.getVertices(); + const edges = flow.parser.yy.getEdges(); + + expect(vert['A'].id).toBe('A'); + expect(vert['B'].id).toBe('B'); + expect(edges.length).toBe(1); + expect(edges[0].start).toBe('A'); + expect(edges[0].end).toBe('B'); + expect(edges[0].type).toBe(`${edgeType.type}`); + expect(edges[0].text).toBe(`${keyword}`); + expect(edges[0].stroke).toBe(`${edgeType.stroke}`); + } + ); }); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-md-string.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-md-string.spec.js index 0e6efaef19..13cb262e3a 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-md-string.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-md-string.spec.js @@ -24,7 +24,7 @@ A["\`The cat in **the** hat\`"]-- "\`The *bat* in the chat\`" -->B["The dog in t expect(vert['A'].labelType).toBe('markdown'); expect(vert['B'].id).toBe('B'); expect(vert['B'].text).toBe('The dog in the hog'); - expect(vert['B'].labelType).toBe('text'); + expect(vert['B'].labelType).toBe('string'); expect(edges.length).toBe(2); expect(edges[0].start).toBe('A'); expect(edges[0].end).toBe('B'); @@ -35,7 +35,7 @@ A["\`The cat in **the** hat\`"]-- "\`The *bat* in the chat\`" -->B["The dog in t expect(edges[1].end).toBe('C'); expect(edges[1].type).toBe('arrow_point'); expect(edges[1].text).toBe('The rat in the mat'); - expect(edges[1].labelType).toBe('text'); + expect(edges[1].labelType).toBe('string'); }); it('mardown formatting in subgraphs', function () { const res = flow.parser.parse(`flowchart LR diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-singlenode.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-singlenode.spec.js index b959f019ea..59336d8d48 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-singlenode.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-singlenode.spec.js @@ -6,6 +6,29 @@ setConfig({ securityLevel: 'strict', }); +const keywords = [ + 'graph', + 'flowchart', + 'flowchart-elk', + 'style', + 'default', + 'linkStyle', + 'interpolate', + 'classDef', + 'class', + 'href', + 'call', + 'click', + '_self', + '_blank', + '_parent', + '_top', + 'end', + 'subgraph', +]; + +const specialChars = ['#', ':', '0', '&', ',', '*', '.', '\\', 'v', '-', '/', '_']; + describe('[Singlenodes] when parsing', () => { beforeEach(function () { flow.parser.yy = flowDb; @@ -259,4 +282,90 @@ describe('[Singlenodes] when parsing', () => { expect(edges.length).toBe(0); expect(vert['i_d'].styles.length).toBe(0); }); + + it.each(keywords)('should handle keywords between dashes "-"', function (keyword) { + const res = flow.parser.parse(`graph TD;a-${keyword}-node;`); + const vert = flow.parser.yy.getVertices(); + expect(vert[`a-${keyword}-node`].text).toBe(`a-${keyword}-node`); + }); + + it.each(keywords)('should handle keywords between periods "."', function (keyword) { + const res = flow.parser.parse(`graph TD;a.${keyword}.node;`); + const vert = flow.parser.yy.getVertices(); + expect(vert[`a.${keyword}.node`].text).toBe(`a.${keyword}.node`); + }); + + it.each(keywords)('should handle keywords between underscores "_"', function (keyword) { + const res = flow.parser.parse(`graph TD;a_${keyword}_node;`); + const vert = flow.parser.yy.getVertices(); + expect(vert[`a_${keyword}_node`].text).toBe(`a_${keyword}_node`); + }); + + it.each(keywords)('should handle nodes ending in %s', function (keyword) { + const res = flow.parser.parse(`graph TD;node_${keyword};node.${keyword};node-${keyword};`); + const vert = flow.parser.yy.getVertices(); + expect(vert[`node_${keyword}`].text).toBe(`node_${keyword}`); + expect(vert[`node.${keyword}`].text).toBe(`node.${keyword}`); + expect(vert[`node-${keyword}`].text).toBe(`node-${keyword}`); + }); + + const errorKeywords = [ + 'graph', + 'flowchart', + 'flowchart-elk', + 'style', + 'linkStyle', + 'interpolate', + 'classDef', + 'class', + '_self', + '_blank', + '_parent', + '_top', + 'end', + 'subgraph', + ]; + it.each(errorKeywords)('should throw error at nodes beginning with %s', function (keyword) { + const str = `graph TD;${keyword}.node;${keyword}-node;${keyword}/node`; + const vert = flow.parser.yy.getVertices(); + + expect(() => flow.parser.parse(str)).toThrowError(); + }); + + const workingKeywords = ['default', 'href', 'click', 'call']; + + it.each(workingKeywords)('should parse node beginning with %s', function (keyword) { + flow.parser.parse(`graph TD; ${keyword}.node;${keyword}-node;${keyword}/node;`); + const vert = flow.parser.yy.getVertices(); + expect(vert[`${keyword}.node`].text).toBe(`${keyword}.node`); + expect(vert[`${keyword}-node`].text).toBe(`${keyword}-node`); + expect(vert[`${keyword}/node`].text).toBe(`${keyword}/node`); + }); + + it.each(specialChars)( + 'should allow node ids of single special characters', + function (specialChar) { + flow.parser.parse(`graph TD; ${specialChar} --> A`); + const vert = flow.parser.yy.getVertices(); + expect(vert[`${specialChar}`].text).toBe(`${specialChar}`); + } + ); + + it.each(specialChars)( + 'should allow node ids with special characters at start of id', + function (specialChar) { + flow.parser.parse(`graph TD; ${specialChar}node --> A`); + const vert = flow.parser.yy.getVertices(); + expect(vert[`${specialChar}node`].text).toBe(`${specialChar}node`); + } + ); + + it.each(specialChars)( + 'should allow node ids with special characters at end of id', + function (specialChar) { + flow.parser.parse(`graph TD; node${specialChar} --> A`); + const vert = flow.parser.yy.getVertices(); + expect(vert[`node${specialChar}`].text).toBe(`node${specialChar}`); + } + ); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-style.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-style.spec.js index 512a0b8338..1ab7543085 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-style.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-style.spec.js @@ -26,15 +26,6 @@ describe('[Style] when parsing', () => { expect(vert['Q'].styles[0]).toBe('background:#fff'); }); - // log.debug(flow.parser.parse('graph TD;style Q background:#fff;')); - it('should handle styles for edges', function () { - const res = flow.parser.parse('graph TD;a-->b;\nstyle #0 stroke: #f66;'); - - const edges = flow.parser.yy.getEdges(); - - expect(edges.length).toBe(1); - }); - it('should handle multiple styles for a vortex', function () { const res = flow.parser.parse('graph TD;style R background:#fff,border:1px solid red;'); @@ -113,6 +104,22 @@ describe('[Style] when parsing', () => { expect(classes['exClass'].styles[1]).toBe('border:1px solid red'); }); + it('should be possible to declare multiple classes', function () { + const res = flow.parser.parse( + 'graph TD;classDef firstClass,secondClass background:#bbb,border:1px solid red;' + ); + + const classes = flow.parser.yy.getClasses(); + + expect(classes['firstClass'].styles.length).toBe(2); + expect(classes['firstClass'].styles[0]).toBe('background:#bbb'); + expect(classes['firstClass'].styles[1]).toBe('border:1px solid red'); + + expect(classes['secondClass'].styles.length).toBe(2); + expect(classes['secondClass'].styles[0]).toBe('background:#bbb'); + expect(classes['secondClass'].styles[1]).toBe('border:1px solid red'); + }); + it('should be possible to declare a class with a dot in the style', function () { const res = flow.parser.parse( 'graph TD;classDef exClass background:#bbb,border:1.5px solid red;' @@ -322,4 +329,20 @@ describe('[Style] when parsing', () => { expect(edges[0].type).toBe('arrow_point'); }); + + it('should handle multiple vertices with style', function () { + const res = flow.parser.parse(` + graph TD + classDef C1 stroke-dasharray:4 + classDef C2 stroke-dasharray:6 + A & B:::C1 & D:::C1 --> E:::C2 + `); + + const vert = flow.parser.yy.getVertices(); + + expect(vert['A'].classes.length).toBe(0); + expect(vert['B'].classes[0]).toBe('C1'); + expect(vert['D'].classes[0]).toBe('C1'); + expect(vert['E'].classes[0]).toBe('C2'); + }); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-text.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-text.spec.js index db43e75bf1..b127e1b65d 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-text.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-text.spec.js @@ -305,6 +305,95 @@ describe('[Text] when parsing', () => { expect(vert['C'].type).toBe('round'); expect(vert['C'].text).toBe('Chimpansen hoppar'); }); + + const keywords = [ + 'graph', + 'flowchart', + 'flowchart-elk', + 'style', + 'default', + 'linkStyle', + 'interpolate', + 'classDef', + 'class', + 'href', + 'call', + 'click', + '_self', + '_blank', + '_parent', + '_top', + 'end', + 'subgraph', + 'kitty', + ]; + + const shapes = [ + { start: '[', end: ']', name: 'square' }, + { start: '(', end: ')', name: 'round' }, + { start: '{', end: '}', name: 'diamond' }, + { start: '(-', end: '-)', name: 'ellipse' }, + { start: '([', end: '])', name: 'stadium' }, + { start: '>', end: ']', name: 'odd' }, + { start: '[(', end: ')]', name: 'cylinder' }, + { start: '(((', end: ')))', name: 'doublecircle' }, + { start: '[/', end: '\\]', name: 'trapezoid' }, + { start: '[\\', end: '/]', name: 'inv_trapezoid' }, + { start: '[/', end: '/]', name: 'lean_right' }, + { start: '[\\', end: '\\]', name: 'lean_left' }, + { start: '[[', end: ']]', name: 'subroutine' }, + { start: '{{', end: '}}', name: 'hexagon' }, + ]; + + shapes.forEach((shape) => { + it.each(keywords)(`should handle %s keyword in ${shape.name} vertex`, function (keyword) { + const rest = flow.parser.parse( + `graph TD;A_${keyword}_node-->B${shape.start}This node has a ${keyword} as text${shape.end};` + ); + + const vert = flow.parser.yy.getVertices(); + const edges = flow.parser.yy.getEdges(); + expect(vert['B'].type).toBe(`${shape.name}`); + expect(vert['B'].text).toBe(`This node has a ${keyword} as text`); + }); + }); + + it.each(keywords)('should handle %s keyword in rect vertex', function (keyword) { + const rest = flow.parser.parse( + `graph TD;A_${keyword}_node-->B[|borders:lt|This node has a ${keyword} as text];` + ); + + const vert = flow.parser.yy.getVertices(); + const edges = flow.parser.yy.getEdges(); + expect(vert['B'].type).toBe('rect'); + expect(vert['B'].text).toBe(`This node has a ${keyword} as text`); + }); + + it('should handle edge case for odd vertex with node id ending with minus', function () { + const res = flow.parser.parse('graph TD;A_node-->odd->Vertex Text];'); + const vert = flow.parser.yy.getVertices(); + + expect(vert['odd-'].type).toBe('odd'); + expect(vert['odd-'].text).toBe('Vertex Text'); + }); + it('should allow forward slashes in lean_right vertices', function () { + const rest = flow.parser.parse(`graph TD;A_node-->B[/This node has a / as text/];`); + + const vert = flow.parser.yy.getVertices(); + const edges = flow.parser.yy.getEdges(); + expect(vert['B'].type).toBe('lean_right'); + expect(vert['B'].text).toBe(`This node has a / as text`); + }); + + it('should allow back slashes in lean_left vertices', function () { + const rest = flow.parser.parse(`graph TD;A_node-->B[\\This node has a \\ as text\\];`); + + const vert = flow.parser.yy.getVertices(); + const edges = flow.parser.yy.getEdges(); + expect(vert['B'].type).toBe('lean_left'); + expect(vert['B'].text).toBe(`This node has a \\ as text`); + }); + it('should handle åäö and minus', function () { const res = flow.parser.parse('graph TD;A-->C{Chimpansen hoppar åäö-ÅÄÖ};'); @@ -484,4 +573,33 @@ describe('[Text] when parsing', () => { expect(vert['A'].text).toBe(',.?!+-*'); expect(edges[0].text).toBe(',.?!+-*'); }); + + it('should throw error at nested set of brackets', function () { + const str = 'graph TD; A[This is a () in text];'; + expect(() => flow.parser.parse(str)).toThrowError("got 'PS'"); + }); + + it('should throw error for strings and text at the same time', function () { + const str = 'graph TD;A(this node has "string" and text)-->|this link has "string" and text|C;'; + + expect(() => flow.parser.parse(str)).toThrowError("got 'STR'"); + }); + + it('should throw error for escaping quotes in text state', function () { + //prettier-ignore + const str = 'graph TD; A[This is a \"()\" in text];'; //eslint-disable-line no-useless-escape + + expect(() => flow.parser.parse(str)).toThrowError("got 'STR'"); + }); + + it('should throw error for nested quoatation marks', function () { + const str = 'graph TD; A["This is a "()" in text"];'; + + expect(() => flow.parser.parse(str)).toThrowError("Expecting 'SQE'"); + }); + + it('should throw error', function () { + const str = `graph TD; node[hello ) world] --> works`; + expect(() => flow.parser.parse(str)).toThrowError("got 'PE'"); + }); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow.jison b/packages/mermaid/src/diagrams/flowchart/parser/flow.jison index 51427118f7..8d746f8082 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow.jison +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow.jison @@ -13,6 +13,12 @@ %x acc_descr_multiline %x dir %x vertex +%x text +%x ellipseText +%x trapText +%x edgeText +%x thickEdgeText +%x dottedEdgeText %x click %x href %x callbackname @@ -23,41 +29,19 @@ %x close_directive %% -\%\%\{ { this.begin('open_directive'); return 'open_directive'; } -((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; } -":" { this.popState(); this.begin('arg_directive'); return ':'; } -\}\%\% { this.popState(); this.popState(); return 'close_directive'; } -((?:(?!\}\%\%).|\n)*) return 'arg_directive'; -accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; } -(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; } -accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; } -(?!\n|;|#)*[^\n]* { this.popState(); return "acc_descr_value"; } -accDescr\s*"{"\s* { this.begin("acc_descr_multiline");} +\%\%\{ { this.begin('open_directive'); return 'open_directive'; } +((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; } +":" { this.popState(); this.begin('arg_directive'); return ':'; } +\}\%\% { this.popState(); this.popState(); return 'close_directive'; } +((?:(?!\}\%\%).|\n)*) return 'arg_directive'; +accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; } +(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; } +accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; } +(?!\n|;|#)*[^\n]* { this.popState(); return "acc_descr_value"; } +accDescr\s*"{"\s* { this.begin("acc_descr_multiline");} [\}] { this.popState(); } [^\}]* return "acc_descr_multiline_value"; -// .*[^\n]* { return "acc_descr_line"} -["][`] { this.begin("md_string");} -[^`"]+ { return "MD_STR";} -[`]["] { this.popState();} -["] this.begin("string"); -["] this.popState(); -[^"]* return "STR"; -"style" return 'STYLE'; -"default" return 'DEFAULT'; -"linkStyle" return 'LINKSTYLE'; -"interpolate" return 'INTERPOLATE'; -"classDef" return 'CLASSDEF'; -"class" return 'CLASS'; - -/* ----interactivity command--- -'href' adds a link to the specified node. 'href' can only be specified when the -line was introduced with 'click'. -'href ""' attaches the specified link to the node that was specified by 'click'. -*/ -"href"[\s]+["] this.begin("href"); -["] this.popState(); -[^"]* return 'HREF'; +// .*[^\n]* { return "acc_descr_line"} /* ---interactivity command--- @@ -74,88 +58,128 @@ Function arguments are optional: 'call ()' simply executes 'callba \) this.popState(); [^)]* return 'CALLBACKARGS'; +[^`"]+ { return "MD_STR";} +[`]["] { this.popState();} +<*>["][`] { this.begin("md_string");} +[^"]+ return "STR"; +["] this.popState(); +<*>["] this.pushState("string"); +"style" return 'STYLE'; +"default" return 'DEFAULT'; +"linkStyle" return 'LINKSTYLE'; +"interpolate" return 'INTERPOLATE'; +"classDef" return 'CLASSDEF'; +"class" return 'CLASS'; + +/* +---interactivity command--- +'href' adds a link to the specified node. 'href' can only be specified when the +line was introduced with 'click'. +'href ""' attaches the specified link to the node that was specified by 'click'. +*/ +"href"[\s] return 'HREF'; + + /* 'click' is the keyword to introduce a line that contains interactivity commands. 'click' must be followed by an existing node-id. All commands are attached to that id. 'click ' can be followed by href or call commands in any desired order */ -"click"[\s]+ this.begin("click"); -[\s\n] this.popState(); -[^\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'; -"end"\b\s* return 'end'; - -"_self" return 'LINK_TARGET'; -"_blank" return 'LINK_TARGET'; -"_parent" return 'LINK_TARGET'; -"_top" return 'LINK_TARGET'; - -

(\r?\n)*\s*\n { this.popState(); return 'NODIR'; } -\s*"LR" { this.popState(); return 'DIR'; } -\s*"RL" { this.popState(); return 'DIR'; } -\s*"TB" { this.popState(); return 'DIR'; } -\s*"BT" { this.popState(); return 'DIR'; } -\s*"TD" { this.popState(); return 'DIR'; } -\s*"BR" { this.popState(); return 'DIR'; } -\s*"<" { this.popState(); return 'DIR'; } -\s*">" { this.popState(); return 'DIR'; } -\s*"^" { this.popState(); return 'DIR'; } -\s*"v" { this.popState(); return 'DIR'; } - -.*direction\s+TB[^\n]* return 'direction_tb'; -.*direction\s+BT[^\n]* return 'direction_bt'; -.*direction\s+RL[^\n]* return 'direction_rl'; -.*direction\s+LR[^\n]* return 'direction_lr'; - -[0-9]+ { return 'NUM';} -\# return 'BRKT'; -":::" return 'STYLE_SEPARATOR'; -":" return 'COLON'; -"&" return 'AMP'; -";" return 'SEMI'; -"," return 'COMMA'; -"*" return 'MULT'; -\s*[xo<]?\-\-+[-xo>]\s* return 'LINK'; -\s*[xo<]?\=\=+[=xo>]\s* return 'LINK'; -\s*[xo<]?\-?\.+\-[xo>]?\s* return 'LINK'; -\s*\~\~[\~]+\s* return 'LINK'; -\s*[xo<]?\-\-\s* return 'START_LINK'; -\s*[xo<]?\=\=\s* return 'START_LINK'; -\s*[xo<]?\-\.\s* return 'START_LINK'; -"(-" return '(-'; -"-)" return '-)'; -"([" return 'STADIUMSTART'; -"])" return 'STADIUMEND'; -"[[" return 'SUBROUTINESTART'; -"]]" return 'SUBROUTINEEND'; -"[|" return 'VERTEX_WITH_PROPS_START'; -"[(" return 'CYLINDERSTART'; -")]" return 'CYLINDEREND'; -"(((" return 'DOUBLECIRCLESTART'; -")))" return 'DOUBLECIRCLEEND'; -\- return 'MINUS'; -"." return 'DOT'; -[\_] return 'UNDERSCORE'; -\+ return 'PLUS'; -\% return 'PCT'; -"=" return 'EQUALS'; -\= return 'EQUALS'; +"click"[\s]+ this.begin("click"); +[\s\n] this.popState(); +[^\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'; +"end"\b\s* return 'end'; + +"_self" return 'LINK_TARGET'; +"_blank" return 'LINK_TARGET'; +"_parent" return 'LINK_TARGET'; +"_top" return 'LINK_TARGET'; + +(\r?\n)*\s*\n { this.popState(); return 'NODIR'; } +\s*"LR" { this.popState(); return 'DIR'; } +\s*"RL" { this.popState(); return 'DIR'; } +\s*"TB" { this.popState(); return 'DIR'; } +\s*"BT" { this.popState(); return 'DIR'; } +\s*"TD" { this.popState(); return 'DIR'; } +\s*"BR" { this.popState(); return 'DIR'; } +\s*"<" { this.popState(); return 'DIR'; } +\s*">" { this.popState(); return 'DIR'; } +\s*"^" { this.popState(); return 'DIR'; } +\s*"v" { this.popState(); return 'DIR'; } + +.*direction\s+TB[^\n]* return 'direction_tb'; +.*direction\s+BT[^\n]* return 'direction_bt'; +.*direction\s+RL[^\n]* return 'direction_rl'; +.*direction\s+LR[^\n]* return 'direction_lr'; + +[0-9]+ return 'NUM'; +\# return 'BRKT'; +":::" return 'STYLE_SEPARATOR'; +":" return 'COLON'; +"&" return 'AMP'; +";" return 'SEMI'; +"," return 'COMMA'; +"*" return 'MULT'; + +\s*[xo<]?\-\-+[-xo>]\s* { this.popState(); return 'LINK'; } +\s*[xo<]?\-\-\s* { this.pushState("edgeText"); return 'START_LINK'; } +[^-]|\-(?!\-)+ return 'EDGE_TEXT'; + +\s*[xo<]?\=\=+[=xo>]\s* { this.popState(); return 'LINK'; } +\s*[xo<]?\=\=\s* { this.pushState("thickEdgeText"); return 'START_LINK'; } +[^=]|\=(?!=) return 'EDGE_TEXT'; + +\s*[xo<]?\-?\.+\-[xo>]?\s* { this.popState(); return 'LINK'; } +\s*[xo<]?\-\.\s* { this.pushState("dottedEdgeText"); return 'START_LINK'; } +[^\.]|\.(?!-) return 'EDGE_TEXT'; + + +<*>\s*\~\~[\~]+\s* return 'LINK'; + +[-/\)][\)] { this.popState(); return '-)'; } +[^\(\)\[\]\{\}]|-/!\)+ return "TEXT" +<*>"(-" { this.pushState("ellipseText"); return '(-'; } + +"])" { this.popState(); return 'STADIUMEND'; } +<*>"([" { this.pushState("text"); return 'STADIUMSTART'; } + +"]]" { this.popState(); return 'SUBROUTINEEND'; } +<*>"[[" { this.pushState("text"); return 'SUBROUTINESTART'; } + +"[|" { return 'VERTEX_WITH_PROPS_START'; } + +\> { this.pushState("text"); return 'TAGEND'; } + +")]" { this.popState(); return 'CYLINDEREND'; } +<*>"[(" { this.pushState("text") ;return 'CYLINDERSTART'; } + +")))" { this.popState(); return 'DOUBLECIRCLEEND'; } +<*>"(((" { this.pushState("text"); return 'DOUBLECIRCLESTART'; } + +[\\(?=\])][\]] { this.popState(); return 'TRAPEND'; } +\/(?=\])\] { this.popState(); return 'INVTRAPEND'; } +\/(?!\])|\\(?!\])|[^\\\[\]\(\)\{\}\/]+ return 'TEXT'; +<*>"[/" { this.pushState("trapText"); return 'TRAPSTART'; } + +<*>"[\\" { this.pushState("trapText"); return 'INVTRAPSTART'; } + + "<" return 'TAGSTART'; ">" return 'TAGEND'; "^" return 'UP'; "\|" return 'SEP'; "v" return 'DOWN'; -[A-Za-z]+ return 'ALPHA'; -"\\]" return 'TRAPEND'; -"[/" return 'TRAPSTART'; -"/]" return 'INVTRAPEND'; -"[\\" return 'INVTRAPSTART'; -[!"#$%&'*+,-.`?\\_/] return 'PUNCTUATION'; +"*" return 'MULT'; +"#" return 'BRKT'; +"&" return 'AMP'; +([A-Za-z0-9!"\#$%&'*+\.`?\\_\/]|\-(?=[^\>\-\.])|=(?!=))+ return 'NODE_STRING'; +"-" return 'MINUS' [\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]| [\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377]| [\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5]| @@ -218,13 +242,20 @@ that id. [\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF]| [\uFFD2-\uFFD7\uFFDA-\uFFDC] return 'UNICODE_TEXT'; -"|" return 'PIPE'; -"(" return 'PS'; -")" return 'PE'; -"[" return 'SQS'; -"]" return 'SQE'; -"{" return 'DIAMOND_START' -"}" return 'DIAMOND_STOP' + +"|" { this.popState(); return 'PIPE'; } +<*>"|" { this.pushState("text"); return 'PIPE'; } + +")" { this.popState(); return 'PE'; } +<*>"(" { this.pushState("text"); return 'PS'; } + +"]" { this.popState(); return 'SQE'; } +<*>"[" { this.pushState("text"); return 'SQS'; } + +(\}) { this.popState(); return 'DIAMOND_STOP' } +<*>"{" { this.pushState("text"); return 'DIAMOND_START' } +[^\[\]\(\)\{\}\|\"]+ return "TEXT"; + "\"" return 'QUOTE'; (\r?\n)+ return 'NEWLINE'; \s return 'SPACE'; @@ -255,11 +286,11 @@ openDirective ; typeDirective - : type_directive { yy.parseDirective($1, 'type_directive'); } + : type_directive { yy.parseDirective($type_directive, 'type_directive'); } ; argDirective - : arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); } + : arg_directive { $arg_directive = $arg_directive.trim().replace(/'/g, '"'); yy.parseDirective($arg_directive, 'arg_directive'); } ; closeDirective @@ -275,15 +306,15 @@ document { $$ = [];} | document line { - if(!Array.isArray($2) || $2.length > 0){ - $1.push($2); + if(!Array.isArray($line) || $line.length > 0){ + $document.push($line); } - $$=$1;} + $$=$document;} ; line : statement - {$$=$1;} + {$$=$statement;} | SEMI | NEWLINE | SPACE @@ -296,15 +327,15 @@ graphConfig | GRAPH NODIR { yy.setDirection('TB');$$ = 'TB';} | GRAPH DIR FirstStmtSeperator - { yy.setDirection($2);$$ = $2;} + { yy.setDirection($DIR);$$ = $DIR;} // | GRAPH SPACE TAGEND FirstStmtSeperator - // { yy.setDirection("LR");$$ = $3;} + // { yy.setDirection("LR");$$ = $TAGEND;} // | GRAPH SPACE TAGSTART FirstStmtSeperator - // { yy.setDirection("RL");$$ = $3;} + // { yy.setDirection("RL");$$ = $TAGSTART;} // | GRAPH SPACE UP FirstStmtSeperator - // { yy.setDirection("BT");$$ = $3;} + // { yy.setDirection("BT");$$ = $UP;} // | GRAPH SPACE DOWN FirstStmtSeperator - // { yy.setDirection("TB");$$ = $3;} + // { yy.setDirection("TB");$$ = $DOWN;} ; ending: endToken ending @@ -332,7 +363,7 @@ spaceList statement : verticeStatement separator - { /* console.warn('finat vs', $1.nodes); */ $$=$1.nodes} + { /* console.warn('finat vs', $verticeStatement.nodes); */ $$=$verticeStatement.nodes} | styleStatement separator {$$=[];} | linkStyleStatement separator @@ -343,106 +374,121 @@ statement {$$=[];} | clickStatement separator {$$=[];} - | subgraph SPACE text SQS text SQE separator document end - {$$=yy.addSubGraph($3,$8,$5);} - | subgraph SPACE text separator document end - {$$=yy.addSubGraph($3,$5,$3);} - // | subgraph SPACE text separator document end - // {$$=yy.addSubGraph($3,$5,$3);} + | subgraph SPACE textNoTags SQS text SQE separator document end + {$$=yy.addSubGraph($textNoTags,$document,$text);} + | subgraph SPACE textNoTags separator document end + {$$=yy.addSubGraph($textNoTags,$document,$textNoTags);} + // | subgraph SPACE textNoTags separator document end + // {$$=yy.addSubGraph($textNoTags,$document,$textNoTags);} | subgraph separator document end - {$$=yy.addSubGraph(undefined,$3,undefined);} + {$$=yy.addSubGraph(undefined,$document,undefined);} | direction - | acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); } - | acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); } - | acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); } + | acc_title acc_title_value { $$=$acc_title_value.trim();yy.setAccTitle($$); } + | acc_descr acc_descr_value { $$=$acc_descr_value.trim();yy.setAccDescription($$); } + | acc_descr_multiline_value { $$=$acc_descr_multiline_value.trim();yy.setAccDescription($$); } ; separator: NEWLINE | SEMI | EOF ; verticeStatement: verticeStatement link node - { /* console.warn('vs',$1.stmt,$3); */ yy.addLink($1.stmt,$3,$2); $$ = { stmt: $3, nodes: $3.concat($1.nodes) } } + { /* console.warn('vs',$verticeStatement.stmt,$node); */ yy.addLink($verticeStatement.stmt,$node,$link); $$ = { stmt: $node, nodes: $node.concat($verticeStatement.nodes) } } | verticeStatement link node spaceList - { /* console.warn('vs',$1.stmt,$3); */ yy.addLink($1.stmt,$3,$2); $$ = { stmt: $3, nodes: $3.concat($1.nodes) } } - |node spaceList {/*console.warn('noda', $1);*/ $$ = {stmt: $1, nodes:$1 }} - |node { /*console.warn('noda', $1);*/ $$ = {stmt: $1, nodes:$1 }} + { /* console.warn('vs',$verticeStatement.stmt,$node); */ yy.addLink($verticeStatement.stmt,$node,$link); $$ = { stmt: $node, nodes: $node.concat($verticeStatement.nodes) } } + |node spaceList {/*console.warn('noda', $node);*/ $$ = {stmt: $node, nodes:$node }} + |node { /*console.warn('noda', $node);*/ $$ = {stmt: $node, nodes:$node }} + ; + +node: styledVertex + { /* console.warn('nod', $styledVertex); */ $$ = [$styledVertex];} + | node spaceList AMP spaceList styledVertex + { $$ = $node.concat($styledVertex); /* console.warn('pip', $node[0], $styledVertex, $$); */ } ; -node: vertex - { /* console.warn('nod', $1); */ $$ = [$1];} - | node spaceList AMP spaceList vertex - { $$ = $1.concat($5); /* console.warn('pip', $1[0], $5, $$); */ } +styledVertex: vertex + { /* console.warn('nod', $vertex); */ $$ = $vertex;} | vertex STYLE_SEPARATOR idString - {$$ = [$1];yy.setClass($1,$3)} + {$$ = $vertex;yy.setClass($vertex,$idString)} ; vertex: idString SQS text SQE - {$$ = $1;yy.addVertex($1,$3,'square');} + {$$ = $idString;yy.addVertex($idString,$text,'square');} | idString DOUBLECIRCLESTART text DOUBLECIRCLEEND - {$$ = $1;yy.addVertex($1,$3,'doublecircle');} + {$$ = $idString;yy.addVertex($idString,$text,'doublecircle');} | idString PS PS text PE PE - {$$ = $1;yy.addVertex($1,$4,'circle');} + {$$ = $idString;yy.addVertex($idString,$text,'circle');} | idString '(-' text '-)' - {$$ = $1;yy.addVertex($1,$3,'ellipse');} + {$$ = $idString;yy.addVertex($idString,$text,'ellipse');} | idString STADIUMSTART text STADIUMEND - {$$ = $1;yy.addVertex($1,$3,'stadium');} + {$$ = $idString;yy.addVertex($idString,$text,'stadium');} | idString SUBROUTINESTART text SUBROUTINEEND - {$$ = $1;yy.addVertex($1,$3,'subroutine');} - | idString VERTEX_WITH_PROPS_START ALPHA COLON ALPHA PIPE text SQE - {$$ = $1;yy.addVertex($1,$7,'rect',undefined,undefined,undefined, Object.fromEntries([[$3, $5]]));} + {$$ = $idString;yy.addVertex($idString,$text,'subroutine');} + | idString VERTEX_WITH_PROPS_START NODE_STRING\[field] COLON NODE_STRING\[value] PIPE text SQE + {$$ = $idString;yy.addVertex($idString,$text,'rect',undefined,undefined,undefined, Object.fromEntries([[$field, $value]]));} | idString CYLINDERSTART text CYLINDEREND - {$$ = $1;yy.addVertex($1,$3,'cylinder');} + {$$ = $idString;yy.addVertex($idString,$text,'cylinder');} | idString PS text PE - {$$ = $1;yy.addVertex($1,$3,'round');} + {$$ = $idString;yy.addVertex($idString,$text,'round');} | idString DIAMOND_START text DIAMOND_STOP - {$$ = $1;yy.addVertex($1,$3,'diamond');} + {$$ = $idString;yy.addVertex($idString,$text,'diamond');} | idString DIAMOND_START DIAMOND_START text DIAMOND_STOP DIAMOND_STOP - {$$ = $1;yy.addVertex($1,$4,'hexagon');} + {$$ = $idString;yy.addVertex($idString,$text,'hexagon');} | idString TAGEND text SQE - {$$ = $1;yy.addVertex($1,$3,'odd');} + {$$ = $idString;yy.addVertex($idString,$text,'odd');} | idString TRAPSTART text TRAPEND - {$$ = $1;yy.addVertex($1,$3,'trapezoid');} + {$$ = $idString;yy.addVertex($idString,$text,'trapezoid');} | idString INVTRAPSTART text INVTRAPEND - {$$ = $1;yy.addVertex($1,$3,'inv_trapezoid');} + {$$ = $idString;yy.addVertex($idString,$text,'inv_trapezoid');} | idString TRAPSTART text INVTRAPEND - {$$ = $1;yy.addVertex($1,$3,'lean_right');} + {$$ = $idString;yy.addVertex($idString,$text,'lean_right');} | idString INVTRAPSTART text TRAPEND - {$$ = $1;yy.addVertex($1,$3,'lean_left');} + {$$ = $idString;yy.addVertex($idString,$text,'lean_left');} | idString - { /*console.warn('h: ', $1);*/$$ = $1;yy.addVertex($1);} + { /*console.warn('h: ', $idString);*/$$ = $idString;yy.addVertex($idString);} ; link: linkStatement arrowText - {$1.text = $2;$$ = $1;} + {$linkStatement.text = $arrowText;$$ = $linkStatement;} | linkStatement TESTSTR SPACE - {$1.text = $2;$$ = $1;} + {$linkStatement.text = $TESTSTR;$$ = $linkStatement;} | linkStatement arrowText SPACE - {$1.text = $2;$$ = $1;} + {$linkStatement.text = $arrowText;$$ = $linkStatement;} | linkStatement - {$$ = $1;} - | START_LINK text LINK - {var inf = yy.destructLink($3, $1); $$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length,"text":$2};} + {$$ = $linkStatement;} + | START_LINK edgeText LINK + {var inf = yy.destructLink($LINK, $START_LINK); $$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length,"text":$edgeText};} ; +edgeText: edgeTextToken + {$$={text:$edgeTextToken, type:'text'};} + | edgeText edgeTextToken + {$$={text:$edgeText.text+''+$edgeTextToken, type:$edgeText.type};} + |STR + {$$={text: $STR, type: 'string'};} + | MD_STR + {$$={text:$MD_STR, type:'markdown'};} + ; + + linkStatement: LINK - {var inf = yy.destructLink($1);$$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length};} + {var inf = yy.destructLink($LINK);$$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length};} ; arrowText: PIPE text PIPE - {$$ = $2;} + {$$ = $text;} ; text: textToken - { $$={text:$1, type: 'text'};} + { $$={text:$textToken, type: 'text'};} | text textToken - { $$={text:$1.text+''+$2, type: $1.type};} + { $$={text:$text.text+''+$textToken, type: $text.type};} | STR - { $$={text: $1, type: 'text'};} + { $$ = {text: $STR, type: 'string'};} | MD_STR - { $$={text: $1, type: 'markdown'};} + { $$={text: $MD_STR, type: 'markdown'};} ; @@ -452,109 +498,104 @@ keywords textNoTags: textNoTagsToken - {$$=$1;} + {$$={text:$textNoTagsToken, type: 'text'};} | textNoTags textNoTagsToken - {$$=$1+''+$2;} + {$$={text:$textNoTags.text+''+$textNoTagsToken, type: $textNoTags.type};} + | STR + { $$={text: $STR, type: 'text'};} + | MD_STR + { $$={text: $MD_STR, type: 'markdown'};} ; -classDefStatement:CLASSDEF SPACE DEFAULT SPACE stylesOpt - {$$ = $1;yy.addClass($3,$5);} - | CLASSDEF SPACE alphaNum SPACE stylesOpt - {$$ = $1;yy.addClass($3,$5);} +classDefStatement:CLASSDEF SPACE idString SPACE stylesOpt + {$$ = $CLASSDEF;yy.addClass($idString,$stylesOpt);} ; -classStatement:CLASS SPACE alphaNum SPACE alphaNum - {$$ = $1;yy.setClass($3, $5);} +classStatement:CLASS SPACE idString\[vertex] SPACE idString\[class] + {$$ = $CLASS;yy.setClass($vertex, $class);} ; clickStatement - : CLICK CALLBACKNAME {$$ = $1;yy.setClickEvent($1, $2);} - | CLICK CALLBACKNAME SPACE STR {$$ = $1;yy.setClickEvent($1, $2);yy.setTooltip($1, $4);} - | CLICK CALLBACKNAME CALLBACKARGS {$$ = $1;yy.setClickEvent($1, $2, $3);} - | CLICK CALLBACKNAME CALLBACKARGS SPACE STR {$$ = $1;yy.setClickEvent($1, $2, $3);yy.setTooltip($1, $5);} - | CLICK HREF {$$ = $1;yy.setLink($1, $2);} - | CLICK HREF SPACE STR {$$ = $1;yy.setLink($1, $2);yy.setTooltip($1, $4);} - | CLICK HREF SPACE LINK_TARGET {$$ = $1;yy.setLink($1, $2, $4);} - | CLICK HREF SPACE STR SPACE LINK_TARGET {$$ = $1;yy.setLink($1, $2, $6);yy.setTooltip($1, $4);} - | CLICK alphaNum {$$ = $1;yy.setClickEvent($1, $2);} - | CLICK alphaNum SPACE STR {$$ = $1;yy.setClickEvent($1, $2);yy.setTooltip($1, $4);} - | CLICK STR {$$ = $1;yy.setLink($1, $2);} - | CLICK STR SPACE STR {$$ = $1;yy.setLink($1, $2);yy.setTooltip($1, $4);} - | CLICK STR SPACE LINK_TARGET {$$ = $1;yy.setLink($1, $2, $4);} - | CLICK STR SPACE STR SPACE LINK_TARGET {$$ = $1;yy.setLink($1, $2, $6);yy.setTooltip($1, $4);} + : CLICK CALLBACKNAME {$$ = $CLICK;yy.setClickEvent($CLICK, $CALLBACKNAME);} + | CLICK CALLBACKNAME SPACE STR {$$ = $CLICK;yy.setClickEvent($CLICK, $CALLBACKNAME);yy.setTooltip($CLICK, $STR);} + | CLICK CALLBACKNAME CALLBACKARGS {$$ = $CLICK;yy.setClickEvent($CLICK, $CALLBACKNAME, $CALLBACKARGS);} + | CLICK CALLBACKNAME CALLBACKARGS SPACE STR {$$ = $CLICK;yy.setClickEvent($CLICK, $CALLBACKNAME, $CALLBACKARGS);yy.setTooltip($CLICK, $STR);} + | CLICK HREF STR {$$ = $CLICK;yy.setLink($CLICK, $STR);} + | CLICK HREF STR SPACE STR {$$ = $CLICK;yy.setLink($CLICK, $STR1);yy.setTooltip($CLICK, $STR2);} + | CLICK HREF STR SPACE LINK_TARGET {$$ = $CLICK;yy.setLink($CLICK, $STR, $LINK_TARGET);} + | CLICK HREF STR\[link] SPACE STR\[tooltip] SPACE LINK_TARGET {$$ = $CLICK;yy.setLink($CLICK, $link, $LINK_TARGET);yy.setTooltip($CLICK, $tooltip);} + | CLICK alphaNum {$$ = $CLICK;yy.setClickEvent($CLICK, $alphaNum);} + | CLICK alphaNum SPACE STR {$$ = $CLICK;yy.setClickEvent($CLICK, $alphaNum);yy.setTooltip($CLICK, $STR);} + | CLICK STR {$$ = $CLICK;yy.setLink($CLICK, $STR);} + | CLICK STR\[link] SPACE STR\[tooltip] {$$ = $CLICK;yy.setLink($CLICK, $link);yy.setTooltip($CLICK, $tooltip);} + | CLICK STR SPACE LINK_TARGET {$$ = $CLICK;yy.setLink($CLICK, $STR, $LINK_TARGET);} + | CLICK STR\[link] SPACE STR\[tooltip] SPACE LINK_TARGET {$$ = $CLICK;yy.setLink($CLICK, $link, $LINK_TARGET);yy.setTooltip($CLICK, $tooltip);} ; -styleStatement:STYLE SPACE alphaNum SPACE stylesOpt - {$$ = $1;yy.addVertex($3,undefined,undefined,$5);} - | STYLE SPACE HEX SPACE stylesOpt - {$$ = $1;yy.updateLink($3,$5);} +styleStatement:STYLE SPACE idString SPACE stylesOpt + {$$ = $STYLE;yy.addVertex($idString,undefined,undefined,$stylesOpt);} ; linkStyleStatement : LINKSTYLE SPACE DEFAULT SPACE stylesOpt - {$$ = $1;yy.updateLink([$3],$5);} + {$$ = $LINKSTYLE;yy.updateLink([$DEFAULT],$stylesOpt);} | LINKSTYLE SPACE numList SPACE stylesOpt - {$$ = $1;yy.updateLink($3,$5);} + {$$ = $LINKSTYLE;yy.updateLink($numList,$stylesOpt);} | LINKSTYLE SPACE DEFAULT SPACE INTERPOLATE SPACE alphaNum SPACE stylesOpt - {$$ = $1;yy.updateLinkInterpolate([$3],$7);yy.updateLink([$3],$9);} + {$$ = $LINKSTYLE;yy.updateLinkInterpolate([$DEFAULT],$alphaNum);yy.updateLink([$DEFAULT],$stylesOpt);} | LINKSTYLE SPACE numList SPACE INTERPOLATE SPACE alphaNum SPACE stylesOpt - {$$ = $1;yy.updateLinkInterpolate($3,$7);yy.updateLink($3,$9);} + {$$ = $LINKSTYLE;yy.updateLinkInterpolate($numList,$alphaNum);yy.updateLink($numList,$stylesOpt);} | LINKSTYLE SPACE DEFAULT SPACE INTERPOLATE SPACE alphaNum - {$$ = $1;yy.updateLinkInterpolate([$3],$7);} + {$$ = $LINKSTYLE;yy.updateLinkInterpolate([$DEFAULT],$alphaNum);} | LINKSTYLE SPACE numList SPACE INTERPOLATE SPACE alphaNum - {$$ = $1;yy.updateLinkInterpolate($3,$7);} + {$$ = $LINKSTYLE;yy.updateLinkInterpolate($numList,$alphaNum);} ; numList: NUM - {$$ = [$1]} + {$$ = [$NUM]} | numList COMMA NUM - {$1.push($3);$$ = $1;} + {$numList.push($NUM);$$ = $numList;} ; stylesOpt: style - {$$ = [$1]} + {$$ = [$style]} | stylesOpt COMMA style - {$1.push($3);$$ = $1;} + {$stylesOpt.push($style);$$ = $stylesOpt;} ; style: styleComponent |style styleComponent - {$$ = $1 + $2;} + {$$ = $style + $styleComponent;} ; -styleComponent: ALPHA | COLON | MINUS | NUM | UNIT | SPACE | HEX | BRKT | DOT | STYLE | PCT ; +styleComponent: NUM | NODE_STRING| COLON | UNIT | SPACE | BRKT | STYLE | PCT ; /* Token lists */ +idStringToken : NUM | NODE_STRING | DOWN | MINUS | DEFAULT | COMMA | COLON | AMP | BRKT | MULT | UNICODE_TEXT; + +textToken : TEXT | TAGSTART | TAGEND | UNICODE_TEXT; + +textNoTagsToken: NUM | NODE_STRING | SPACE | MINUS | AMP | UNICODE_TEXT | COLON | MULT | BRKT | keywords | START_LINK ; -textToken : textNoTagsToken | TAGSTART | TAGEND | START_LINK | PCT | DEFAULT; +edgeTextToken : EDGE_TEXT | UNICODE_TEXT ; -textNoTagsToken: alphaNumToken | SPACE | MINUS | keywords ; +alphaNumToken : NUM | UNICODE_TEXT | NODE_STRING | DIR | DOWN | MINUS | COMMA | COLON | AMP | BRKT | MULT; idString :idStringToken - {$$=$1} + {$$=$idStringToken} | idString idStringToken - {$$=$1+''+$2} + {$$=$idString+''+$idStringToken} ; alphaNum - : alphaNumStatement - {$$=$1;} - | alphaNum alphaNumStatement - {$$=$1+''+$2;} + : alphaNumToken + {$$=$alphaNumToken;} + | alphaNum alphaNumToken + {$$=$alphaNum+''+$alphaNumToken;} ; -alphaNumStatement - : DIR - {$$=$1;} - | alphaNumToken - {$$=$1;} - | DOWN - {$$='v';} - | MINUS - {$$='-';} - ; direction : direction_tb @@ -567,9 +608,4 @@ direction { $$={stmt:'dir', value:'LR'};} ; -alphaNumToken : PUNCTUATION | AMP | UNICODE_TEXT | NUM| ALPHA | COLON | COMMA | PLUS | EQUALS | MULT | DOT | BRKT| UNDERSCORE ; - -idStringToken : ALPHA|UNDERSCORE |UNICODE_TEXT | NUM| COLON | COMMA | PLUS | MINUS | DOWN |EQUALS | MULT | BRKT | DOT | PUNCTUATION | AMP | DEFAULT; - -graphCodeTokens: STADIUMSTART | STADIUMEND | SUBROUTINESTART | SUBROUTINEEND | VERTEX_WITH_PROPS_START | CYLINDERSTART | CYLINDEREND | TRAPSTART | TRAPEND | INVTRAPSTART | INVTRAPEND | PIPE | PS | PE | SQS | SQE | DIAMOND_START | DIAMOND_STOP | TAGSTART | TAGEND | ARROW_CROSS | ARROW_POINT | ARROW_CIRCLE | ARROW_OPEN | QUOTE | SEMI; %% diff --git a/packages/mermaid/src/diagrams/gantt/ganttDb.js b/packages/mermaid/src/diagrams/gantt/ganttDb.js index 3964027025..3ff7955f85 100644 --- a/packages/mermaid/src/diagrams/gantt/ganttDb.js +++ b/packages/mermaid/src/diagrams/gantt/ganttDb.js @@ -16,7 +16,7 @@ import { clear as commonClear, setDiagramTitle, getDiagramTitle, -} from '../../commonDb.js'; +} from '../common/commonDb.js'; dayjs.extend(dayjsIsoWeek); dayjs.extend(dayjsCustomParseFormat); @@ -37,6 +37,7 @@ const tags = ['active', 'done', 'crit', 'milestone']; let funs = []; let inclusiveEndDates = false; let topAxis = false; +let weekday = 'sunday'; // The serial order of the task in the script let lastOrder = 0; @@ -66,6 +67,7 @@ export const clear = function () { lastOrder = 0; links = {}; commonClear(); + weekday = 'sunday'; }; export const setAxisFormat = function (txt) { @@ -179,6 +181,14 @@ export const isInvalidDate = function (date, dateFormat, excludes, includes) { return excludes.includes(date.format(dateFormat.trim())); }; +export const setWeekday = function (txt) { + weekday = txt; +}; + +export const getWeekday = function () { + return weekday; +}; + /** * TODO: fully document what this function does and what types it accepts * @@ -759,6 +769,8 @@ export default { bindFunctions, parseDuration, isInvalidDate, + setWeekday, + getWeekday, }; /** diff --git a/packages/mermaid/src/diagrams/gantt/ganttDiagram.ts b/packages/mermaid/src/diagrams/gantt/ganttDiagram.ts index 0104c7d0c8..a9ebfdb93f 100644 --- a/packages/mermaid/src/diagrams/gantt/ganttDiagram.ts +++ b/packages/mermaid/src/diagrams/gantt/ganttDiagram.ts @@ -1,9 +1,9 @@ -// @ts-ignore: TODO Fix ts errors +// @ts-ignore: JISON doesn't support types import ganttParser from './parser/gantt.jison'; import ganttDb from './ganttDb.js'; import ganttRenderer from './ganttRenderer.js'; import ganttStyles from './styles.js'; -import { DiagramDefinition } from '../../diagram-api/types.js'; +import type { DiagramDefinition } from '../../diagram-api/types.js'; export const diagram: DiagramDefinition = { parser: ganttParser, diff --git a/packages/mermaid/src/diagrams/gantt/ganttRenderer.js b/packages/mermaid/src/diagrams/gantt/ganttRenderer.js index ff16fef7c7..522f59e2c9 100644 --- a/packages/mermaid/src/diagrams/gantt/ganttRenderer.js +++ b/packages/mermaid/src/diagrams/gantt/ganttRenderer.js @@ -13,7 +13,13 @@ import { timeMinute, timeHour, timeDay, - timeWeek, + timeMonday, + timeTuesday, + timeWednesday, + timeThursday, + timeFriday, + timeSaturday, + timeSunday, timeMonth, } from 'd3'; import common from '../common/common.js'; @@ -24,6 +30,20 @@ export const setConf = function () { log.debug('Something is calling, setConf, remove the call'); }; +/** + * This will map any day of the week that can be set in the `weekday` option to + * the corresponding d3-time function that is used to calculate the ticks. + */ +const mapWeekdayToTimeFunction = { + monday: timeMonday, + tuesday: timeTuesday, + wednesday: timeWednesday, + thursday: timeThursday, + friday: timeFriday, + saturday: timeSaturday, + sunday: timeSunday, +}; + /** * For this issue: * https://github.com/mermaid-js/mermaid/issues/1618 @@ -59,8 +79,6 @@ let w; export const draw = function (text, id, version, diagObj) { const conf = getConfig().gantt; - // diagObj.db.clear(); - // parser.parse(text); const securityLevel = getConfig().securityLevel; // Handle root and Document for when rendering in sandbox mode let sandboxElement; @@ -563,6 +581,8 @@ export const draw = function (text, id, version, diagObj) { if (resultTickInterval !== null) { const every = resultTickInterval[1]; const interval = resultTickInterval[2]; + const weekday = diagObj.db.getWeekday() || conf.weekday; + switch (interval) { case 'minute': bottomXAxis.ticks(timeMinute.every(every)); @@ -574,7 +594,7 @@ export const draw = function (text, id, version, diagObj) { bottomXAxis.ticks(timeDay.every(every)); break; case 'week': - bottomXAxis.ticks(timeWeek.every(every)); + bottomXAxis.ticks(mapWeekdayToTimeFunction[weekday].every(every)); break; case 'month': bottomXAxis.ticks(timeMonth.every(every)); @@ -602,6 +622,8 @@ export const draw = function (text, id, version, diagObj) { if (resultTickInterval !== null) { const every = resultTickInterval[1]; const interval = resultTickInterval[2]; + const weekday = diagObj.db.getWeekday() || conf.weekday; + switch (interval) { case 'minute': topXAxis.ticks(timeMinute.every(every)); @@ -613,7 +635,7 @@ export const draw = function (text, id, version, diagObj) { topXAxis.ticks(timeDay.every(every)); break; case 'week': - topXAxis.ticks(timeWeek.every(every)); + topXAxis.ticks(mapWeekdayToTimeFunction[weekday].every(every)); break; case 'month': topXAxis.ticks(timeMonth.every(every)); diff --git a/packages/mermaid/src/diagrams/gantt/parser/gantt.jison b/packages/mermaid/src/diagrams/gantt/parser/gantt.jison index 0eb45ec416..f7fd40c1bc 100644 --- a/packages/mermaid/src/diagrams/gantt/parser/gantt.jison +++ b/packages/mermaid/src/diagrams/gantt/parser/gantt.jison @@ -77,24 +77,31 @@ that id. [\s\n] this.popState(); [^\s\n]* return 'click'; -"gantt" return 'gantt'; -"dateFormat"\s[^#\n;]+ return 'dateFormat'; -"inclusiveEndDates" return 'inclusiveEndDates'; -"topAxis" return 'topAxis'; -"axisFormat"\s[^#\n;]+ return 'axisFormat'; -"tickInterval"\s[^#\n;]+ return 'tickInterval'; -"includes"\s[^#\n;]+ return 'includes'; -"excludes"\s[^#\n;]+ return 'excludes'; -"todayMarker"\s[^\n;]+ return 'todayMarker'; -\d\d\d\d"-"\d\d"-"\d\d return 'date'; -"title"\s[^#\n;]+ return 'title'; -"accDescription"\s[^#\n;]+ return 'accDescription' -"section"\s[^#:\n;]+ return 'section'; -[^#:\n;]+ return 'taskTxt'; -":"[^#\n;]+ return 'taskData'; -":" return ':'; -<> return 'EOF'; -. return 'INVALID'; +"gantt" return 'gantt'; +"dateFormat"\s[^#\n;]+ return 'dateFormat'; +"inclusiveEndDates" return 'inclusiveEndDates'; +"topAxis" return 'topAxis'; +"axisFormat"\s[^#\n;]+ return 'axisFormat'; +"tickInterval"\s[^#\n;]+ return 'tickInterval'; +"includes"\s[^#\n;]+ return 'includes'; +"excludes"\s[^#\n;]+ return 'excludes'; +"todayMarker"\s[^\n;]+ return 'todayMarker'; +weekday\s+monday return 'weekday_monday' +weekday\s+tuesday return 'weekday_tuesday' +weekday\s+wednesday return 'weekday_wednesday' +weekday\s+thursday return 'weekday_thursday' +weekday\s+friday return 'weekday_friday' +weekday\s+saturday return 'weekday_saturday' +weekday\s+sunday return 'weekday_sunday' +\d\d\d\d"-"\d\d"-"\d\d return 'date'; +"title"\s[^#\n;]+ return 'title'; +"accDescription"\s[^#\n;]+ return 'accDescription' +"section"\s[^#:\n;]+ return 'section'; +[^#:\n;]+ return 'taskTxt'; +":"[^#\n;]+ return 'taskData'; +":" return ':'; +<> return 'EOF'; +. return 'INVALID'; /lex @@ -121,6 +128,16 @@ line | EOF { $$=[];} ; +weekday + : weekday_monday { yy.setWeekday("monday");} + | weekday_tuesday { yy.setWeekday("tuesday");} + | weekday_wednesday { yy.setWeekday("wednesday");} + | weekday_thursday { yy.setWeekday("thursday");} + | weekday_friday { yy.setWeekday("friday");} + | weekday_saturday { yy.setWeekday("saturday");} + | weekday_sunday { yy.setWeekday("sunday");} + ; + statement : dateFormat {yy.setDateFormat($1.substr(11));$$=$1.substr(11);} | inclusiveEndDates {yy.enableInclusiveEndDates();$$=$1.substr(18);} @@ -130,6 +147,7 @@ statement | excludes {yy.setExcludes($1.substr(9));$$=$1.substr(9);} | includes {yy.setIncludes($1.substr(9));$$=$1.substr(9);} | todayMarker {yy.setTodayMarker($1.substr(12));$$=$1.substr(12);} + | weekday | title {yy.setDiagramTitle($1.substr(6));$$=$1.substr(6);} | acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); } | acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); } diff --git a/packages/mermaid/src/diagrams/gantt/parser/gantt.spec.js b/packages/mermaid/src/diagrams/gantt/parser/gantt.spec.js index 020bab0ed3..e7ce1ffa4f 100644 --- a/packages/mermaid/src/diagrams/gantt/parser/gantt.spec.js +++ b/packages/mermaid/src/diagrams/gantt/parser/gantt.spec.js @@ -180,4 +180,12 @@ row2`; expect(ganttDb.getAccTitle()).toBe(expectedTitle); expect(ganttDb.getAccDescription()).toBe(expectedAccDescription); }); + + it.each(['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'])( + 'should allow for setting the starting weekday to %s for tick interval', + (day) => { + parser.parse(`gantt\nweekday ${day}`); + expect(ganttDb.getWeekday()).toBe(day); + } + ); }); diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.js b/packages/mermaid/src/diagrams/git/gitGraphAst.js index 416479d151..dc9c32da9b 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.js +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.js @@ -12,7 +12,7 @@ import { clear as commonClear, setDiagramTitle, getDiagramTitle, -} from '../../commonDb.js'; +} from '../common/commonDb.js'; let mainBranchName = getConfig().gitGraph.mainBranchName; let mainBranchOrder = getConfig().gitGraph.mainBranchOrder; diff --git a/packages/mermaid/src/diagrams/git/gitGraphDiagram.ts b/packages/mermaid/src/diagrams/git/gitGraphDiagram.ts index 08ff126c44..2a9efdb59c 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphDiagram.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphDiagram.ts @@ -1,9 +1,9 @@ -// @ts-ignore: TODO Fix ts errors +// @ts-ignore: JISON doesn't support types import gitGraphParser from './parser/gitGraph.jison'; import gitGraphDb from './gitGraphAst.js'; import gitGraphRenderer from './gitGraphRenderer.js'; import gitGraphStyles from './styles.js'; -import { DiagramDefinition } from '../../diagram-api/types.js'; +import type { DiagramDefinition } from '../../diagram-api/types.js'; export const diagram: DiagramDefinition = { parser: gitGraphParser, diff --git a/packages/mermaid/src/diagrams/git/gitGraphRenderer-old.js b/packages/mermaid/src/diagrams/git/gitGraphRenderer-old.js index b8cff72ec2..a3b05ad79f 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphRenderer-old.js +++ b/packages/mermaid/src/diagrams/git/gitGraphRenderer-old.js @@ -1,7 +1,6 @@ import { curveBasis, line, select } from 'd3'; import db from './gitGraphAst.js'; -import gitGraphParser from './parser/gitGraph.js'; import { logger } from '../../logger.js'; import { interpolateToCurve } from '../../utils.js'; @@ -328,13 +327,7 @@ function renderLines(svg, commit, direction, branchColor = 0) { export const draw = function (txt, id, ver) { try { - const parser = gitGraphParser.parser; - parser.yy = db; - parser.yy.clear(); - logger.debug('in gitgraph renderer', txt + '\n', 'id:', id, ver); - // Parse the graph definition - parser.parse(txt + '\n'); config = Object.assign(config, apiConfig, db.getOptions()); logger.debug('effective options', config); diff --git a/packages/mermaid/src/diagrams/git/gitGraphRenderer.js b/packages/mermaid/src/diagrams/git/gitGraphRenderer.js index 8d88c601dd..2b88cfe7ef 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphRenderer.js +++ b/packages/mermaid/src/diagrams/git/gitGraphRenderer.js @@ -19,12 +19,14 @@ let branchPos = {}; let commitPos = {}; let lanes = []; let maxPos = 0; +let dir = 'LR'; const clear = () => { branchPos = {}; commitPos = {}; allCommitsDict = {}; maxPos = 0; lanes = []; + dir = 'LR'; }; /** @@ -77,6 +79,10 @@ const drawCommits = (svg, commits, modifyGraph) => { const gLabels = svg.append('g').attr('class', 'commit-labels'); let pos = 0; + if (dir === 'TB') { + pos = 30; + } + const keys = Object.keys(commits); const sortedKeys = keys.sort((a, b) => { return commits[a].seq - commits[b].seq; @@ -84,8 +90,9 @@ const drawCommits = (svg, commits, modifyGraph) => { sortedKeys.forEach((key) => { const commit = commits[key]; - const y = branchPos[commit.branch].pos; - const x = pos + 10; + const y = dir === 'TB' ? pos + 10 : branchPos[commit.branch].pos; + const x = dir === 'TB' ? branchPos[commit.branch].pos : pos + 10; + // Don't draw the commits now but calculate the positioning which is used by the branch lines etc. if (modifyGraph) { let typeClass; @@ -208,7 +215,11 @@ const drawCommits = (svg, commits, modifyGraph) => { } } } - commitPos[commit.id] = { x: pos + 10, y: y }; + if (dir === 'TB') { + commitPos[commit.id] = { x: x, y: pos + 10 }; + } else { + commitPos[commit.id] = { x: pos + 10, y: y }; + } // The first iteration over the commits are for positioning purposes, this // is required for drawing the lines. The circles and labels is drawn after the labels @@ -240,14 +251,27 @@ const drawCommits = (svg, commits, modifyGraph) => { .attr('y', y + 13.5) .attr('width', bbox.width + 2 * py) .attr('height', bbox.height + 2 * py); - text.attr('x', pos + 10 - bbox.width / 2); + + if (dir === 'TB') { + labelBkg.attr('x', x - (bbox.width + 4 * px + 5)).attr('y', y - 12); + text.attr('x', x - (bbox.width + 4 * px)).attr('y', y + bbox.height - 12); + } + + if (dir !== 'TB') { + text.attr('x', pos + 10 - bbox.width / 2); + } if (gitGraphConfig.rotateCommitLabel) { - let r_x = -7.5 - ((bbox.width + 10) / 25) * 9.5; - let r_y = 10 + (bbox.width / 25) * 8.5; - wrapper.attr( - 'transform', - 'translate(' + r_x + ', ' + r_y + ') rotate(' + -45 + ', ' + pos + ', ' + y + ')' - ); + if (dir === 'TB') { + text.attr('transform', 'rotate(' + -45 + ', ' + x + ', ' + y + ')'); + labelBkg.attr('transform', 'rotate(' + -45 + ', ' + x + ', ' + y + ')'); + } else { + let r_x = -7.5 - ((bbox.width + 10) / 25) * 9.5; + let r_y = 10 + (bbox.width / 25) * 8.5; + wrapper.attr( + 'transform', + 'translate(' + r_x + ', ' + r_y + ') rotate(' + -45 + ', ' + pos + ', ' + y + ')' + ); + } } } if (commit.tag) { @@ -280,6 +304,30 @@ const drawCommits = (svg, commits, modifyGraph) => { .attr('cy', ly) .attr('r', 1.5) .attr('class', 'tag-hole'); + + if (dir === 'TB') { + rect + .attr('class', 'tag-label-bkg') + .attr( + 'points', + ` + ${x},${pos + py} + ${x},${pos - py} + ${x + 10},${pos - h2 - py} + ${x + 10 + tagBbox.width + px},${pos - h2 - py} + ${x + 10 + tagBbox.width + px},${pos + h2 + py} + ${x + 10},${pos + h2 + py}` + ) + .attr('transform', 'translate(12,12) rotate(45, ' + x + ',' + pos + ')'); + hole + .attr('cx', x + px / 2) + .attr('cy', pos) + .attr('transform', 'translate(12,12) rotate(45, ' + x + ',' + pos + ')'); + tag + .attr('x', x + 5) + .attr('y', pos + 3) + .attr('transform', 'translate(14,14) rotate(45, ' + x + ',' + pos + ')'); + } } } pos += 50; @@ -365,46 +413,94 @@ const drawArrow = (svg, commit1, commit2, allCommits) => { colorClassNum = branchPos[commit2.branch].index; const lineY = p1.y < p2.y ? findLane(p1.y, p2.y) : findLane(p2.y, p1.y); + const lineX = p1.x < p2.x ? findLane(p1.x, p2.x) : findLane(p2.x, p1.x); - if (p1.y < p2.y) { - lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${lineY - radius} ${arc} ${p1.x + offset} ${lineY} L ${ - p2.x - radius - } ${lineY} ${arc2} ${p2.x} ${lineY + offset} L ${p2.x} ${p2.y}`; + if (dir === 'TB') { + if (p1.x < p2.x) { + lineDef = `M ${p1.x} ${p1.y} L ${lineX - radius} ${p1.y} ${arc2} ${lineX} ${ + p1.y + offset + } L ${lineX} ${p2.y - radius} ${arc} ${lineX + offset} ${p2.y} L ${p2.x} ${p2.y}`; + } else { + lineDef = `M ${p1.x} ${p1.y} L ${lineX + radius} ${p1.y} ${arc} ${lineX} ${ + p1.y + offset + } L ${lineX} ${p2.y - radius} ${arc2} ${lineX - offset} ${p2.y} L ${p2.x} ${p2.y}`; + } } else { - lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${lineY + radius} ${arc2} ${ - p1.x + offset - } ${lineY} L ${p2.x - radius} ${lineY} ${arc} ${p2.x} ${lineY - offset} L ${p2.x} ${p2.y}`; + if (p1.y < p2.y) { + lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${lineY - radius} ${arc} ${ + p1.x + offset + } ${lineY} L ${p2.x - radius} ${lineY} ${arc2} ${p2.x} ${lineY + offset} L ${p2.x} ${p2.y}`; + } else { + lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${lineY + radius} ${arc2} ${ + p1.x + offset + } ${lineY} L ${p2.x - radius} ${lineY} ${arc} ${p2.x} ${lineY - offset} L ${p2.x} ${p2.y}`; + } } } else { - if (p1.y < p2.y) { - arc = 'A 20 20, 0, 0, 0,'; - radius = 20; - offset = 20; + if (dir === 'TB') { + if (p1.x < p2.x) { + arc = 'A 20 20, 0, 0, 0,'; + arc2 = 'A 20 20, 0, 0, 1,'; + radius = 20; + offset = 20; + + // Figure out the color of the arrow,arrows going down take the color from the destination branch + colorClassNum = branchPos[commit2.branch].index; + + lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc2} ${p2.x} ${ + p1.y + offset + } L ${p2.x} ${p2.y}`; + } + if (p1.x > p2.x) { + arc = 'A 20 20, 0, 0, 0,'; + arc2 = 'A 20 20, 0, 0, 1,'; + radius = 20; + offset = 20; + + // Arrows going up take the color from the source branch + colorClassNum = branchPos[commit1.branch].index; + lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc2} ${p1.x - offset} ${ + p2.y + } L ${p2.x} ${p2.y}`; + } - // Figure out the color of the arrow,arrows going down take the color from the destination branch - colorClassNum = branchPos[commit2.branch].index; + if (p1.x === p2.x) { + colorClassNum = branchPos[commit1.branch].index; + lineDef = `M ${p1.x} ${p1.y} L ${p1.x + radius} ${p1.y} ${arc} ${p1.x + offset} ${ + p2.y + radius + } L ${p2.x} ${p2.y}`; + } + } else { + if (p1.y < p2.y) { + arc = 'A 20 20, 0, 0, 0,'; + radius = 20; + offset = 20; - lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc} ${p1.x + offset} ${p2.y} L ${ - p2.x - } ${p2.y}`; - } - if (p1.y > p2.y) { - arc = 'A 20 20, 0, 0, 0,'; - radius = 20; - offset = 20; - - // Arrows going up take the color from the source branch - colorClassNum = branchPos[commit1.branch].index; - lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc} ${p2.x} ${p1.y - offset} L ${ - p2.x - } ${p2.y}`; - } + // Figure out the color of the arrow,arrows going down take the color from the destination branch + colorClassNum = branchPos[commit2.branch].index; + + lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc} ${p1.x + offset} ${p2.y} L ${ + p2.x + } ${p2.y}`; + } + if (p1.y > p2.y) { + arc = 'A 20 20, 0, 0, 0,'; + radius = 20; + offset = 20; + + // Arrows going up take the color from the source branch + colorClassNum = branchPos[commit1.branch].index; + lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc} ${p2.x} ${p1.y - offset} L ${ + p2.x + } ${p2.y}`; + } - if (p1.y === p2.y) { - colorClassNum = branchPos[commit1.branch].index; - lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc} ${p1.x + offset} ${p2.y} L ${ - p2.x - } ${p2.y}`; + if (p1.y === p2.y) { + colorClassNum = branchPos[commit1.branch].index; + lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc} ${p1.x + offset} ${p2.y} L ${ + p2.x + } ${p2.y}`; + } } } svg @@ -445,6 +541,13 @@ const drawBranches = (svg, branches) => { line.attr('y2', pos); line.attr('class', 'branch branch' + adjustIndexForTheme); + if (dir === 'TB') { + line.attr('y1', 30); + line.attr('x1', pos); + line.attr('y2', maxPos); + line.attr('x2', pos); + } + lanes.push(pos); let name = branch.name; @@ -467,7 +570,6 @@ const drawBranches = (svg, branches) => { .attr('y', -bbox.height / 2 + 8) .attr('width', bbox.width + 18) .attr('height', bbox.height + 4); - label.attr( 'transform', 'translate(' + @@ -476,7 +578,13 @@ const drawBranches = (svg, branches) => { (pos - bbox.height / 2 - 1) + ')' ); - bkg.attr('transform', 'translate(' + -19 + ', ' + (pos - bbox.height / 2) + ')'); + if (dir === 'TB') { + bkg.attr('x', pos - bbox.width / 2 - 10).attr('y', 0); + label.attr('transform', 'translate(' + (pos - bbox.width / 2 - 5) + ', ' + 0 + ')'); + } + if (dir !== 'TB') { + bkg.attr('transform', 'translate(' + -19 + ', ' + (pos - bbox.height / 2) + ')'); + } }); }; @@ -495,16 +603,25 @@ export const draw = function (txt, id, ver, diagObj) { allCommitsDict = diagObj.db.getCommits(); const branches = diagObj.db.getBranchesAsObjArray(); - - // Position branches vertically + dir = diagObj.db.getDirection(); + const diagram = select(`[id="${id}"]`); + // Position branches let pos = 0; branches.forEach((branch, index) => { + const labelElement = drawText(branch.name); + const g = diagram.append('g'); + const branchLabel = g.insert('g').attr('class', 'branchLabel'); + const label = branchLabel.insert('g').attr('class', 'label branch-label'); + label.node().appendChild(labelElement); + let bbox = labelElement.getBBox(); + branchPos[branch.name] = { pos, index }; - pos += 50 + (gitGraphConfig.rotateCommitLabel ? 40 : 0); + pos += 50 + (gitGraphConfig.rotateCommitLabel ? 40 : 0) + (dir === 'TB' ? bbox.width / 2 : 0); + label.remove(); + branchLabel.remove(); + g.remove(); }); - const diagram = select(`[id="${id}"]`); - drawCommits(diagram, allCommitsDict, false); if (gitGraphConfig.showBranches) { drawBranches(diagram, branches); diff --git a/packages/mermaid/src/diagrams/git/parser/gitGraph.jison b/packages/mermaid/src/diagrams/git/parser/gitGraph.jison index 1c87f3bf35..9ff5623f83 100644 --- a/packages/mermaid/src/diagrams/git/parser/gitGraph.jison +++ b/packages/mermaid/src/diagrams/git/parser/gitGraph.jison @@ -51,7 +51,7 @@ cherry\-pick(?=\s|$) return 'CHERRY_PICK'; // "reset" return 'RESET'; checkout(?=\s|$) return 'CHECKOUT'; "LR" return 'DIR'; -"BT" return 'DIR'; +"TB" return 'DIR'; ":" return ':'; "^" return 'CARET' "options"\r?\n this.begin("options"); // diff --git a/packages/mermaid/src/diagrams/info/infoRenderer.ts b/packages/mermaid/src/diagrams/info/infoRenderer.ts index 79f3ec9920..25ae72fce0 100644 --- a/packages/mermaid/src/diagrams/info/infoRenderer.ts +++ b/packages/mermaid/src/diagrams/info/infoRenderer.ts @@ -1,7 +1,7 @@ -import { select } from 'd3'; import { log } from '../../logger.js'; -import { getConfig } from '../../config.js'; -import type { DrawDefinition, HTML, SVG } from '../../diagram-api/types.js'; +import { configureSvgSize } from '../../setupGraphViewbox.js'; +import type { DrawDefinition, Group, SVG } from '../../diagram-api/types.js'; +import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; /** * Draws a an info picture in the tag with id: id based on the graph definition in text. @@ -11,40 +11,20 @@ import type { DrawDefinition, HTML, SVG } from '../../diagram-api/types.js'; * @param version - MermaidJS version. */ const draw: DrawDefinition = (text, id, version) => { - try { - log.debug('rendering info diagram\n' + text); + log.debug('rendering info diagram\n' + text); - const { securityLevel } = getConfig(); - // handle root and document for when rendering in sandbox mode - let sandboxElement: HTML | undefined; - let document: Document | null | undefined; - if (securityLevel === 'sandbox') { - sandboxElement = select('#i' + id); - document = sandboxElement.nodes()[0].contentDocument; - } + const svg: SVG = selectSvgElement(id); + configureSvgSize(svg, 100, 400, true); - // @ts-ignore - figure out how to assign HTML to document type - const root: HTML = - sandboxElement !== undefined && document !== undefined && document !== null - ? select(document) - : select('body'); - - const svg: SVG = root.select('#' + id); - svg.attr('height', 100); - svg.attr('width', 400); - - const g = svg.append('g'); - - g.append('text') // text label for the x axis - .attr('x', 100) - .attr('y', 40) - .attr('class', 'version') - .attr('font-size', '32px') - .style('text-anchor', 'middle') - .text('v ' + version); - } catch (e) { - log.error('error while rendering info diagram', e); - } + const group: Group = svg.append('g'); + group + .append('text') + .attr('x', 100) + .attr('y', 40) + .attr('class', 'version') + .attr('font-size', 32) + .style('text-anchor', 'middle') + .text(`v${version}`); }; export const renderer = { draw }; diff --git a/packages/mermaid/src/diagrams/mindmap/mindmap-definition.ts b/packages/mermaid/src/diagrams/mindmap/mindmap-definition.ts index 846fd5dc58..b352144359 100644 --- a/packages/mermaid/src/diagrams/mindmap/mindmap-definition.ts +++ b/packages/mermaid/src/diagrams/mindmap/mindmap-definition.ts @@ -1,4 +1,4 @@ -// @ts-ignore: TODO Fix ts errors +// @ts-ignore: JISON doesn't support types import mindmapParser from './parser/mindmap.jison'; import * as mindmapDb from './mindmapDb.js'; import mindmapRenderer from './mindmapRenderer.js'; diff --git a/packages/mermaid/src/diagrams/mindmap/mindmapRenderer.js b/packages/mermaid/src/diagrams/mindmap/mindmapRenderer.js index 01d675d839..7e741657b9 100644 --- a/packages/mermaid/src/diagrams/mindmap/mindmapRenderer.js +++ b/packages/mermaid/src/diagrams/mindmap/mindmapRenderer.js @@ -167,14 +167,8 @@ function positionNodes(cy) { export const draw = async (text, id, version, diagObj) => { const conf = getConfig(); - // console.log('Config: ', conf); conf.htmlLabels = false; - // This is done only for throwing the error if the text is not valid. - diagObj.db.clear(); - // Parse the graph definition - diagObj.parser.parse(text); - log.debug('Rendering mindmap diagram\n' + text, diagObj.parser); const securityLevel = getConfig().securityLevel; diff --git a/packages/mermaid/src/diagrams/mindmap/parser/mindmap.jison b/packages/mermaid/src/diagrams/mindmap/parser/mindmap.jison index 9dd046a3d6..afd5e2300d 100644 --- a/packages/mermaid/src/diagrams/mindmap/parser/mindmap.jison +++ b/packages/mermaid/src/diagrams/mindmap/parser/mindmap.jison @@ -40,7 +40,7 @@ "[" { this.begin('NODE');return 'NODE_DSTART'; } [\s]+ return 'SPACELIST' /* skip all whitespace */ ; // !(-\() return 'NODE_ID'; -[^\(\[\n\-\)\{\}]+ return 'NODE_ID'; +[^\(\[\n\)\{\}]+ return 'NODE_ID'; <> return 'EOF'; ["][`] { this.begin("NSTR2");} [^`"]+ { return "NODE_DESCR";} diff --git a/packages/mermaid/src/diagrams/pie/amonts.csv b/packages/mermaid/src/diagrams/pie/amonts.csv deleted file mode 100644 index 25cf919ddb..0000000000 --- a/packages/mermaid/src/diagrams/pie/amonts.csv +++ /dev/null @@ -1,10 +0,0 @@ -name,amounts -Foo, 33 -Rishab, 12 -Alexis, 41 -Tom, 16 -Courtney, 59 -Christina, 38 -Jack, 21 -Mickey, 25 -Paul, 30 diff --git a/packages/mermaid/src/diagrams/pie/parser/pie.spec.js b/packages/mermaid/src/diagrams/pie/parser/pie.spec.js deleted file mode 100644 index 5e5c0b4f51..0000000000 --- a/packages/mermaid/src/diagrams/pie/parser/pie.spec.js +++ /dev/null @@ -1,132 +0,0 @@ -import pieDb from '../pieDb.js'; -import pie from './pie.jison'; -import { setConfig } from '../../../config.js'; - -setConfig({ - securityLevel: 'strict', -}); - -describe('when parsing pie', function () { - beforeEach(function () { - pie.parser.yy = pieDb; - pie.parser.yy.clear(); - }); - it('should handle very simple pie', function () { - const res = pie.parser.parse(`pie -"ash" : 100 -`); - const sections = pieDb.getSections(); - const section1 = sections['ash']; - expect(section1).toBe(100); - }); - it('should handle simple pie', function () { - const res = pie.parser.parse(`pie -"ash" : 60 -"bat" : 40 -`); - const sections = pieDb.getSections(); - const section1 = sections['ash']; - expect(section1).toBe(60); - }); - it('should handle simple pie with comments', function () { - const res = pie.parser.parse(`pie - %% comments -"ash" : 60 -"bat" : 40 -`); - const sections = pieDb.getSections(); - const section1 = sections['ash']; - expect(section1).toBe(60); - }); - - it('should handle simple pie with a directive', function () { - const res = pie.parser.parse(`%%{init: {'logLevel':0}}%% -pie -"ash" : 60 -"bat" : 40 -`); - const sections = pieDb.getSections(); - const section1 = sections['ash']; - expect(section1).toBe(60); - }); - - it('should handle simple pie with a title', function () { - const res = pie.parser.parse(`pie title a 60/40 pie -"ash" : 60 -"bat" : 40 -`); - const sections = pieDb.getSections(); - const title = pieDb.getDiagramTitle(); - const section1 = sections['ash']; - expect(section1).toBe(60); - expect(title).toBe('a 60/40 pie'); - }); - - it('should handle simple pie without an acc description (accDescr)', function () { - const res = pie.parser.parse(`pie title a neat chart -"ash" : 60 -"bat" : 40 -`); - - const sections = pieDb.getSections(); - const title = pieDb.getDiagramTitle(); - const description = pieDb.getAccDescription(); - const section1 = sections['ash']; - expect(section1).toBe(60); - expect(title).toBe('a neat chart'); - expect(description).toBe(''); - }); - - it('should handle simple pie with an acc description (accDescr)', function () { - const res = pie.parser.parse(`pie title a neat chart - accDescr: a neat description -"ash" : 60 -"bat" : 40 -`); - - const sections = pieDb.getSections(); - const title = pieDb.getDiagramTitle(); - const description = pieDb.getAccDescription(); - const section1 = sections['ash']; - expect(section1).toBe(60); - expect(title).toBe('a neat chart'); - expect(description).toBe('a neat description'); - }); - it('should handle simple pie with a multiline acc description (accDescr)', function () { - const res = pie.parser.parse(`pie title a neat chart - accDescr { - a neat description - on multiple lines - } -"ash" : 60 -"bat" : 40 -`); - - const sections = pieDb.getSections(); - const title = pieDb.getDiagramTitle(); - const description = pieDb.getAccDescription(); - const section1 = sections['ash']; - expect(section1).toBe(60); - expect(title).toBe('a neat chart'); - expect(description).toBe('a neat description\non multiple lines'); - }); - - it('should handle simple pie with positive decimal', function () { - const res = pie.parser.parse(`pie -"ash" : 60.67 -"bat" : 40 -`); - const sections = pieDb.getSections(); - const section1 = sections['ash']; - expect(section1).toBe(60.67); - }); - - it('should handle simple pie with negative decimal', function () { - expect(() => { - pie.parser.parse(`pie -"ash" : 60.67 -"bat" : 40..12 -`); - }).toThrowError(); - }); -}); diff --git a/packages/mermaid/src/diagrams/pie/pie.spec.ts b/packages/mermaid/src/diagrams/pie/pie.spec.ts new file mode 100644 index 0000000000..7c8e0809a3 --- /dev/null +++ b/packages/mermaid/src/diagrams/pie/pie.spec.ts @@ -0,0 +1,180 @@ +// @ts-ignore: JISON doesn't support types +import { parser } from './parser/pie.jison'; +import { DEFAULT_PIE_DB, db } from './pieDb.js'; +import { setConfig } from '../../config.js'; + +setConfig({ + securityLevel: 'strict', +}); + +describe('pie', () => { + beforeAll(() => { + parser.yy = db; + }); + + beforeEach(() => { + parser.yy.clear(); + }); + + describe('parse', () => { + it('should handle very simple pie', () => { + parser.parse(`pie + "ash": 100 + `); + + const sections = db.getSections(); + expect(sections['ash']).toBe(100); + }); + + it('should handle simple pie', () => { + parser.parse(`pie + "ash" : 60 + "bat" : 40 + `); + + const sections = db.getSections(); + expect(sections['ash']).toBe(60); + expect(sections['bat']).toBe(40); + }); + + it('should handle simple pie with showData', () => { + parser.parse(`pie showData + "ash" : 60 + "bat" : 40 + `); + + expect(db.getShowData()).toBeTruthy(); + + const sections = db.getSections(); + expect(sections['ash']).toBe(60); + expect(sections['bat']).toBe(40); + }); + + it('should handle simple pie with comments', () => { + parser.parse(`pie + %% comments + "ash" : 60 + "bat" : 40 + `); + + const sections = db.getSections(); + expect(sections['ash']).toBe(60); + expect(sections['bat']).toBe(40); + }); + + it('should handle simple pie with a directive', () => { + parser.parse(`%%{init: {'logLevel':0}}%% + pie + "ash" : 60 + "bat" : 40 + `); + const sections = db.getSections(); + expect(sections['ash']).toBe(60); + expect(sections['bat']).toBe(40); + }); + + it('should handle simple pie with a title', () => { + parser.parse(`pie title a 60/40 pie + "ash" : 60 + "bat" : 40 + `); + + expect(db.getDiagramTitle()).toBe('a 60/40 pie'); + + const sections = db.getSections(); + expect(sections['ash']).toBe(60); + expect(sections['bat']).toBe(40); + }); + + it('should handle simple pie with an acc title (accTitle)', () => { + parser.parse(`pie title a neat chart + accTitle: a neat acc title + "ash" : 60 + "bat" : 40 + `); + + expect(db.getDiagramTitle()).toBe('a neat chart'); + + expect(db.getAccTitle()).toBe('a neat acc title'); + + const sections = db.getSections(); + expect(sections['ash']).toBe(60); + expect(sections['bat']).toBe(40); + }); + + it('should handle simple pie with an acc description (accDescr)', () => { + parser.parse(`pie title a neat chart + accDescr: a neat description + "ash" : 60 + "bat" : 40 + `); + + expect(db.getDiagramTitle()).toBe('a neat chart'); + + expect(db.getAccDescription()).toBe('a neat description'); + + const sections = db.getSections(); + expect(sections['ash']).toBe(60); + expect(sections['bat']).toBe(40); + }); + + it('should handle simple pie with a multiline acc description (accDescr)', () => { + parser.parse(`pie title a neat chart + accDescr { + a neat description + on multiple lines + } + "ash" : 60 + "bat" : 40 + `); + + expect(db.getDiagramTitle()).toBe('a neat chart'); + + expect(db.getAccDescription()).toBe('a neat description\non multiple lines'); + + const sections = db.getSections(); + expect(sections['ash']).toBe(60); + expect(sections['bat']).toBe(40); + }); + + it('should handle simple pie with positive decimal', () => { + parser.parse(`pie + "ash" : 60.67 + "bat" : 40 + `); + + const sections = db.getSections(); + expect(sections['ash']).toBe(60.67); + expect(sections['bat']).toBe(40); + }); + + it('should handle simple pie with negative decimal', () => { + expect(() => { + parser.parse(`pie + "ash" : -60.67 + "bat" : 40.12 + `); + }).toThrowError(); + }); + }); + + describe('config', () => { + it.todo('setConfig', () => { + // db.setConfig({ useWidth: 850, useMaxWidth: undefined }); + + const config = db.getConfig(); + expect(config.useWidth).toBe(850); + expect(config.useMaxWidth).toBeTruthy(); + }); + + it('getConfig', () => { + expect(db.getConfig()).toStrictEqual(DEFAULT_PIE_DB.config); + }); + + it.todo('resetConfig', () => { + // db.setConfig({ textPosition: 0 }); + // db.resetConfig(); + expect(db.getConfig().textPosition).toStrictEqual(DEFAULT_PIE_DB.config.textPosition); + }); + }); +}); diff --git a/packages/mermaid/src/diagrams/pie/pieDb.js b/packages/mermaid/src/diagrams/pie/pieDb.js deleted file mode 100644 index 2c86752c66..0000000000 --- a/packages/mermaid/src/diagrams/pie/pieDb.js +++ /dev/null @@ -1,69 +0,0 @@ -import { log } from '../../logger.js'; -import mermaidAPI from '../../mermaidAPI.js'; -import * as configApi from '../../config.js'; -import common from '../common/common.js'; -import { - setAccTitle, - getAccTitle, - setDiagramTitle, - getDiagramTitle, - getAccDescription, - setAccDescription, - clear as commonClear, -} from '../../commonDb.js'; - -let sections = {}; -let showData = false; - -export const parseDirective = function (statement, context, type) { - mermaidAPI.parseDirective(this, statement, context, type); -}; - -const addSection = function (id, value) { - id = common.sanitizeText(id, configApi.getConfig()); - if (sections[id] === undefined) { - sections[id] = value; - log.debug('Added new section :', id); - } -}; -const getSections = () => sections; - -const setShowData = function (toggle) { - showData = toggle; -}; - -const getShowData = function () { - return showData; -}; - -const cleanupValue = function (value) { - if (value.substring(0, 1) === ':') { - value = value.substring(1).trim(); - return Number(value.trim()); - } else { - return Number(value.trim()); - } -}; - -const clear = function () { - sections = {}; - showData = false; - commonClear(); -}; - -export default { - parseDirective, - getConfig: () => configApi.getConfig().pie, - addSection, - getSections, - cleanupValue, - clear, - setAccTitle, - getAccTitle, - setDiagramTitle, - getDiagramTitle, - setShowData, - getShowData, - getAccDescription, - setAccDescription, -}; diff --git a/packages/mermaid/src/diagrams/pie/pieDb.ts b/packages/mermaid/src/diagrams/pie/pieDb.ts new file mode 100644 index 0000000000..7f209de460 --- /dev/null +++ b/packages/mermaid/src/diagrams/pie/pieDb.ts @@ -0,0 +1,84 @@ +import { log } from '../../logger.js'; +import { parseDirective as _parseDirective } from '../../directiveUtils.js'; +import { getConfig as commonGetConfig } from '../../config.js'; +import { sanitizeText } from '../common/common.js'; +import { + setAccTitle, + getAccTitle, + setDiagramTitle, + getDiagramTitle, + getAccDescription, + setAccDescription, + clear as commonClear, +} from '../common/commonDb.js'; +import type { ParseDirectiveDefinition } from '../../diagram-api/types.js'; +import type { PieFields, PieDB, Sections } from './pieTypes.js'; +import type { RequiredDeep } from 'type-fest'; +import type { PieDiagramConfig } from '../../config.type.js'; +import DEFAULT_CONFIG from '../../defaultConfig.js'; + +export const DEFAULT_PIE_CONFIG: Required = DEFAULT_CONFIG.pie; + +export const DEFAULT_PIE_DB: RequiredDeep = { + sections: {}, + showData: false, + config: DEFAULT_PIE_CONFIG, +} as const; + +let sections: Sections = DEFAULT_PIE_DB.sections; +let showData: boolean = DEFAULT_PIE_DB.showData; +const config: Required = structuredClone(DEFAULT_PIE_CONFIG); + +const getConfig = (): Required => structuredClone(config); + +const parseDirective: ParseDirectiveDefinition = (statement, context, type) => { + _parseDirective(this, statement, context, type); +}; + +const clear = (): void => { + sections = structuredClone(DEFAULT_PIE_DB.sections); + showData = DEFAULT_PIE_DB.showData; + commonClear(); +}; + +const addSection = (label: string, value: number): void => { + label = sanitizeText(label, commonGetConfig()); + if (sections[label] === undefined) { + sections[label] = value; + log.debug(`added new section: ${label}, with value: ${value}`); + } +}; + +const getSections = (): Sections => sections; + +const cleanupValue = (value: string): number => { + if (value.substring(0, 1) === ':') { + value = value.substring(1).trim(); + } + return Number(value.trim()); +}; + +const setShowData = (toggle: boolean): void => { + showData = toggle; +}; + +const getShowData = (): boolean => showData; + +export const db: PieDB = { + getConfig, + + parseDirective, + clear, + setDiagramTitle, + getDiagramTitle, + setAccTitle, + getAccTitle, + setAccDescription, + getAccDescription, + + addSection, + getSections, + cleanupValue, + setShowData, + getShowData, +}; diff --git a/packages/mermaid/src/diagrams/pie/pieDetector.ts b/packages/mermaid/src/diagrams/pie/pieDetector.ts index 93eded52cd..f5acd1aa03 100644 --- a/packages/mermaid/src/diagrams/pie/pieDetector.ts +++ b/packages/mermaid/src/diagrams/pie/pieDetector.ts @@ -15,10 +15,8 @@ const loader: DiagramLoader = async () => { return { id, diagram }; }; -const plugin: ExternalDiagramDefinition = { +export const pie: ExternalDiagramDefinition = { id, detector, loader, }; - -export default plugin; diff --git a/packages/mermaid/src/diagrams/pie/pieDiagram.ts b/packages/mermaid/src/diagrams/pie/pieDiagram.ts index 4c6b7d3bc9..f0aa19b419 100644 --- a/packages/mermaid/src/diagrams/pie/pieDiagram.ts +++ b/packages/mermaid/src/diagrams/pie/pieDiagram.ts @@ -1,9 +1,9 @@ -import { DiagramDefinition } from '../../diagram-api/types.js'; -// @ts-ignore: TODO Fix ts errors +import type { DiagramDefinition } from '../../diagram-api/types.js'; +// @ts-ignore: JISON doesn't support types import parser from './parser/pie.jison'; -import db from './pieDb.js'; -import styles from './styles.js'; -import renderer from './pieRenderer.js'; +import { db } from './pieDb.js'; +import styles from './pieStyles.js'; +import { renderer } from './pieRenderer.js'; export const diagram: DiagramDefinition = { parser, diff --git a/packages/mermaid/src/diagrams/pie/pieRenderer.js b/packages/mermaid/src/diagrams/pie/pieRenderer.js deleted file mode 100644 index 1ee34e192a..0000000000 --- a/packages/mermaid/src/diagrams/pie/pieRenderer.js +++ /dev/null @@ -1,207 +0,0 @@ -/** Created by AshishJ on 11-09-2019. */ -import { select, scaleOrdinal, pie as d3pie, arc } from 'd3'; -import { log } from '../../logger.js'; -import { configureSvgSize } from '../../setupGraphViewbox.js'; -import * as configApi from '../../config.js'; -import { parseFontSize } from '../../utils.js'; - -let conf = configApi.getConfig(); - -/** - * Draws a Pie Chart with the data given in text. - * - * @param text - * @param id - */ -let width; -const height = 450; -export const draw = (txt, id, _version, diagObj) => { - try { - conf = configApi.getConfig(); - log.debug('Rendering info diagram\n' + txt); - - const securityLevel = configApi.getConfig().securityLevel; - // Handle root and Document for when rendering in sandbox mode - let sandboxElement; - if (securityLevel === 'sandbox') { - sandboxElement = select('#i' + id); - } - const root = - securityLevel === 'sandbox' - ? select(sandboxElement.nodes()[0].contentDocument.body) - : select('body'); - const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document; - - // Parse the Pie Chart definition - diagObj.db.clear(); - diagObj.parser.parse(txt); - log.debug('Parsed info diagram'); - const elem = doc.getElementById(id); - width = elem.parentElement.offsetWidth; - - if (width === undefined) { - width = 1200; - } - - if (conf.useWidth !== undefined) { - width = conf.useWidth; - } - if (conf.pie.useWidth !== undefined) { - width = conf.pie.useWidth; - } - - const diagram = root.select('#' + id); - configureSvgSize(diagram, height, width, conf.pie.useMaxWidth); - - // Set viewBox - elem.setAttribute('viewBox', '0 0 ' + width + ' ' + height); - - // Fetch the default direction, use TD if none was found - var margin = 40; - var legendRectSize = 18; - var legendSpacing = 4; - - var radius = Math.min(width, height) / 2 - margin; - - var svg = diagram - .append('g') - .attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')'); - - var data = diagObj.db.getSections(); - var sum = 0; - Object.keys(data).forEach(function (key) { - sum += data[key]; - }); - - const themeVariables = conf.themeVariables; - var myGeneratedColors = [ - themeVariables.pie1, - themeVariables.pie2, - themeVariables.pie3, - themeVariables.pie4, - themeVariables.pie5, - themeVariables.pie6, - themeVariables.pie7, - themeVariables.pie8, - themeVariables.pie9, - themeVariables.pie10, - themeVariables.pie11, - themeVariables.pie12, - ]; - - const textPosition = conf.pie?.textPosition ?? 0.75; - let [outerStrokeWidth] = parseFontSize(themeVariables.pieOuterStrokeWidth); - outerStrokeWidth ??= 2; - - // Set the color scale - var color = scaleOrdinal().range(myGeneratedColors); - - // Compute the position of each group on the pie: - var pieData = Object.entries(data).map(function (el, idx) { - return { - order: idx, - name: el[0], - value: el[1], - }; - }); - var pie = d3pie() - .value(function (d) { - return d.value; - }) - .sort(function (a, b) { - // Sort slices in clockwise direction - return a.order - b.order; - }); - var dataReady = pie(pieData); - - // Shape helper to build arcs: - var arcGenerator = arc().innerRadius(0).outerRadius(radius); - var labelArcGenerator = arc() - .innerRadius(radius * textPosition) - .outerRadius(radius * textPosition); - - svg - .append('circle') - .attr('cx', 0) - .attr('cy', 0) - .attr('r', radius + outerStrokeWidth / 2) - .attr('class', 'pieOuterCircle'); - - // Build the pie chart: each part of the pie is a path that we build using the arc function. - svg - .selectAll('mySlices') - .data(dataReady) - .enter() - .append('path') - .attr('d', arcGenerator) - .attr('fill', function (d) { - return color(d.data.name); - }) - .attr('class', 'pieCircle'); - - // Now add the percentage. - // Use the centroid method to get the best coordinates. - svg - .selectAll('mySlices') - .data(dataReady) - .enter() - .append('text') - .text(function (d) { - return ((d.data.value / sum) * 100).toFixed(0) + '%'; - }) - .attr('transform', function (d) { - return 'translate(' + labelArcGenerator.centroid(d) + ')'; - }) - .style('text-anchor', 'middle') - .attr('class', 'slice'); - - svg - .append('text') - .text(diagObj.db.getDiagramTitle()) - .attr('x', 0) - .attr('y', -(height - 50) / 2) - .attr('class', 'pieTitleText'); - - // Add the legends/annotations for each section - var legend = svg - .selectAll('.legend') - .data(color.domain()) - .enter() - .append('g') - .attr('class', 'legend') - .attr('transform', function (d, i) { - const height = legendRectSize + legendSpacing; - const offset = (height * color.domain().length) / 2; - const horizontal = 12 * legendRectSize; - const vertical = i * height - offset; - return 'translate(' + horizontal + ',' + vertical + ')'; - }); - - legend - .append('rect') - .attr('width', legendRectSize) - .attr('height', legendRectSize) - .style('fill', color) - .style('stroke', color); - - legend - .data(dataReady) - .append('text') - .attr('x', legendRectSize + legendSpacing) - .attr('y', legendRectSize - legendSpacing) - .text(function (d) { - if (diagObj.db.getShowData() || conf.showData || conf.pie.showData) { - return d.data.name + ' [' + d.data.value + ']'; - } else { - return d.data.name; - } - }); - } catch (e) { - log.error('Error while rendering info diagram'); - log.error(e); - } -}; - -export default { - draw, -}; diff --git a/packages/mermaid/src/diagrams/pie/pieRenderer.ts b/packages/mermaid/src/diagrams/pie/pieRenderer.ts new file mode 100644 index 0000000000..80f4f0a5a4 --- /dev/null +++ b/packages/mermaid/src/diagrams/pie/pieRenderer.ts @@ -0,0 +1,182 @@ +import type d3 from 'd3'; +import { scaleOrdinal, pie as d3pie, arc } from 'd3'; + +import { log } from '../../logger.js'; +import { configureSvgSize } from '../../setupGraphViewbox.js'; +import { getConfig } from '../../config.js'; +import { cleanAndMerge, parseFontSize } from '../../utils.js'; +import type { DrawDefinition, Group, SVG } from '../../diagram-api/types.js'; +import type { D3Sections, PieDB, Sections } from './pieTypes.js'; +import type { MermaidConfig, PieDiagramConfig } from '../../config.type.js'; +import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; + +const createPieArcs = (sections: Sections): d3.PieArcDatum[] => { + // Compute the position of each group on the pie: + const pieData: D3Sections[] = Object.entries(sections) + .map((element: [string, number]): D3Sections => { + return { + label: element[0], + value: element[1], + }; + }) + .sort((a: D3Sections, b: D3Sections): number => { + return b.value - a.value; + }); + const pie: d3.Pie = d3pie().value( + (d3Section: D3Sections): number => d3Section.value + ); + return pie(pieData); +}; + +/** + * Draws a Pie Chart with the data given in text. + * + * @param text - pie chart code + * @param id - diagram id + * @param _version - MermaidJS version from package.json. + * @param diagObj - A standard diagram containing the DB and the text and type etc of the diagram. + */ +export const draw: DrawDefinition = (text, id, _version, diagObj) => { + log.debug('rendering pie chart\n' + text); + + const db = diagObj.db as PieDB; + const globalConfig: MermaidConfig = getConfig(); + const pieConfig: Required = cleanAndMerge(db.getConfig(), globalConfig.pie); + + const height = 450; + // TODO: remove document width + const width: number = + document.getElementById(id)?.parentElement?.offsetWidth ?? pieConfig.useWidth; + const svg: SVG = selectSvgElement(id); + // Set viewBox + svg.attr('viewBox', `0 0 ${width} ${height}`); + configureSvgSize(svg, height, width, pieConfig.useMaxWidth); + + const MARGIN = 40; + const LEGEND_RECT_SIZE = 18; + const LEGEND_SPACING = 4; + + const group: Group = svg.append('g'); + group.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')'); + + const { themeVariables } = globalConfig; + let [outerStrokeWidth] = parseFontSize(themeVariables.pieOuterStrokeWidth); + outerStrokeWidth ??= 2; + + const textPosition: number = pieConfig.textPosition; + const radius: number = Math.min(width, height) / 2 - MARGIN; + // Shape helper to build arcs: + const arcGenerator: d3.Arc> = arc< + d3.PieArcDatum + >() + .innerRadius(0) + .outerRadius(radius); + const labelArcGenerator: d3.Arc> = arc< + d3.PieArcDatum + >() + .innerRadius(radius * textPosition) + .outerRadius(radius * textPosition); + + group + .append('circle') + .attr('cx', 0) + .attr('cy', 0) + .attr('r', radius + outerStrokeWidth / 2) + .attr('class', 'pieOuterCircle'); + + const sections: Sections = db.getSections(); + const arcs: d3.PieArcDatum[] = createPieArcs(sections); + + const myGeneratedColors = [ + themeVariables.pie1, + themeVariables.pie2, + themeVariables.pie3, + themeVariables.pie4, + themeVariables.pie5, + themeVariables.pie6, + themeVariables.pie7, + themeVariables.pie8, + themeVariables.pie9, + themeVariables.pie10, + themeVariables.pie11, + themeVariables.pie12, + ]; + // Set the color scale + const color: d3.ScaleOrdinal = scaleOrdinal(myGeneratedColors); + + // Build the pie chart: each part of the pie is a path that we build using the arc function. + group + .selectAll('mySlices') + .data(arcs) + .enter() + .append('path') + .attr('d', arcGenerator) + .attr('fill', (datum: d3.PieArcDatum) => { + return color(datum.data.label); + }) + .attr('class', 'pieCircle'); + + let sum = 0; + Object.keys(sections).forEach((key: string): void => { + sum += sections[key]; + }); + // Now add the percentage. + // Use the centroid method to get the best coordinates. + group + .selectAll('mySlices') + .data(arcs) + .enter() + .append('text') + .text((datum: d3.PieArcDatum): string => { + return ((datum.data.value / sum) * 100).toFixed(0) + '%'; + }) + .attr('transform', (datum: d3.PieArcDatum): string => { + return 'translate(' + labelArcGenerator.centroid(datum) + ')'; + }) + .style('text-anchor', 'middle') + .attr('class', 'slice'); + + group + .append('text') + .text(db.getDiagramTitle()) + .attr('x', 0) + .attr('y', -(height - 50) / 2) + .attr('class', 'pieTitleText'); + + // Add the legends/annotations for each section + const legend = group + .selectAll('.legend') + .data(color.domain()) + .enter() + .append('g') + .attr('class', 'legend') + .attr('transform', (_datum, index: number): string => { + const height = LEGEND_RECT_SIZE + LEGEND_SPACING; + const offset = (height * color.domain().length) / 2; + const horizontal = 12 * LEGEND_RECT_SIZE; + const vertical = index * height - offset; + return 'translate(' + horizontal + ',' + vertical + ')'; + }); + + legend + .append('rect') + .attr('width', LEGEND_RECT_SIZE) + .attr('height', LEGEND_RECT_SIZE) + .style('fill', color) + .style('stroke', color); + + legend + .data(arcs) + .append('text') + .attr('x', LEGEND_RECT_SIZE + LEGEND_SPACING) + .attr('y', LEGEND_RECT_SIZE - LEGEND_SPACING) + .text((datum: d3.PieArcDatum): string => { + const { label, value } = datum.data; + if (db.getShowData()) { + return `${label} [${value}]`; + } + return label; + }); +}; + +export const renderer = { draw }; diff --git a/packages/mermaid/src/diagrams/pie/styles.js b/packages/mermaid/src/diagrams/pie/pieStyles.ts similarity index 79% rename from packages/mermaid/src/diagrams/pie/styles.js rename to packages/mermaid/src/diagrams/pie/pieStyles.ts index 6f0f600061..39a7f21d54 100644 --- a/packages/mermaid/src/diagrams/pie/styles.js +++ b/packages/mermaid/src/diagrams/pie/pieStyles.ts @@ -1,4 +1,7 @@ -const getStyles = (options) => +import type { DiagramStylesProvider } from '../../diagram-api/types.js'; +import type { PieStyleOptions } from './pieTypes.js'; + +const getStyles: DiagramStylesProvider = (options: PieStyleOptions) => ` .pieCircle{ stroke: ${options.pieStrokeColor}; diff --git a/packages/mermaid/src/diagrams/pie/pieTypes.ts b/packages/mermaid/src/diagrams/pie/pieTypes.ts new file mode 100644 index 0000000000..67fb1dca25 --- /dev/null +++ b/packages/mermaid/src/diagrams/pie/pieTypes.ts @@ -0,0 +1,64 @@ +import type { PieDiagramConfig } from '../../config.type.js'; +import type { DiagramDB, ParseDirectiveDefinition } from '../../diagram-api/types.js'; + +export interface PieFields { + sections: Sections; + showData: boolean; + config: PieDiagramConfig; +} + +export interface PieStyleOptions { + fontFamily: string; + pie1: string; + pie2: string; + pie3: string; + pie4: string; + pie5: string; + pie6: string; + pie7: string; + pie8: string; + pie9: string; + pie10: string; + pie11: string; + pie12: string; + pieTitleTextSize: string; + pieTitleTextColor: string; + pieSectionTextSize: string; + pieSectionTextColor: string; + pieLegendTextSize: string; + pieLegendTextColor: string; + pieStrokeColor: string; + pieStrokeWidth: string; + pieOuterStrokeWidth: string; + pieOuterStrokeColor: string; + pieOpacity: string; +} + +export type Sections = Record; + +export interface D3Sections { + label: string; + value: number; +} + +export interface PieDB extends DiagramDB { + // config + getConfig: () => Required; + + // common db + parseDirective: ParseDirectiveDefinition; + clear: () => void; + setDiagramTitle: (title: string) => void; + getDiagramTitle: () => string; + setAccTitle: (title: string) => void; + getAccTitle: () => string; + setAccDescription: (describetion: string) => void; + getAccDescription: () => string; + + // diagram db + addSection: (label: string, value: number) => void; + getSections: () => Sections; + cleanupValue: (value: string) => number; + setShowData: (toggle: boolean) => void; + getShowData: () => boolean; +} diff --git a/packages/mermaid/src/diagrams/quadrant-chart/parser/quadrant.jison.spec.ts b/packages/mermaid/src/diagrams/quadrant-chart/parser/quadrant.jison.spec.ts index 05b18b9353..faa9281f0d 100644 --- a/packages/mermaid/src/diagrams/quadrant-chart/parser/quadrant.jison.spec.ts +++ b/packages/mermaid/src/diagrams/quadrant-chart/parser/quadrant.jison.spec.ts @@ -1,6 +1,7 @@ -// @ts-ignore: TODO Fix ts errors +// @ts-ignore: JISON doesn't support types import { parser } from './quadrant.jison'; -import { Mock, vi } from 'vitest'; +import type { Mock } from 'vitest'; +import { vi } from 'vitest'; const parserFnConstructor = (str: string) => { return () => { diff --git a/packages/mermaid/src/diagrams/quadrant-chart/quadrantBuilder.ts b/packages/mermaid/src/diagrams/quadrant-chart/quadrantBuilder.ts index 8168551ad4..9c11627620 100644 --- a/packages/mermaid/src/diagrams/quadrant-chart/quadrantBuilder.ts +++ b/packages/mermaid/src/diagrams/quadrant-chart/quadrantBuilder.ts @@ -1,7 +1,7 @@ // @ts-ignore: TODO Fix ts errors import { scaleLinear } from 'd3'; import { log } from '../../logger.js'; -import { QuadrantChartConfig } from '../../config.type.js'; +import type { BaseDiagramConfig, QuadrantChartConfig } from '../../config.type.js'; import defaultConfig from '../../defaultConfig.js'; import { getThemeVariables } from '../../themes/theme-default.js'; @@ -71,7 +71,8 @@ export interface quadrantBuilderData { points: QuadrantPointInputType[]; } -export interface QuadrantBuilderConfig extends QuadrantChartConfig { +export interface QuadrantBuilderConfig + extends Required> { showXAxis: boolean; showYAxis: boolean; showTitle: boolean; diff --git a/packages/mermaid/src/diagrams/quadrant-chart/quadrantDb.ts b/packages/mermaid/src/diagrams/quadrant-chart/quadrantDb.ts index c0c0f4c8ac..0dc87a2d48 100644 --- a/packages/mermaid/src/diagrams/quadrant-chart/quadrantDb.ts +++ b/packages/mermaid/src/diagrams/quadrant-chart/quadrantDb.ts @@ -10,7 +10,7 @@ import { getAccDescription, setAccDescription, clear as commonClear, -} from '../../commonDb.js'; +} from '../common/commonDb.js'; import { QuadrantBuilder } from './quadrantBuilder.js'; const config = configApi.getConfig(); diff --git a/packages/mermaid/src/diagrams/quadrant-chart/quadrantDiagram.ts b/packages/mermaid/src/diagrams/quadrant-chart/quadrantDiagram.ts index 40ae798d2d..a9e822d0ef 100644 --- a/packages/mermaid/src/diagrams/quadrant-chart/quadrantDiagram.ts +++ b/packages/mermaid/src/diagrams/quadrant-chart/quadrantDiagram.ts @@ -1,5 +1,5 @@ -import { DiagramDefinition } from '../../diagram-api/types.js'; -// @ts-ignore: TODO Fix ts errors +import type { DiagramDefinition } from '../../diagram-api/types.js'; +// @ts-ignore: JISON doesn't support types import parser from './parser/quadrant.jison'; import db from './quadrantDb.js'; import renderer from './quadrantRenderer.js'; diff --git a/packages/mermaid/src/diagrams/quadrant-chart/quadrantRenderer.ts b/packages/mermaid/src/diagrams/quadrant-chart/quadrantRenderer.ts index 92943337a0..9dd309b533 100644 --- a/packages/mermaid/src/diagrams/quadrant-chart/quadrantRenderer.ts +++ b/packages/mermaid/src/diagrams/quadrant-chart/quadrantRenderer.ts @@ -3,8 +3,8 @@ import { select } from 'd3'; import * as configApi from '../../config.js'; import { log } from '../../logger.js'; import { configureSvgSize } from '../../setupGraphViewbox.js'; -import { Diagram } from '../../Diagram.js'; -import { +import type { Diagram } from '../../Diagram.js'; +import type { QuadrantBuildType, QuadrantLineType, QuadrantPointType, diff --git a/packages/mermaid/src/diagrams/requirement/requirementDb.js b/packages/mermaid/src/diagrams/requirement/requirementDb.js index 1d0a3e2e12..c56c4ca162 100644 --- a/packages/mermaid/src/diagrams/requirement/requirementDb.js +++ b/packages/mermaid/src/diagrams/requirement/requirementDb.js @@ -8,7 +8,7 @@ import { getAccDescription, setAccDescription, clear as commonClear, -} from '../../commonDb.js'; +} from '../common/commonDb.js'; let relations = []; let latestRequirement = {}; diff --git a/packages/mermaid/src/diagrams/requirement/requirementDiagram.ts b/packages/mermaid/src/diagrams/requirement/requirementDiagram.ts index 4505afc568..619f5b0528 100644 --- a/packages/mermaid/src/diagrams/requirement/requirementDiagram.ts +++ b/packages/mermaid/src/diagrams/requirement/requirementDiagram.ts @@ -1,5 +1,5 @@ -import { DiagramDefinition } from '../../diagram-api/types.js'; -// @ts-ignore: TODO Fix ts errors +import type { DiagramDefinition } from '../../diagram-api/types.js'; +// @ts-ignore: JISON doesn't support types import parser from './parser/requirementDiagram.jison'; import db from './requirementDb.js'; import styles from './styles.js'; diff --git a/packages/mermaid/src/diagrams/requirement/requirementRenderer.js b/packages/mermaid/src/diagrams/requirement/requirementRenderer.js index b88f5c2033..49b7828651 100644 --- a/packages/mermaid/src/diagrams/requirement/requirementRenderer.js +++ b/packages/mermaid/src/diagrams/requirement/requirementRenderer.js @@ -306,8 +306,6 @@ const elementString = (str) => { export const draw = (text, id, _version, diagObj) => { conf = getConfig().requirement; - diagObj.db.clear(); - diagObj.parser.parse(text); const securityLevel = conf.securityLevel; // Handle root and Document for when rendering in sandbox mode diff --git a/packages/mermaid/src/diagrams/sankey/parser/energy.csv b/packages/mermaid/src/diagrams/sankey/parser/energy.csv new file mode 100644 index 0000000000..d9a8fac9ae --- /dev/null +++ b/packages/mermaid/src/diagrams/sankey/parser/energy.csv @@ -0,0 +1,99 @@ +%% There are leading and trailing spaces, do not crop + Agricultural 'waste',Bio-conversion,124.729 +%% line with a comment + +%% Normal line +Bio-conversion,Liquid,0.597 + +%% Line with unquoted sankey keyword +sankey,target,10 + +%% Quoted sankey keyword +"sankey",target,10 + +%% Another normal line +Bio-conversion,Losses,26.862 + +%% Line with integer amount +Bio-conversion,Solid,280 + +%% Some blank lines in the middle of CSV + + +%% Another normal line +Bio-conversion,Gas,81.144 + +%% Quoted line +"Biofuel imports",Liquid,35 + +%% Quoted line with escaped quotes inside +"""Biomass imports""",Solid,35 + +%% Lines containing commas inside +%% Quoted and unquoted values should be equal in terms of graph +"District heating","Heating and cooling, commercial",22.505 +District heating,"Heating and cooling, homes",46.184 + +%% A bunch of lines, normal CSV +Coal imports,Coal,11.606 +Coal reserves,Coal,63.965 +Coal,Solid,75.571 +District heating,Industry,10.639 +Electricity grid,Over generation / exports,104.453 +Electricity grid,Heating and cooling - homes,113.726 +Electricity grid,H2 conversion,27.14 +Electricity grid,Industry,342.165 +Electricity grid,Road transport,37.797 +Electricity grid,Agriculture,4.412 +Electricity grid,Heating and cooling - commercial,40.858 +Electricity grid,Losses,56.691 +Electricity grid,Rail transport,7.863 +Electricity grid,Lighting & appliances - commercial,90.008 +Electricity grid,Lighting & appliances - homes,93.494 +Gas imports,Ngas,40.719 +Gas reserves,Ngas,82.233 +Gas,Heating and cooling - commercial,0.129 +Gas,Losses,1.401 +Gas,Thermal generation,151.891 +Gas,Agriculture,2.096 +Gas,Industry,48.58 +Geothermal,Electricity grid,7.013 +H2 conversion,H2,20.897 +H2 conversion,Losses,6.242 +H2,Road transport,20.897 +Hydro,Electricity grid,6.995 +Liquid,Industry,121.066 +Liquid,International shipping,128.69 +Liquid,Road transport,135.835 +Liquid,Domestic aviation,14.458 +Liquid,International aviation,206.267 +Liquid,Agriculture,3.64 +Liquid,National navigation,33.218 +Liquid,Rail transport,4.413 +Marine algae,Bio-conversion,4.375 +Ngas,Gas,122.952 +Nuclear,Thermal generation,839.978 +Oil imports,Oil,504.287 +Oil reserves,Oil,107.703 +Oil,Liquid,611.99 +Other waste,Solid,56.587 +Other waste,Bio-conversion,77.81 +Pumped heat,Heating and cooling - homes,193.026 +Pumped heat,Heating and cooling - commercial,70.672 +Solar PV,Electricity grid,59.901 +Solar Thermal,Heating and cooling - homes,19.263 +Solar,Solar Thermal,19.263 +Solar,Solar PV,59.901 +Solid,Agriculture,0.882 +Solid,Thermal generation,400.12 +Solid,Industry,46.477 +Thermal generation,Electricity grid,525.531 +Thermal generation,Losses,787.129 +Thermal generation,District heating,79.329 +Tidal,Electricity grid,9.452 +UK land based bioenergy,Bio-conversion,182.01 +"""Wave""",Electricity grid,19.013 +"""Wind""",Electricity grid,289.366 + +%% lines at the end, do not remove + diff --git a/packages/mermaid/src/diagrams/sankey/parser/sankey.jison b/packages/mermaid/src/diagrams/sankey/parser/sankey.jison new file mode 100644 index 0000000000..b11f8a87b0 --- /dev/null +++ b/packages/mermaid/src/diagrams/sankey/parser/sankey.jison @@ -0,0 +1,69 @@ +/** mermaid */ + +//--------------------------------------------------------- +// We support csv format as defined here: +// https://www.ietf.org/rfc/rfc4180.txt +// There are some minor changes for compliance with jison +// We also parse only 3 columns: source,target,value +// And allow blank lines for visual purposes +//--------------------------------------------------------- + +%lex + +%options case-insensitive +%options easy_keword_rules + +%x escaped_text +%x csv + +// as per section 6.1 of RFC 2234 [2] +COMMA \u002C +CR \u000D +LF \u000A +CRLF \u000D\u000A +ESCAPED_QUOTE \u0022 +DQUOTE \u0022 +TEXTDATA [\u0020-\u0021\u0023-\u002B\u002D-\u007E] + +%% + +"sankey-beta" { this.pushState('csv'); return 'SANKEY'; } +<> { return 'EOF' } // match end of file +({CRLF}|{LF}) { return 'NEWLINE' } +{COMMA} { return 'COMMA' } +{DQUOTE} { this.pushState('escaped_text'); return 'DQUOTE'; } +{TEXTDATA}* { return 'NON_ESCAPED_TEXT' } +{DQUOTE}(?!{DQUOTE}) {this.popState('escaped_text'); return 'DQUOTE'; } // unescaped DQUOTE closes string +({TEXTDATA}|{COMMA}|{CR}|{LF}|{DQUOTE}{DQUOTE})* { return 'ESCAPED_TEXT'; } + +/lex + +%start start + +%% // language grammar + +start: SANKEY NEWLINE csv opt_eof; + +csv: record csv_tail; +csv_tail: NEWLINE csv | ; +opt_eof: EOF | ; + +record + : field\[source] COMMA field\[target] COMMA field\[value] { + const source = yy.findOrCreateNode($source.trim().replaceAll('""', '"')); + const target = yy.findOrCreateNode($target.trim().replaceAll('""', '"')); + const value = parseFloat($value.trim()); + yy.addLink(source,target,value); + } // parse only 3 fields, this is not part of CSV standard + ; + +field + : escaped { $$=$escaped; } + | non_escaped { $$=$non_escaped; } + ; + +escaped: DQUOTE ESCAPED_TEXT DQUOTE { $$=$ESCAPED_TEXT; }; + +non_escaped: NON_ESCAPED_TEXT { $$=$NON_ESCAPED_TEXT; }; + + diff --git a/packages/mermaid/src/diagrams/sankey/parser/sankey.spec.ts b/packages/mermaid/src/diagrams/sankey/parser/sankey.spec.ts new file mode 100644 index 0000000000..4517ca01fc --- /dev/null +++ b/packages/mermaid/src/diagrams/sankey/parser/sankey.spec.ts @@ -0,0 +1,24 @@ +// @ts-ignore: jison doesn't export types +import sankey from './sankey.jison'; +import db from '../sankeyDB.js'; +import { cleanupComments } from '../../../diagram-api/comments.js'; +import { prepareTextForParsing } from '../sankeyUtils.js'; +import * as fs from 'fs'; +import * as path from 'path'; + +describe('Sankey diagram', function () { + describe('when parsing an info graph it', function () { + beforeEach(function () { + sankey.parser.yy = db; + sankey.parser.yy.clear(); + }); + + it('parses csv', async () => { + const csv = path.resolve(__dirname, './energy.csv'); + const data = fs.readFileSync(csv, 'utf8'); + const graphDefinition = prepareTextForParsing(cleanupComments('sankey-beta\n\n ' + data)); + + sankey.parser.parse(graphDefinition); + }); + }); +}); diff --git a/packages/mermaid/src/diagrams/sankey/sankeyDB.ts b/packages/mermaid/src/diagrams/sankey/sankeyDB.ts new file mode 100644 index 0000000000..8b3a22c5a0 --- /dev/null +++ b/packages/mermaid/src/diagrams/sankey/sankeyDB.ts @@ -0,0 +1,81 @@ +import * as configApi from '../../config.js'; +import common from '../common/common.js'; +import { + setAccTitle, + getAccTitle, + getAccDescription, + setAccDescription, + setDiagramTitle, + getDiagramTitle, + clear as commonClear, +} from '../common/commonDb.js'; + +// Sankey diagram represented by nodes and links between those nodes +let links: SankeyLink[] = []; +// Array of nodes guarantees their order +let nodes: SankeyNode[] = []; +// We also have to track nodes uniqueness (by ID) +let nodesMap: Record = {}; + +const clear = (): void => { + links = []; + nodes = []; + nodesMap = {}; + commonClear(); +}; + +class SankeyLink { + constructor(public source: SankeyNode, public target: SankeyNode, public value: number = 0) {} +} + +/** + * @param source - Node where the link starts + * @param target - Node where the link ends + * @param value - Describes the amount to be passed + */ +const addLink = (source: SankeyNode, target: SankeyNode, value: number): void => { + links.push(new SankeyLink(source, target, value)); +}; + +class SankeyNode { + constructor(public ID: string) {} +} + +const findOrCreateNode = (ID: string): SankeyNode => { + ID = common.sanitizeText(ID, configApi.getConfig()); + + if (!nodesMap[ID]) { + nodesMap[ID] = new SankeyNode(ID); + nodes.push(nodesMap[ID]); + } + return nodesMap[ID]; +}; + +const getNodes = () => nodes; +const getLinks = () => links; + +const getGraph = () => ({ + nodes: nodes.map((node) => ({ id: node.ID })), + links: links.map((link) => ({ + source: link.source.ID, + target: link.target.ID, + value: link.value, + })), +}); + +export default { + nodesMap, + getConfig: () => configApi.getConfig().sankey, + getNodes, + getLinks, + getGraph, + addLink, + findOrCreateNode, + getAccTitle, + setAccTitle, + getAccDescription, + setAccDescription, + getDiagramTitle, + setDiagramTitle, + clear, +}; diff --git a/packages/mermaid/src/diagrams/sankey/sankeyDetector.ts b/packages/mermaid/src/diagrams/sankey/sankeyDetector.ts new file mode 100644 index 0000000000..73c4d14289 --- /dev/null +++ b/packages/mermaid/src/diagrams/sankey/sankeyDetector.ts @@ -0,0 +1,20 @@ +import type { DiagramDetector, ExternalDiagramDefinition } from '../../diagram-api/types.js'; + +const id = 'sankey'; + +const detector: DiagramDetector = (txt) => { + return /^\s*sankey-beta/.test(txt); +}; + +const loader = async () => { + const { diagram } = await import('./sankeyDiagram.js'); + return { id, diagram }; +}; + +const plugin: ExternalDiagramDefinition = { + id, + detector, + loader, +}; + +export default plugin; diff --git a/packages/mermaid/src/diagrams/sankey/sankeyDiagram.ts b/packages/mermaid/src/diagrams/sankey/sankeyDiagram.ts new file mode 100644 index 0000000000..6fed435ac4 --- /dev/null +++ b/packages/mermaid/src/diagrams/sankey/sankeyDiagram.ts @@ -0,0 +1,15 @@ +import type { DiagramDefinition } from '../../diagram-api/types.js'; +// @ts-ignore: jison doesn't export types +import parser from './parser/sankey.jison'; +import db from './sankeyDB.js'; +import renderer from './sankeyRenderer.js'; +import { prepareTextForParsing } from './sankeyUtils.js'; + +const originalParse = parser.parse.bind(parser); +parser.parse = (text: string) => originalParse(prepareTextForParsing(text)); + +export const diagram: DiagramDefinition = { + parser, + db, + renderer, +}; diff --git a/packages/mermaid/src/diagrams/sankey/sankeyRenderer.ts b/packages/mermaid/src/diagrams/sankey/sankeyRenderer.ts new file mode 100644 index 0000000000..9f5b3c1720 --- /dev/null +++ b/packages/mermaid/src/diagrams/sankey/sankeyRenderer.ts @@ -0,0 +1,215 @@ +import type { Diagram } from '../../Diagram.js'; +import * as configApi from '../../config.js'; + +import { + select as d3select, + scaleOrdinal as d3scaleOrdinal, + schemeTableau10 as d3schemeTableau10, +} from 'd3'; + +import type { SankeyNode as d3SankeyNode } from 'd3-sankey'; +import { + sankey as d3Sankey, + sankeyLinkHorizontal as d3SankeyLinkHorizontal, + sankeyLeft as d3SankeyLeft, + sankeyRight as d3SankeyRight, + sankeyCenter as d3SankeyCenter, + sankeyJustify as d3SankeyJustify, +} from 'd3-sankey'; +import { configureSvgSize } from '../../setupGraphViewbox.js'; +import { Uid } from '../../rendering-util/uid.js'; +import type { SankeyNodeAlignment } from '../../config.type.js'; + +// Map config options to alignment functions +const alignmentsMap: Record< + SankeyNodeAlignment, + (node: d3SankeyNode, n: number) => number +> = { + left: d3SankeyLeft, + right: d3SankeyRight, + center: d3SankeyCenter, + justify: d3SankeyJustify, +}; + +/** + * Draws Sankey diagram. + * + * @param text - The text of the diagram + * @param id - The id of the diagram which will be used as a DOM element id¨ + * @param _version - Mermaid version from package.json + * @param diagObj - A standard diagram containing the db and the text and type etc of the diagram + */ +export const draw = function (text: string, id: string, _version: string, diagObj: Diagram): void { + // Get Sankey config + const { securityLevel, sankey: conf } = configApi.getConfig(); + const defaultSankeyConfig = configApi!.defaultConfig!.sankey!; + + // TODO: + // This code repeats for every diagram + // Figure out what is happening there, probably it should be separated + // The main thing is svg object that is a d3 wrapper for svg operations + // + let sandboxElement: any; + if (securityLevel === 'sandbox') { + sandboxElement = d3select('#i' + id); + } + const root = + securityLevel === 'sandbox' + ? d3select(sandboxElement.nodes()[0].contentDocument.body) + : d3select('body'); + // @ts-ignore TODO root.select is not callable + const svg = securityLevel === 'sandbox' ? root.select(`[id="${id}"]`) : d3select(`[id="${id}"]`); + + // Establish svg dimensions and get width and height + // + const width = conf?.width ?? defaultSankeyConfig.width!; + const height = conf?.height ?? defaultSankeyConfig.width!; + const useMaxWidth = conf?.useMaxWidth ?? defaultSankeyConfig.useMaxWidth!; + const nodeAlignment = conf?.nodeAlignment ?? defaultSankeyConfig.nodeAlignment!; + const prefix = conf?.prefix ?? defaultSankeyConfig.prefix!; + const suffix = conf?.suffix ?? defaultSankeyConfig.suffix!; + const showValues = conf?.showValues ?? defaultSankeyConfig.showValues!; + + // FIX: using max width prevents height from being set, is it intended? + // to add height directly one can use `svg.attr('height', height)` + // + // @ts-ignore TODO: svg type vs selection mismatch + configureSvgSize(svg, height, width, useMaxWidth); + + // Prepare data for construction based on diagObj.db + // This must be a mutable object with `nodes` and `links` properties: + // + // { + // "nodes": [ { "id": "Alice" }, { "id": "Bob" }, { "id": "Carol" } ], + // "links": [ { "source": "Alice", "target": "Bob", "value": 23 }, { "source": "Bob", "target": "Carol", "value": 43 } ] + // } + // + // @ts-ignore TODO: db should be coerced to sankey DB type + const graph = diagObj.db.getGraph(); + + // Get alignment function + const nodeAlign = alignmentsMap[nodeAlignment]; + + // Construct and configure a Sankey generator + // That will be a function that calculates nodes and links dimensions + // + const nodeWidth = 10; + const sankey = d3Sankey() + .nodeId((d: any) => d.id) // we use 'id' property to identify node + .nodeWidth(nodeWidth) + .nodePadding(10 + (showValues ? 15 : 0)) + .nodeAlign(nodeAlign) + .extent([ + [0, 0], + [width, height], + ]); + + // Compute the Sankey layout: calculate nodes and links positions + // Our `graph` object will be mutated by this and enriched with other properties + // + sankey(graph); + + // Get color scheme for the graph + const colorScheme = d3scaleOrdinal(d3schemeTableau10); + + // Create rectangles for nodes + svg + .append('g') + .attr('class', 'nodes') + .selectAll('.node') + .data(graph.nodes) + .join('g') + .attr('class', 'node') + .attr('id', (d: any) => (d.uid = Uid.next('node-')).id) + .attr('transform', function (d: any) { + return 'translate(' + d.x0 + ',' + d.y0 + ')'; + }) + .attr('x', (d: any) => d.x0) + .attr('y', (d: any) => d.y0) + .append('rect') + .attr('height', (d: any) => { + return d.y1 - d.y0; + }) + .attr('width', (d: any) => d.x1 - d.x0) + .attr('fill', (d: any) => colorScheme(d.id)); + + const getText = ({ id, value }: { id: string; value: number }) => { + if (!showValues) { + return id; + } + return `${id}\n${prefix}${Math.round(value * 100) / 100}${suffix}`; + }; + + // Create labels for nodes + svg + .append('g') + .attr('class', 'node-labels') + .attr('font-family', 'sans-serif') + .attr('font-size', 14) + .selectAll('text') + .data(graph.nodes) + .join('text') + .attr('x', (d: any) => (d.x0 < width / 2 ? d.x1 + 6 : d.x0 - 6)) + .attr('y', (d: any) => (d.y1 + d.y0) / 2) + .attr('dy', `${showValues ? '0' : '0.35'}em`) + .attr('text-anchor', (d: any) => (d.x0 < width / 2 ? 'start' : 'end')) + .text(getText); + + // Creates the paths that represent the links. + const link = svg + .append('g') + .attr('class', 'links') + .attr('fill', 'none') + .attr('stroke-opacity', 0.5) + .selectAll('.link') + .data(graph.links) + .join('g') + .attr('class', 'link') + .style('mix-blend-mode', 'multiply'); + + const linkColor = conf?.linkColor || 'gradient'; + + if (linkColor === 'gradient') { + const gradient = link + .append('linearGradient') + .attr('id', (d: any) => (d.uid = Uid.next('linearGradient-')).id) + .attr('gradientUnits', 'userSpaceOnUse') + .attr('x1', (d: any) => d.source.x1) + .attr('x2', (d: any) => d.target.x0); + + gradient + .append('stop') + .attr('offset', '0%') + .attr('stop-color', (d: any) => colorScheme(d.source.id)); + + gradient + .append('stop') + .attr('offset', '100%') + .attr('stop-color', (d: any) => colorScheme(d.target.id)); + } + + let coloring: any; + switch (linkColor) { + case 'gradient': + coloring = (d: any) => d.uid; + break; + case 'source': + coloring = (d: any) => colorScheme(d.source.id); + break; + case 'target': + coloring = (d: any) => colorScheme(d.target.id); + break; + default: + coloring = linkColor; + } + + link + .append('path') + .attr('d', d3SankeyLinkHorizontal()) + .attr('stroke', coloring) + .attr('stroke-width', (d: any) => Math.max(1, d.width)); +}; + +export default { + draw, +}; diff --git a/packages/mermaid/src/diagrams/sankey/sankeyUtils.ts b/packages/mermaid/src/diagrams/sankey/sankeyUtils.ts new file mode 100644 index 0000000000..45ecf21dda --- /dev/null +++ b/packages/mermaid/src/diagrams/sankey/sankeyUtils.ts @@ -0,0 +1,8 @@ +export const prepareTextForParsing = (text: string): string => { + const textToParse = text + .replaceAll(/^[^\S\n\r]+|[^\S\n\r]+$/g, '') // remove all trailing spaces for each row + .replaceAll(/([\n\r])+/g, '\n') // remove empty lines duplicated + .trim(); + + return textToParse; +}; diff --git a/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison b/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison index 074cd5975e..4e971d989d 100644 --- a/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison +++ b/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison @@ -38,6 +38,8 @@ "box" { this.begin('LINE'); return 'box'; } "participant" { this.begin('ID'); return 'participant'; } "actor" { this.begin('ID'); return 'participant_actor'; } +"create" return 'create'; +"destroy" { this.begin('ID'); return 'destroy'; } [^\->:\n,;]+?([\-]*[^\->:\n,;]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; } "as" { this.popState(); this.popState(); this.begin('LINE'); return 'AS'; } (?:) { this.popState(); this.popState(); return 'NEWLINE'; } @@ -138,6 +140,7 @@ directive statement : participant_statement + | 'create' participant_statement {$2.type='createParticipant'; $$=$2;} | 'box' restOfLine box_section end { $3.unshift({type: 'boxStart', boxData:yy.parseBoxData($2) }); @@ -234,10 +237,11 @@ else_sections ; 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;} + : 'participant' actor 'AS' restOfLine 'NEWLINE' {$2.draw='participant'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;} + | 'participant' actor 'NEWLINE' {$2.draw='participant'; $2.type='addParticipant';$$=$2;} + | 'participant_actor' actor 'AS' restOfLine 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;} + | 'participant_actor' actor 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant'; $$=$2;} + | 'destroy' actor 'NEWLINE' {$2.type='destroyParticipant'; $$=$2;} ; note_statement @@ -297,7 +301,7 @@ placement signal : actor signaltype '+' actor text2 - { $$ = [$1,$4,{type: 'addMessage', from:$1.actor, to:$4.actor, signalType:$2, msg:$5}, + { $$ = [$1,$4,{type: 'addMessage', from:$1.actor, to:$4.actor, signalType:$2, msg:$5, activate: true}, {type: 'activeStart', signalType: yy.LINETYPE.ACTIVE_START, actor: $4} ]} | actor signaltype '-' actor text2 diff --git a/packages/mermaid/src/diagrams/sequence/sequenceDb.js b/packages/mermaid/src/diagrams/sequence/sequenceDb.js index 89869b64ff..813d9e127a 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceDb.js +++ b/packages/mermaid/src/diagrams/sequence/sequenceDb.js @@ -10,16 +10,20 @@ import { getAccDescription, setAccDescription, clear as commonClear, -} from '../../commonDb.js'; +} from '../common/commonDb.js'; let prevActor = undefined; let actors = {}; +let createdActors = {}; +let destroyedActors = {}; let boxes = []; let messages = []; const notes = []; let sequenceNumbersEnabled = false; let wrapEnabled; let currentBox = undefined; +let lastCreated = undefined; +let lastDestroyed = undefined; export const parseDirective = function (statement, context, type) { mermaidAPI.parseDirective(this, statement, context, type); @@ -120,7 +124,8 @@ export const addSignal = function ( idFrom, idTo, message = { text: undefined, wrap: undefined }, - messageType + messageType, + activate = false ) { if (messageType === LINETYPE.ACTIVE_END) { const cnt = activationCount(idFrom.actor); @@ -143,6 +148,7 @@ export const addSignal = function ( message: message.text, wrap: (message.wrap === undefined && autoWrap()) || !!message.wrap, type: messageType, + activate, }); return true; }; @@ -165,6 +171,12 @@ export const getBoxes = function () { export const getActors = function () { return actors; }; +export const getCreatedActors = function () { + return createdActors; +}; +export const getDestroyedActors = function () { + return destroyedActors; +}; export const getActor = function (id) { return actors[id]; }; @@ -194,6 +206,8 @@ export const autoWrap = () => { export const clear = function () { actors = {}; + createdActors = {}; + destroyedActors = {}; boxes = []; messages = []; sequenceNumbersEnabled = false; @@ -438,6 +452,19 @@ export const getActorProperty = function (actor, key) { return undefined; }; +/** + * @typedef {object} AddMessageParams A message from one actor to another. + * @property {string} from - The id of the actor sending the message. + * @property {string} to - The id of the actor receiving the message. + * @property {string} msg - The message text. + * @property {number} signalType - The type of signal. + * @property {"addMessage"} type - Set to `"addMessage"` if this is an `AddMessageParams`. + * @property {boolean} [activate] - If `true`, this signal starts an activation. + */ + +/** + * @param {object | object[] | AddMessageParams} param - Object of parameters. + */ export const apply = function (param) { if (Array.isArray(param)) { param.forEach(function (item) { @@ -459,10 +486,21 @@ export const apply = function (param) { }); break; case 'addParticipant': - addActor(param.actor, param.actor, param.description, 'participant'); + addActor(param.actor, param.actor, param.description, param.draw); + break; + case 'createParticipant': + if (actors[param.actor]) { + throw new Error( + "It is not possible to have actors with the same id, even if one is destroyed before the next is created. Use 'AS' aliases to simulate the behavior" + ); + } + lastCreated = param.actor; + addActor(param.actor, param.actor, param.description, param.draw); + createdActors[param.actor] = messages.length; break; - case 'addActor': - addActor(param.actor, param.actor, param.description, 'actor'); + case 'destroyParticipant': + lastDestroyed = param.actor; + destroyedActors[param.actor] = messages.length; break; case 'activeStart': addSignal(param.actor, undefined, undefined, param.signalType); @@ -486,7 +524,28 @@ export const apply = function (param) { addDetails(param.actor, param.text); break; case 'addMessage': - addSignal(param.from, param.to, param.msg, param.signalType); + if (lastCreated) { + if (param.to !== lastCreated) { + throw new Error( + 'The created participant ' + + lastCreated + + ' does not have an associated creating message after its declaration. Please check the sequence diagram.' + ); + } else { + lastCreated = undefined; + } + } else if (lastDestroyed) { + if (param.to !== lastDestroyed && param.from !== lastDestroyed) { + throw new Error( + 'The destroyed participant ' + + lastDestroyed + + ' does not have an associated destroying message after its declaration. Please check the sequence diagram.' + ); + } else { + lastDestroyed = undefined; + } + } + addSignal(param.from, param.to, param.msg, param.signalType, param.activate); break; case 'boxStart': addBox(param.boxData); @@ -566,6 +625,8 @@ export default { showSequenceNumbers, getMessages, getActors, + getCreatedActors, + getDestroyedActors, getActor, getActorKeys, getActorProperty, diff --git a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js index baa5d2fcf5..ed6f07300c 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js +++ b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js @@ -104,6 +104,7 @@ describe('more than one sequence diagram', () => { expect(diagram1.db.getMessages()).toMatchInlineSnapshot(` [ { + "activate": false, "from": "Alice", "message": "Hello Bob, how are you?", "to": "Bob", @@ -111,6 +112,7 @@ describe('more than one sequence diagram', () => { "wrap": false, }, { + "activate": false, "from": "Bob", "message": "I am good thanks!", "to": "Alice", @@ -127,6 +129,7 @@ describe('more than one sequence diagram', () => { expect(diagram2.db.getMessages()).toMatchInlineSnapshot(` [ { + "activate": false, "from": "Alice", "message": "Hello Bob, how are you?", "to": "Bob", @@ -134,6 +137,7 @@ describe('more than one sequence diagram', () => { "wrap": false, }, { + "activate": false, "from": "Bob", "message": "I am good thanks!", "to": "Alice", @@ -152,6 +156,7 @@ describe('more than one sequence diagram', () => { expect(diagram3.db.getMessages()).toMatchInlineSnapshot(` [ { + "activate": false, "from": "Alice", "message": "Hello John, how are you?", "to": "John", @@ -159,6 +164,7 @@ describe('more than one sequence diagram', () => { "wrap": false, }, { + "activate": false, "from": "John", "message": "I am good thanks!", "to": "Alice", @@ -172,14 +178,11 @@ describe('more than one sequence diagram', () => { describe('when parsing a sequenceDiagram', function () { beforeEach(function () { - // diagram.db = sequenceDb; - // diagram.db.clear(); diagram = new Diagram(` sequenceDiagram Alice->Bob:Hello Bob, how are you? Note right of Bob: Bob thinks Bob-->Alice: I am good thanks!`); - diagram.db.clear(); }); it('should handle a sequenceDiagram definition', async function () { const str = ` @@ -551,6 +554,7 @@ deactivate Bob`; expect(messages.length).toBe(4); expect(messages[0].type).toBe(diagram.db.LINETYPE.DOTTED); + expect(messages[0].activate).toBeTruthy(); expect(messages[1].type).toBe(diagram.db.LINETYPE.ACTIVE_START); expect(messages[1].from.actor).toBe('Bob'); expect(messages[2].type).toBe(diagram.db.LINETYPE.DOTTED); @@ -1404,6 +1408,62 @@ link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com expect(boxes[0].actorKeys).toEqual(['a', 'b']); expect(boxes[0].fill).toEqual('Aqua'); }); + + it('should handle simple actor creation', async () => { + const str = ` + sequenceDiagram + participant a as Alice + a ->>b: Hello Bob? + create participant c + b-->>c: Hello c! + c ->> b: Hello b? + create actor d as Donald + a ->> d: Hello Donald? + `; + await mermaidAPI.parse(str); + const actors = diagram.db.getActors(); + const createdActors = diagram.db.getCreatedActors(); + expect(actors['c'].name).toEqual('c'); + expect(actors['c'].description).toEqual('c'); + expect(actors['c'].type).toEqual('participant'); + expect(createdActors['c']).toEqual(1); + expect(actors['d'].name).toEqual('d'); + expect(actors['d'].description).toEqual('Donald'); + expect(actors['d'].type).toEqual('actor'); + expect(createdActors['d']).toEqual(3); + }); + it('should handle simple actor destruction', async () => { + const str = ` + sequenceDiagram + participant a as Alice + a ->>b: Hello Bob? + destroy a + b-->>a: Hello Alice! + b ->> c: Where is Alice? + destroy c + b ->> c: Where are you? + `; + await mermaidAPI.parse(str); + const destroyedActors = diagram.db.getDestroyedActors(); + expect(destroyedActors['a']).toEqual(1); + expect(destroyedActors['c']).toEqual(3); + }); + it('should handle the creation and destruction of the same actor', async () => { + const str = ` + sequenceDiagram + a ->>b: Hello Bob? + create participant c + b ->>c: Hello c! + c ->> b: Hello b? + destroy c + b ->> c : Bye c ! + `; + await mermaidAPI.parse(str); + const createdActors = diagram.db.getCreatedActors(); + const destroyedActors = diagram.db.getDestroyedActors(); + expect(createdActors['c']).toEqual(1); + expect(destroyedActors['c']).toEqual(3); + }); }); describe('when checking the bounds in a sequenceDiagram', function () { beforeAll(() => { @@ -1426,8 +1486,6 @@ describe('when checking the bounds in a sequenceDiagram', function () { let conf; beforeEach(function () { mermaidAPI.reset(); - // diagram.db = sequenceDb; - // diagram.db.clear(); diagram.renderer.bounds.init(); conf = diagram.db.getConfig(); }); @@ -1579,7 +1637,6 @@ sequenceDiagram Alice->Bob:Hello Bob, how are you? Note right of Bob: Bob thinks Bob-->Alice: I am good thanks!`); - diagram.db.clear(); }); ['tspan', 'fo', 'old', undefined].forEach(function (textPlacement) { it(` @@ -1953,8 +2010,6 @@ describe('when rendering a sequenceDiagram with actor mirror activated', () => { let conf; beforeEach(function () { mermaidAPI.reset(); - // diagram.db = sequenceDb; - diagram.db.clear(); conf = diagram.db.getConfig(); diagram.renderer.bounds.init(); }); @@ -1973,7 +2028,9 @@ participant Alice`; expect(bounds.startx).toBe(0); expect(bounds.starty).toBe(0); expect(bounds.stopx).toBe(conf.width); - expect(bounds.stopy).toBe(models.lastActor().y + models.lastActor().height + conf.boxMargin); + expect(bounds.stopy).toBe( + models.lastActor().stopy + models.lastActor().height + conf.boxMargin + ); }); }); }); @@ -1994,12 +2051,8 @@ describe('when rendering a sequenceDiagram with directives', () => { mermaidAPI.initialize({ sequence: conf }); }); - let conf; beforeEach(function () { mermaidAPI.reset(); - // diagram.db = sequenceDb; - diagram.db.clear(); - conf = diagram.db.getConfig(); diagram.renderer.bounds.init(); }); @@ -2011,10 +2064,7 @@ sequenceDiagram participant Alice `; diagram = new Diagram(str); - diagram.renderer.bounds.init(); - await mermaidAPI.parse(str); - diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); @@ -2025,7 +2075,7 @@ participant Alice expect(bounds.startx).toBe(0); expect(bounds.starty).toBe(0); expect(bounds.stopy).toBe( - models.lastActor().y + models.lastActor().height + mermaid.sequence.boxMargin + models.lastActor().stopy + models.lastActor().height + mermaid.sequence.boxMargin ); }); it('should handle one actor, when logLevel is 3 (dfg0)', async () => { @@ -2035,7 +2085,7 @@ sequenceDiagram participant Alice `; - diagram.parse(str); + diagram = new Diagram(str); diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); @@ -2045,7 +2095,7 @@ participant Alice expect(bounds.startx).toBe(0); expect(bounds.starty).toBe(0); expect(bounds.stopy).toBe( - models.lastActor().y + models.lastActor().height + mermaid.sequence.boxMargin + models.lastActor().stopy + models.lastActor().height + mermaid.sequence.boxMargin ); }); it('should hide sequence numbers when autonumber is removed when autonumber is enabled', async () => { @@ -2056,7 +2106,7 @@ Alice->Bob:Hello Bob, how are you? Note right of Bob: Bob thinks Bob-->Alice: I am good thanks!`; - await mermaidAPI.parse(str1); + diagram = new Diagram(str1); diagram.renderer.draw(str1, 'tst', '1.2.3', diagram); // needs to be rendered for the correct value of visibility auto numbers expect(diagram.db.showSequenceNumbers()).toBe(true); @@ -2066,7 +2116,7 @@ Alice->Bob:Hello Bob, how are you? Note right of Bob: Bob thinks Bob-->Alice: I am good thanks!`; - await mermaidAPI.parse(str2); + diagram = new Diagram(str2); diagram.renderer.draw(str2, 'tst', '1.2.3', diagram); expect(diagram.db.showSequenceNumbers()).toBe(false); }); diff --git a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.ts b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.ts index 382d47b619..8779b9cc43 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.ts +++ b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.ts @@ -1,5 +1,5 @@ -import { DiagramDefinition } from '../../diagram-api/types.js'; -// @ts-ignore: TODO Fix ts errors +import type { DiagramDefinition } from '../../diagram-api/types.js'; +// @ts-ignore: JISON doesn't support types import parser from './parser/sequenceDiagram.jison'; import db from './sequenceDb.js'; import styles from './styles.js'; diff --git a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts index 16a3d21fc5..a596a3a029 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts +++ b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts @@ -1,14 +1,14 @@ // @ts-nocheck TODO: fix file -import { select, selectAll } from 'd3'; -import svgDraw, { drawText, fixLifeLineHeights } from './svgDraw.js'; +import { select } from 'd3'; +import svgDraw, { ACTOR_TYPE_WIDTH, drawText, fixLifeLineHeights } from './svgDraw.js'; import { log } from '../../logger.js'; import common from '../common/common.js'; -import * as svgDrawCommon from '../common/svgDrawCommon'; +import * as svgDrawCommon from '../common/svgDrawCommon.js'; import * as configApi from '../../config.js'; import assignWithDepth from '../../assignWithDepth.js'; import utils from '../../utils.js'; import { configureSvgSize } from '../../setupGraphViewbox.js'; -import { Diagram } from '../../Diagram.js'; +import type { Diagram } from '../../Diagram.js'; let conf = {}; @@ -478,29 +478,19 @@ const drawMessage = function (diagram, msgModel, lineStartY: number, diagObj: Di } }; -export const drawActors = function ( +const addActorRenderingData = function ( diagram, actors, + createdActors, actorKeys, verticalPos, - configuration, messages, isFooter ) { - if (configuration.hideUnusedParticipants === true) { - const newActors = new Set(); - messages.forEach((message) => { - newActors.add(message.from); - newActors.add(message.to); - }); - actorKeys = actorKeys.filter((actorKey) => newActors.has(actorKey)); - } - - // Draw the actors let prevWidth = 0; let prevMargin = 0; - let maxHeight = 0; let prevBox = undefined; + let maxHeight = 0; for (const actorKey of actorKeys) { const actor = actors[actorKey]; @@ -528,12 +518,16 @@ export const drawActors = function ( actor.height = common.getMax(actor.height || conf.height, conf.height); actor.margin = actor.margin || conf.actorMargin; + maxHeight = common.getMax(maxHeight, actor.height); + + // if the actor is created by a message, widen margin + if (createdActors[actor.name]) { + prevMargin += actor.width / 2; + } + actor.x = prevWidth + prevMargin; - actor.y = bounds.getVerticalPos(); + actor.starty = bounds.getVerticalPos(); - // Draw the box with the attached line - const height = svgDraw.drawActor(diagram, actor, conf, isFooter); - maxHeight = common.getMax(maxHeight, height); bounds.insert(actor.x, verticalPos, actor.x + actor.width, actor.height); prevWidth += actor.width + prevMargin; @@ -554,6 +548,28 @@ export const drawActors = function ( bounds.bumpVerticalPos(maxHeight); }; +export const drawActors = function (diagram, actors, actorKeys, isFooter) { + if (!isFooter) { + for (const actorKey of actorKeys) { + const actor = actors[actorKey]; + // Draw the box with the attached line + svgDraw.drawActor(diagram, actor, conf, false); + } + } else { + let maxHeight = 0; + bounds.bumpVerticalPos(conf.boxMargin * 2); + for (const actorKey of actorKeys) { + const actor = actors[actorKey]; + if (!actor.stopy) { + actor.stopy = bounds.getVerticalPos(); + } + const height = svgDraw.drawActor(diagram, actor, conf, true); + maxHeight = common.getMax(maxHeight, height); + } + bounds.bumpVerticalPos(maxHeight + conf.boxMargin); + } +}; + export const drawActorsPopup = function (diagram, actors, actorKeys, doc) { let maxHeight = 0; let maxWidth = 0; @@ -606,10 +622,10 @@ const activationBounds = function (actor, actors) { const left = activations.reduce(function (acc, activation) { return common.getMin(acc, activation.startx); - }, actorObj.x + actorObj.width / 2); + }, actorObj.x + actorObj.width / 2 - 1); const right = activations.reduce(function (acc, activation) { return common.getMax(acc, activation.stopx); - }, actorObj.x + actorObj.width / 2); + }, actorObj.x + actorObj.width / 2 + 1); return [left, right]; }; @@ -633,6 +649,95 @@ function adjustLoopHeightForWrap(loopWidths, msg, preMargin, postMargin, addLoop bounds.bumpVerticalPos(heightAdjust); } +/** + * Adjust the msgModel and the actor for the rendering in case the latter is created or destroyed by the msg + * @param msg - the potentially creating or destroying message + * @param msgModel - the model associated with the message + * @param lineStartY - the y position of the message line + * @param index - the index of the current actor under consideration + * @param actors - the array of all actors + * @param createdActors - the array of actors created in the diagram + * @param destroyedActors - the array of actors destroyed in the diagram + */ +function adjustCreatedDestroyedData( + msg, + msgModel, + lineStartY, + index, + actors, + createdActors, + destroyedActors +) { + function receiverAdjustment(actor, adjustment) { + if (actor.x < actors[msg.from].x) { + bounds.insert( + msgModel.stopx - adjustment, + msgModel.starty, + msgModel.startx, + msgModel.stopy + actor.height / 2 + conf.noteMargin + ); + msgModel.stopx = msgModel.stopx + adjustment; + } else { + bounds.insert( + msgModel.startx, + msgModel.starty, + msgModel.stopx + adjustment, + msgModel.stopy + actor.height / 2 + conf.noteMargin + ); + msgModel.stopx = msgModel.stopx - adjustment; + } + } + + function senderAdjustment(actor, adjustment) { + if (actor.x < actors[msg.to].x) { + bounds.insert( + msgModel.startx - adjustment, + msgModel.starty, + msgModel.stopx, + msgModel.stopy + actor.height / 2 + conf.noteMargin + ); + msgModel.startx = msgModel.startx + adjustment; + } else { + bounds.insert( + msgModel.stopx, + msgModel.starty, + msgModel.startx + adjustment, + msgModel.stopy + actor.height / 2 + conf.noteMargin + ); + msgModel.startx = msgModel.startx - adjustment; + } + } + + // if it is a create message + if (createdActors[msg.to] == index) { + const actor = actors[msg.to]; + const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 + 3 : actor.width / 2 + 3; + receiverAdjustment(actor, adjustment); + actor.starty = lineStartY - actor.height / 2; + bounds.bumpVerticalPos(actor.height / 2); + } + // if it is a destroy sender message + else if (destroyedActors[msg.from] == index) { + const actor = actors[msg.from]; + if (conf.mirrorActors) { + const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 : actor.width / 2; + senderAdjustment(actor, adjustment); + } + actor.stopy = lineStartY - actor.height / 2; + bounds.bumpVerticalPos(actor.height / 2); + } + // if it is a destroy receiver message + else if (destroyedActors[msg.to] == index) { + const actor = actors[msg.to]; + if (conf.mirrorActors) { + const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 + 3 : actor.width / 2 + 3; + receiverAdjustment(actor, adjustment); + } + actor.stopy = lineStartY - actor.height / 2; + bounds.bumpVerticalPos(actor.height / 2); + } +} + /** * Draws a sequenceDiagram in the tag with id: id based on the graph definition in text. * @@ -644,9 +749,6 @@ function adjustLoopHeightForWrap(loopWidths, msg, preMargin, postMargin, addLoop export const draw = function (_text: string, id: string, _version: string, diagObj: Diagram) { const { securityLevel, sequence } = configApi.getConfig(); conf = sequence; - diagObj.db.clear(); - // Parse the graph definition - diagObj.parser.parse(_text); // Handle root and Document for when rendering in sandbox mode let sandboxElement; if (securityLevel === 'sandbox') { @@ -666,8 +768,10 @@ export const draw = function (_text: string, id: string, _version: string, diagO // Fetch data from the parsing const actors = diagObj.db.getActors(); + const createdActors = diagObj.db.getCreatedActors(); + const destroyedActors = diagObj.db.getDestroyedActors(); const boxes = diagObj.db.getBoxes(); - const actorKeys = diagObj.db.getActorKeys(); + let actorKeys = diagObj.db.getActorKeys(); const messages = diagObj.db.getMessages(); const title = diagObj.db.getDiagramTitle(); const hasBoxes = diagObj.db.hasAtLeastOneBox(); @@ -686,7 +790,16 @@ export const draw = function (_text: string, id: string, _version: string, diagO } } - drawActors(diagram, actors, actorKeys, 0, conf, messages, false); + if (conf.hideUnusedParticipants === true) { + const newActors = new Set(); + messages.forEach((message) => { + newActors.add(message.from); + newActors.add(message.to); + }); + actorKeys = actorKeys.filter((actorKey) => newActors.has(actorKey)); + } + + addActorRenderingData(diagram, actors, createdActors, actorKeys, 0, messages, false); const loopWidths = calculateLoopBounds(messages, actors, maxMessageWidthPerActor, diagObj); // The arrow head definition is attached to the svg once @@ -720,7 +833,8 @@ export const draw = function (_text: string, id: string, _version: string, diagO let sequenceIndex = 1; let sequenceIndexStep = 1; const messagesToDraw = []; - messages.forEach(function (msg) { + const backgrounds = []; + messages.forEach(function (msg, index) { let loopModel, noteModel, msgModel; switch (msg.type) { @@ -757,7 +871,7 @@ export const draw = function (_text: string, id: string, _version: string, diagO break; case diagObj.db.LINETYPE.RECT_END: loopModel = bounds.endLoop(); - svgDraw.drawBackgroundRect(diagram, loopModel); + backgrounds.push(loopModel); bounds.models.addLoop(loopModel); bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos()); break; @@ -876,13 +990,20 @@ export const draw = function (_text: string, id: string, _version: string, diagO break; default: try { - // lastMsg = msg - bounds.resetVerticalPos(); msgModel = msg.msgModel; msgModel.starty = bounds.getVerticalPos(); msgModel.sequenceIndex = sequenceIndex; msgModel.sequenceVisible = diagObj.db.showSequenceNumbers(); const lineStartY = boundMessage(diagram, msgModel); + adjustCreatedDestroyedData( + msg, + msgModel, + lineStartY, + index, + actors, + createdActors, + destroyedActors + ); messagesToDraw.push({ messageModel: msgModel, lineStartY: lineStartY }); bounds.models.addMessage(msgModel); } catch (e) { @@ -907,15 +1028,16 @@ export const draw = function (_text: string, id: string, _version: string, diagO } }); - messagesToDraw.forEach((e) => drawMessage(diagram, e.messageModel, e.lineStartY, diagObj)); + log.debug('createdActors', createdActors); + log.debug('destroyedActors', destroyedActors); + drawActors(diagram, actors, actorKeys, false); + messagesToDraw.forEach((e) => drawMessage(diagram, e.messageModel, e.lineStartY, diagObj)); if (conf.mirrorActors) { - // Draw actors below diagram - bounds.bumpVerticalPos(conf.boxMargin * 2); - drawActors(diagram, actors, actorKeys, bounds.getVerticalPos(), conf, messages, true); - bounds.bumpVerticalPos(conf.boxMargin); - fixLifeLineHeights(diagram, bounds.getVerticalPos()); + drawActors(diagram, actors, actorKeys, true); } + backgrounds.forEach((e) => svgDraw.drawBackgroundRect(diagram, e)); + fixLifeLineHeights(diagram, actors, actorKeys, conf); bounds.models.boxes.forEach(function (box) { box.height = bounds.getVerticalPos() - box.y; @@ -937,11 +1059,6 @@ export const draw = function (_text: string, id: string, _version: string, diagO const { bounds: box } = bounds.getBounds(); - // Adjust line height of actor lines now that the height of the diagram is known - log.debug('For line height fix Querying: #' + id + ' .actor-line'); - const actorLines = selectAll('#' + id + ' .actor-line'); - actorLines.attr('y2', box.stopy); - // Make sure the height of the diagram supports long menus. let boxHeight = box.stopy - box.starty; if (boxHeight < requiredBoxSize.maxHeight) { @@ -1272,9 +1389,8 @@ const buildNoteModel = function (msg, actors, diagObj) { }; const buildMessageModel = function (msg, actors, diagObj) { - let process = false; if ( - [ + ![ diagObj.db.LINETYPE.SOLID_OPEN, diagObj.db.LINETYPE.DOTTED_OPEN, diagObj.db.LINETYPE.SOLID, @@ -1285,17 +1401,47 @@ const buildMessageModel = function (msg, actors, diagObj) { diagObj.db.LINETYPE.DOTTED_POINT, ].includes(msg.type) ) { - process = true; - } - if (!process) { return {}; } - const fromBounds = activationBounds(msg.from, actors); - const toBounds = activationBounds(msg.to, actors); - const fromIdx = fromBounds[0] <= toBounds[0] ? 1 : 0; - const toIdx = fromBounds[0] < toBounds[0] ? 0 : 1; - const allBounds = [...fromBounds, ...toBounds]; - const boundedWidth = Math.abs(toBounds[toIdx] - fromBounds[fromIdx]); + const [fromLeft, fromRight] = activationBounds(msg.from, actors); + const [toLeft, toRight] = activationBounds(msg.to, actors); + const isArrowToRight = fromLeft <= toLeft; + const startx = isArrowToRight ? fromRight : fromLeft; + let stopx = isArrowToRight ? toLeft : toRight; + + // As the line width is considered, the left and right values will be off by 2. + const isArrowToActivation = Math.abs(toLeft - toRight) > 2; + + /** + * Adjust the value based on the arrow direction + * @param value - The value to adjust + * @returns The adjustment with correct sign to be added to the actual value. + */ + const adjustValue = (value: number) => { + return isArrowToRight ? -value : value; + }; + + /** + * This is an edge case for the first activation. + * Proper fix would require significant changes. + * So, we set an activate flag in the message, and cross check that with isToActivation + * In cases where the message is to an activation that was properly detected, we don't want to move the arrow head + * The activation will not be detected on the first message, so we need to move the arrow head + */ + if (msg.activate && !isArrowToActivation) { + stopx += adjustValue(conf.activationWidth / 2 - 1); + } + + /** + * Shorten the length of arrow at the end and move the marker forward (using refX) to have a clean arrowhead + * This is not required for open arrows that don't have arrowheads + */ + if (![diagObj.db.LINETYPE.SOLID_OPEN, diagObj.db.LINETYPE.DOTTED_OPEN].includes(msg.type)) { + stopx += adjustValue(3); + } + + const allBounds = [fromLeft, fromRight, toLeft, toRight]; + const boundedWidth = Math.abs(startx - stopx); if (msg.wrap && msg.message) { msg.message = utils.wrapLabel( msg.message, @@ -1312,8 +1458,8 @@ const buildMessageModel = function (msg, actors, diagObj) { conf.width ), height: 0, - startx: fromBounds[fromIdx], - stopx: toBounds[toIdx], + startx, + stopx, starty: 0, stopy: 0, message: msg.message, diff --git a/packages/mermaid/src/diagrams/sequence/svgDraw.js b/packages/mermaid/src/diagrams/sequence/svgDraw.js index a3f7514f61..f81147c10c 100644 --- a/packages/mermaid/src/diagrams/sequence/svgDraw.js +++ b/packages/mermaid/src/diagrams/sequence/svgDraw.js @@ -1,9 +1,11 @@ import common from '../common/common.js'; -import * as svgDrawCommon from '../common/svgDrawCommon'; +import * as svgDrawCommon from '../common/svgDrawCommon.js'; import { addFunction } from '../../interactionDb.js'; import { ZERO_WIDTH_SPACE, parseFontSize } from '../../utils.js'; import { sanitizeUrl } from '@braintree/sanitize-url'; +export const ACTOR_TYPE_WIDTH = 18 * 2; + export const drawRect = function (elem, rectData) { return svgDrawCommon.drawRect(elem, rectData); }; @@ -294,14 +296,19 @@ export const drawLabel = function (elem, txtObject) { let actorCnt = -1; -export const fixLifeLineHeights = (diagram, bounds) => { - if (!diagram.selectAll) { +export const fixLifeLineHeights = (diagram, actors, actorKeys, conf) => { + if (!diagram.select) { return; } - diagram - .selectAll('.actor-line') - .attr('class', '200') - .attr('y2', bounds - 55); + actorKeys.forEach((actorKey) => { + const actor = actors[actorKey]; + const actorDOM = diagram.select('#actor' + actor.actorCnt); + if (!conf.mirrorActors && actor.stopy) { + actorDOM.attr('y2', actor.stopy + actor.height / 2); + } else if (conf.mirrorActors) { + actorDOM.attr('y2', actor.stopy); + } + }); }; /** @@ -313,10 +320,11 @@ export const fixLifeLineHeights = (diagram, bounds) => { * @param {boolean} isFooter - If the actor is the footer one */ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) { + const actorY = isFooter ? actor.stopy : actor.starty; const center = actor.x + actor.width / 2; - const centerY = actor.y + 5; + const centerY = actorY + 5; - const boxpluslineGroup = elem.append('g'); + const boxpluslineGroup = elem.append('g').lower(); var g = boxpluslineGroup; if (!isFooter) { @@ -328,6 +336,7 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) { .attr('x2', center) .attr('y2', 2000) .attr('class', 'actor-line') + .attr('class', '200') .attr('stroke-width', '0.5px') .attr('stroke', '#999'); @@ -348,7 +357,7 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) { rect.fill = '#eaeaea'; } rect.x = actor.x; - rect.y = actor.y; + rect.y = actorY; rect.width = actor.width; rect.height = actor.height; rect.class = cssclass; @@ -388,8 +397,11 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) { }; const drawActorTypeActor = function (elem, actor, conf, isFooter) { + const actorY = isFooter ? actor.stopy : actor.starty; const center = actor.x + actor.width / 2; - const centerY = actor.y + 80; + const centerY = actorY + 80; + + elem.lower(); if (!isFooter) { actorCnt++; @@ -401,15 +413,18 @@ const drawActorTypeActor = function (elem, actor, conf, isFooter) { .attr('x2', center) .attr('y2', 2000) .attr('class', 'actor-line') + .attr('class', '200') .attr('stroke-width', '0.5px') .attr('stroke', '#999'); + + actor.actorCnt = actorCnt; } const actElem = elem.append('g'); actElem.attr('class', 'actor-man'); const rect = svgDrawCommon.getNoteRect(); rect.x = actor.x; - rect.y = actor.y; + rect.y = actorY; rect.fill = '#eaeaea'; rect.width = actor.width; rect.height = actor.height; @@ -421,33 +436,33 @@ const drawActorTypeActor = function (elem, actor, conf, isFooter) { .append('line') .attr('id', 'actor-man-torso' + actorCnt) .attr('x1', center) - .attr('y1', actor.y + 25) + .attr('y1', actorY + 25) .attr('x2', center) - .attr('y2', actor.y + 45); + .attr('y2', actorY + 45); actElem .append('line') .attr('id', 'actor-man-arms' + actorCnt) - .attr('x1', center - 18) - .attr('y1', actor.y + 33) - .attr('x2', center + 18) - .attr('y2', actor.y + 33); + .attr('x1', center - ACTOR_TYPE_WIDTH / 2) + .attr('y1', actorY + 33) + .attr('x2', center + ACTOR_TYPE_WIDTH / 2) + .attr('y2', actorY + 33); actElem .append('line') - .attr('x1', center - 18) - .attr('y1', actor.y + 60) + .attr('x1', center - ACTOR_TYPE_WIDTH / 2) + .attr('y1', actorY + 60) .attr('x2', center) - .attr('y2', actor.y + 45); + .attr('y2', actorY + 45); actElem .append('line') .attr('x1', center) - .attr('y1', actor.y + 45) - .attr('x2', center + 16) - .attr('y2', actor.y + 60); + .attr('y1', actorY + 45) + .attr('x2', center + ACTOR_TYPE_WIDTH / 2 - 2) + .attr('y2', actorY + 60); const circle = actElem.append('circle'); circle.attr('cx', actor.x + actor.width / 2); - circle.attr('cy', actor.y + 10); + circle.attr('cy', actorY + 10); circle.attr('r', 15); circle.attr('width', actor.width); circle.attr('height', actor.height); @@ -688,7 +703,7 @@ export const insertArrowHead = function (elem) { .append('defs') .append('marker') .attr('id', 'arrowhead') - .attr('refX', 9) + .attr('refX', 7.9) .attr('refY', 5) .attr('markerUnits', 'userSpaceOnUse') .attr('markerWidth', 12) @@ -708,7 +723,7 @@ export const insertArrowFilledHead = function (elem) { .append('defs') .append('marker') .attr('id', 'filled-head') - .attr('refX', 18) + .attr('refX', 15.5) .attr('refY', 7) .attr('markerWidth', 20) .attr('markerHeight', 28) @@ -753,7 +768,7 @@ export const insertArrowCrossHead = function (elem) { .attr('markerHeight', 8) .attr('orient', 'auto') .attr('refX', 4) - .attr('refY', 5); + .attr('refY', 4.5); // The cross marker .append('path') diff --git a/packages/mermaid/src/diagrams/state/stateDb.js b/packages/mermaid/src/diagrams/state/stateDb.js index d9c789a99b..f71290ec3a 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.js +++ b/packages/mermaid/src/diagrams/state/stateDb.js @@ -11,7 +11,7 @@ import { clear as commonClear, setDiagramTitle, getDiagramTitle, -} from '../../commonDb.js'; +} from '../common/commonDb.js'; import { DEFAULT_DIAGRAM_DIRECTION, diff --git a/packages/mermaid/src/diagrams/state/stateDiagram-v2.ts b/packages/mermaid/src/diagrams/state/stateDiagram-v2.ts index 616a97556a..36fc95edd1 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram-v2.ts +++ b/packages/mermaid/src/diagrams/state/stateDiagram-v2.ts @@ -1,5 +1,5 @@ -import { DiagramDefinition } from '../../diagram-api/types.js'; -// @ts-ignore: TODO Fix ts errors +import type { DiagramDefinition } from '../../diagram-api/types.js'; +// @ts-ignore: JISON doesn't support types import parser from './parser/stateDiagram.jison'; import db from './stateDb.js'; import styles from './styles.js'; diff --git a/packages/mermaid/src/diagrams/state/stateDiagram.ts b/packages/mermaid/src/diagrams/state/stateDiagram.ts index 44552c2461..643e847ce1 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram.ts +++ b/packages/mermaid/src/diagrams/state/stateDiagram.ts @@ -1,5 +1,5 @@ -import { DiagramDefinition } from '../../diagram-api/types.js'; -// @ts-ignore: TODO Fix ts errors +import type { DiagramDefinition } from '../../diagram-api/types.js'; +// @ts-ignore: JISON doesn't support types import parser from './parser/stateDiagram.jison'; import db from './stateDb.js'; import styles from './styles.js'; diff --git a/packages/mermaid/src/diagrams/state/stateRenderer-v2.js b/packages/mermaid/src/diagrams/state/stateRenderer-v2.js index 20ae0d1125..1c9b2d1d3c 100644 --- a/packages/mermaid/src/diagrams/state/stateRenderer-v2.js +++ b/packages/mermaid/src/diagrams/state/stateRenderer-v2.js @@ -84,17 +84,8 @@ export const setConf = function (cnf) { * @returns {object} ClassDef styles (a Map with keys = strings, values = ) */ export const getClasses = function (text, diagramObj) { - log.trace('Extracting classes'); - diagramObj.db.clear(); - try { - // Parse the graph definition - diagramObj.parser.parse(text); - // must run extract() to turn the parsed statements into states, relationships, classes, etc. - diagramObj.db.extract(diagramObj.db.getRootDocV2()); - return diagramObj.db.getClasses(); - } catch (e) { - return e; - } + diagramObj.db.extract(diagramObj.db.getRootDocV2()); + return diagramObj.db.getClasses(); }; /** @@ -358,7 +349,7 @@ const setupDoc = (g, parentParsedItem, doc, diagramStates, diagramDb, altFlag) = * Look through all of the documents (docs) in the parsedItems * Because is a _document_ direction, the default direction is not necessarily the same as the overall default _diagram_ direction. * @param {object[]} parsedItem - the parsed statement item to look through - * @param [defaultDir=DEFAULT_NESTED_DOC_DIR] - the direction to use if none is found + * @param [defaultDir] - the direction to use if none is found * @returns {string} */ const getDir = (parsedItem, defaultDir = DEFAULT_NESTED_DOC_DIR) => { @@ -384,7 +375,6 @@ const getDir = (parsedItem, defaultDir = DEFAULT_NESTED_DOC_DIR) => { */ export const draw = async function (text, id, _version, diag) { log.info('Drawing state diagram (v2)', id); - // diag.sb.clear(); nodeDb = {}; // Fetch the default direction, use TD if none was found let dir = diag.db.getDirection(); diff --git a/packages/mermaid/src/diagrams/state/stateRenderer.js b/packages/mermaid/src/diagrams/state/stateRenderer.js index 74913a748a..1b3e0f27ed 100644 --- a/packages/mermaid/src/diagrams/state/stateRenderer.js +++ b/packages/mermaid/src/diagrams/state/stateRenderer.js @@ -57,28 +57,12 @@ export const draw = function (text, id, _version, diagObj) { : select('body'); const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document; - // diagObj.db.clear(); - // parser.parse(text); log.debug('Rendering diagram ' + text); // Fetch the default direction, use TD if none was found const diagram = root.select(`[id='${id}']`); insertMarkers(diagram); - // Layout graph, Create a new directed graph - const graph = new graphlib.Graph({ - multigraph: true, - compound: true, - // acyclicer: 'greedy', - rankdir: 'RL', - // ranksep: '20' - }); - - // Default to assigning a new object as a label for each new edge. - graph.setDefaultEdgeLabel(function () { - return {}; - }); - const rootDoc = diagObj.db.getRootDoc(); renderDoc(rootDoc, diagram, undefined, false, root, doc, diagObj); diff --git a/packages/mermaid/src/diagrams/timeline/timeline-definition.ts b/packages/mermaid/src/diagrams/timeline/timeline-definition.ts index 7f671291f2..0c55616108 100644 --- a/packages/mermaid/src/diagrams/timeline/timeline-definition.ts +++ b/packages/mermaid/src/diagrams/timeline/timeline-definition.ts @@ -1,4 +1,4 @@ -// @ts-ignore: TODO Fix ts errors +// @ts-ignore: JISON doesn't support types import parser from './parser/timeline.jison'; import * as db from './timelineDb.js'; import renderer from './timelineRenderer.js'; diff --git a/packages/mermaid/src/diagrams/timeline/timeline.spec.js b/packages/mermaid/src/diagrams/timeline/timeline.spec.js index 1f6a960244..a6a94fd0c7 100644 --- a/packages/mermaid/src/diagrams/timeline/timeline.spec.js +++ b/packages/mermaid/src/diagrams/timeline/timeline.spec.js @@ -1,7 +1,6 @@ import { parser as timeline } from './parser/timeline.jison'; import * as timelineDB from './timelineDb.js'; // import { injectUtils } from './mermaidUtils.js'; -import * as _commonDb from '../../commonDb.js'; import { parseDirective as _parseDirective } from '../../directiveUtils.js'; import { @@ -18,7 +17,6 @@ import { // getConfig, // sanitizeText, // setupGraphViewBox, -// _commonDb, // _parseDirective // ); diff --git a/packages/mermaid/src/diagrams/timeline/timelineDb.js b/packages/mermaid/src/diagrams/timeline/timelineDb.js index 337cfe4416..e5e22147d1 100644 --- a/packages/mermaid/src/diagrams/timeline/timelineDb.js +++ b/packages/mermaid/src/diagrams/timeline/timelineDb.js @@ -1,5 +1,5 @@ import { parseDirective as _parseDirective } from '../../directiveUtils.js'; -import * as commonDb from '../../commonDb.js'; +import * as commonDb from '../common/commonDb.js'; let currentSection = ''; let currentTaskId = 0; diff --git a/packages/mermaid/src/diagrams/timeline/timelineRenderer.ts b/packages/mermaid/src/diagrams/timeline/timelineRenderer.ts index 17460bac2d..ee351d905b 100644 --- a/packages/mermaid/src/diagrams/timeline/timelineRenderer.ts +++ b/packages/mermaid/src/diagrams/timeline/timelineRenderer.ts @@ -1,11 +1,12 @@ // @ts-nocheck - don't check until handle it -import { select, Selection } from 'd3'; +import type { Selection } from 'd3'; +import { select } from 'd3'; import svgDraw from './svgDraw.js'; import { log } from '../../logger.js'; import { getConfig } from '../../config.js'; import { setupGraphViewbox } from '../../setupGraphViewbox.js'; -import { Diagram } from '../../Diagram.js'; -import { MermaidConfig } from '../../config.type.js'; +import type { Diagram } from '../../Diagram.js'; +import type { MermaidConfig } from '../../config.type.js'; interface Block { number: number; @@ -30,12 +31,6 @@ export const draw = function (text: string, id: string, version: string, diagObj // @ts-expect-error - wrong config? const LEFT_MARGIN = 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; diff --git a/packages/mermaid/src/diagrams/user-journey/journeyDb.js b/packages/mermaid/src/diagrams/user-journey/journeyDb.js index d4f34e9422..509c5dc148 100644 --- a/packages/mermaid/src/diagrams/user-journey/journeyDb.js +++ b/packages/mermaid/src/diagrams/user-journey/journeyDb.js @@ -8,7 +8,7 @@ import { getAccDescription, setAccDescription, clear as commonClear, -} from '../../commonDb.js'; +} from '../common/commonDb.js'; let currentSection = ''; diff --git a/packages/mermaid/src/diagrams/user-journey/journeyDiagram.ts b/packages/mermaid/src/diagrams/user-journey/journeyDiagram.ts index 969cf0e5e3..c2b6cd7172 100644 --- a/packages/mermaid/src/diagrams/user-journey/journeyDiagram.ts +++ b/packages/mermaid/src/diagrams/user-journey/journeyDiagram.ts @@ -1,5 +1,5 @@ -import { DiagramDefinition } from '../../diagram-api/types.js'; -// @ts-ignore: TODO Fix ts errors +import type { DiagramDefinition } from '../../diagram-api/types.js'; +// @ts-ignore: JISON doesn't support types import parser from './parser/journey.jison'; import db from './journeyDb.js'; import styles from './styles.js'; diff --git a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts index 9ea880f691..28c83f19d7 100644 --- a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts +++ b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts @@ -49,8 +49,6 @@ const conf = getConfig().journey; const LEFT_MARGIN = conf.leftMargin; export const draw = function (text, id, version, diagObj) { const conf = getConfig().journey; - diagObj.db.clear(); - diagObj.parser.parse(text + '\n'); const securityLevel = getConfig().securityLevel; // Handle root and Document for when rendering in sandbox mode diff --git a/packages/mermaid/src/diagrams/user-journey/svgDraw.js b/packages/mermaid/src/diagrams/user-journey/svgDraw.js index 108f4b2f96..7a8f791fac 100644 --- a/packages/mermaid/src/diagrams/user-journey/svgDraw.js +++ b/packages/mermaid/src/diagrams/user-journey/svgDraw.js @@ -1,5 +1,5 @@ import { arc as d3arc } from 'd3'; -import * as svgDrawCommon from '../common/svgDrawCommon'; +import * as svgDrawCommon from '../common/svgDrawCommon.js'; export const drawRect = function (elem, rectData) { return svgDrawCommon.drawRect(elem, rectData); diff --git a/packages/mermaid/src/directiveUtils.ts b/packages/mermaid/src/directiveUtils.ts index 563856631a..baf628e74c 100644 --- a/packages/mermaid/src/directiveUtils.ts +++ b/packages/mermaid/src/directiveUtils.ts @@ -1,7 +1,5 @@ import * as configApi from './config.js'; - import { log } from './logger.js'; -import { directiveSanitizer } from './utils.js'; let currentDirective: { type?: string; args?: any } | undefined = {}; @@ -60,9 +58,6 @@ const handleDirective = function (p: any, directive: any, type: string): void { delete directive.args[prop]; } }); - log.info('sanitize in handleDirective', directive.args); - directiveSanitizer(directive.args); - log.info('sanitize in handleDirective (done)', directive.args); configApi.addDirective(directive.args); break; } diff --git a/packages/mermaid/src/docs/.vitepress/components/HomePage.vue b/packages/mermaid/src/docs/.vitepress/components/HomePage.vue index 19f3912a7e..b6998f2499 100644 --- a/packages/mermaid/src/docs/.vitepress/components/HomePage.vue +++ b/packages/mermaid/src/docs/.vitepress/components/HomePage.vue @@ -16,8 +16,12 @@ import { teamMembers } from '../contributors';


- Join the community and - get involved! + Join the community + and get involved!

diff --git a/packages/mermaid/src/docs/.vitepress/config.ts b/packages/mermaid/src/docs/.vitepress/config.ts index 8fceb810be..ede064fa46 100644 --- a/packages/mermaid/src/docs/.vitepress/config.ts +++ b/packages/mermaid/src/docs/.vitepress/config.ts @@ -35,7 +35,12 @@ export default defineConfig({ themeConfig: { nav: nav(), editLink: { - pattern: 'https://github.com/mermaid-js/mermaid/edit/develop/packages/mermaid/src/docs/:path', + pattern: ({ filePath, frontmatter }) => { + if (typeof frontmatter.editLink === 'string') { + return frontmatter.editLink; + } + return `https://github.com/mermaid-js/mermaid/edit/develop/packages/mermaid/src/docs/${filePath}`; + }, text: 'Edit this page on GitHub', }, sidebar: { @@ -67,6 +72,11 @@ function nav() { activeMatch: '/config/', }, { text: 'Integrations', link: '/ecosystem/integrations', activeMatch: '/ecosystem/' }, + { + text: 'Contributing', + link: '/community/development.html', + activeMatch: '/community/', + }, { text: 'Latest News', link: '/news/announcements', @@ -96,14 +106,11 @@ function sidebarAll() { return [ { text: '📔 Introduction', - collapsible: true, + collapsed: false, items: [ { text: 'About Mermaid', link: '/intro/' }, - { text: 'Deployment', link: '/intro/n00b-gettingStarted' }, - { - text: 'Syntax and Configuration', - link: '/intro/n00b-syntaxReference', - }, + { text: 'Getting Started', link: '/intro/getting-started' }, + { text: 'Syntax and Configuration', link: '/intro/syntax-reference' }, ], }, ...sidebarSyntax(), @@ -118,7 +125,7 @@ function sidebarSyntax() { return [ { text: '📊 Diagram Syntax', - collapsible: true, + collapsed: false, items: [ { text: 'Flowchart', link: '/syntax/flowchart' }, { text: 'Sequence Diagram', link: '/syntax/sequenceDiagram' }, @@ -134,10 +141,11 @@ function sidebarSyntax() { { text: 'Quadrant Chart', link: '/syntax/quadrantChart' }, { text: 'Requirement Diagram', link: '/syntax/requirementDiagram' }, { text: 'Gitgraph (Git) Diagram 🔥', link: '/syntax/gitgraph' }, - { text: 'C4C Diagram (Context) Diagram 🦺⚠️', link: '/syntax/c4c' }, + { text: 'C4 Diagram 🦺⚠️', link: '/syntax/c4' }, { text: 'Mindmaps 🔥', link: '/syntax/mindmap' }, { text: 'Timeline 🔥', link: '/syntax/timeline' }, { text: 'Zenuml 🔥', link: '/syntax/zenuml' }, + { text: 'Sankey 🔥', link: '/syntax/sankey' }, { text: 'Other Examples', link: '/syntax/examples' }, ], }, @@ -148,17 +156,18 @@ function sidebarConfig() { return [ { text: '⚙️ Deployment and Configuration', - collapsible: true, + collapsed: false, items: [ { text: 'Configuration', link: '/config/configuration' }, { text: 'Tutorials', link: '/config/Tutorials' }, { text: 'API-Usage', link: '/config/usage' }, { text: 'Mermaid API Configuration', link: '/config/setup/README' }, + { text: 'Mermaid Configuration Options', link: '/config/schema-docs/config' }, { text: 'Directives', link: '/config/directives' }, { text: 'Theming', link: '/config/theming' }, { text: 'Accessibility', link: '/config/accessibility' }, { text: 'Mermaid CLI', link: '/config/mermaidCLI' }, - { text: 'Advanced usage', link: '/config/n00b-advanced' }, + { text: 'Advanced usage', link: '/config/advanced' }, { text: 'FAQ', link: '/config/faq' }, ], }, @@ -169,7 +178,7 @@ function sidebarEcosystem() { return [ { text: '📚 Ecosystem', - collapsible: true, + collapsed: false, items: [ { text: 'Showcases', link: '/ecosystem/showcases' }, { text: 'Use-Cases and Integrations', link: '/ecosystem/integrations' }, @@ -182,10 +191,12 @@ function sidebarCommunity() { return [ { text: '🙌 Contributions and Community', - collapsible: true, + collapsed: false, items: [ - { text: 'Overview for Beginners', link: '/community/n00b-overview' }, - ...sidebarCommunityDevelopContribute(), + { text: 'Contributing to Mermaid', link: '/community/development' }, + { text: 'Contributing Code', link: '/community/code' }, + { text: 'Contributing Documentation', link: '/community/documentation' }, + { text: 'Questions and Suggestions', link: '/community/questions-and-suggestions' }, { text: 'Adding Diagrams', link: '/community/newDiagram' }, { text: 'Security', link: '/community/security' }, ], @@ -193,45 +204,11 @@ function sidebarCommunity() { ]; } -// Development and Contributing -function sidebarCommunityDevelopContribute() { - const page_path = '/community/development'; - return [ - { - text: 'Contributing to Mermaid', - link: page_path + '#contributing-to-mermaid', - collapsible: true, - items: [ - { - text: 'Technical Requirements and Setup', - link: pathToId(page_path, 'technical-requirements-and-setup'), - }, - { - text: 'Contributing Code', - link: pathToId(page_path, 'contributing-code'), - }, - { - text: 'Contributing Documentation', - link: pathToId(page_path, 'contributing-documentation'), - }, - { - text: 'Questions or Suggestions?', - link: pathToId(page_path, 'questions-or-suggestions'), - }, - { - text: 'Last Words', - link: pathToId(page_path, 'last-words'), - }, - ], - }, - ]; -} - function sidebarNews() { return [ { text: '📰 Latest News', - collapsible: true, + collapsed: false, items: [ { text: 'Announcements', link: '/news/announcements' }, { text: 'Blog', link: '/news/blog' }, diff --git a/packages/mermaid/src/docs/.vitepress/contributors.ts b/packages/mermaid/src/docs/.vitepress/contributors.ts index bef2c1311c..93eeee2e21 100644 --- a/packages/mermaid/src/docs/.vitepress/contributors.ts +++ b/packages/mermaid/src/docs/.vitepress/contributors.ts @@ -1,30 +1,5 @@ import contributorUsernamesJson from './contributor-names.json'; - -export interface Contributor { - name: string; - avatar: string; -} - -export interface SocialEntry { - icon: string | { svg: string }; - link: string; -} - -export interface CoreTeam { - name: string; - // required to download avatars from GitHub - github: string; - avatar?: string; - twitter?: string; - mastodon?: string; - sponsor?: string; - website?: string; - linkedIn?: string; - title?: string; - org?: string; - desc?: string; - links?: SocialEntry[]; -} +import { CoreTeam, knut, plainTeamMembers } from './teamMembers.js'; const contributorUsernames: string[] = contributorUsernamesJson; @@ -38,6 +13,7 @@ const websiteSVG = { const createLinks = (tm: CoreTeam): CoreTeam => { tm.avatar = `/user-avatars/${tm.github}.png`; + tm.title = tm.title ?? 'Developer'; tm.links = [{ icon: 'github', link: `https://github.com/${tm.github}` }]; if (tm.mastodon) { tm.links.push({ icon: 'mastodon', link: tm.mastodon }); @@ -54,91 +30,6 @@ const createLinks = (tm: CoreTeam): CoreTeam => { return tm; }; -const knut: CoreTeam = { - github: 'knsv', - name: 'Knut Sveidqvist', - title: 'Creator', - twitter: 'knutsveidqvist', - sponsor: 'https://github.com/sponsors/knsv', -}; - -const plainTeamMembers: CoreTeam[] = [ - { - github: 'NeilCuzon', - name: 'Neil Cuzon', - title: 'Developer', - }, - { - github: 'tylerlong', - name: 'Tyler Liu', - title: 'Developer', - }, - { - github: 'sidharthv96', - name: 'Sidharth Vinod', - title: 'Developer', - twitter: 'sidv42', - mastodon: 'https://techhub.social/@sidv', - sponsor: 'https://github.com/sponsors/sidharthv96', - linkedIn: 'sidharth-vinod', - website: 'https://sidharth.dev', - }, - { - github: 'ashishjain0512', - name: 'Ashish Jain', - title: 'Developer', - }, - { - github: 'mmorel-35', - name: 'Matthieu Morel', - title: 'Developer', - linkedIn: 'matthieumorel35', - }, - { - github: 'aloisklink', - name: 'Alois Klink', - title: 'Developer', - linkedIn: 'aloisklink', - website: 'https://aloisklink.com', - }, - { - github: 'pbrolin47', - name: 'Per Brolin', - title: 'Developer', - }, - { - github: 'Yash-Singh1', - name: 'Yash Singh', - title: 'Developer', - }, - { - github: 'GDFaber', - name: 'Marc Faber', - title: 'Developer', - linkedIn: 'marc-faber', - }, - { - github: 'MindaugasLaganeckas', - name: 'Mindaugas Laganeckas', - title: 'Developer', - }, - { - github: 'jgreywolf', - name: 'Justin Greywolf', - title: 'Developer', - }, - { - github: 'IOrlandoni', - name: 'Nacho Orlandoni', - title: 'Developer', - }, - { - github: 'huynhicode', - name: 'Steph Huynh', - title: 'Developer', - }, -]; - const teamMembers = plainTeamMembers.map((tm) => createLinks(tm)); teamMembers.sort( (a, b) => contributorUsernames.indexOf(a.github) - contributorUsernames.indexOf(b.github) diff --git a/packages/mermaid/src/docs/.vitepress/mermaid-markdown-all.ts b/packages/mermaid/src/docs/.vitepress/mermaid-markdown-all.ts index 14340462c5..30f044d988 100644 --- a/packages/mermaid/src/docs/.vitepress/mermaid-markdown-all.ts +++ b/packages/mermaid/src/docs/.vitepress/mermaid-markdown-all.ts @@ -54,6 +54,15 @@ const MermaidExample = async (md: MarkdownRenderer) => { return `

NOTE

${token.content}}

`; } + if (token.info.trim() === 'regexp') { + // shiki doesn't yet support regexp code blocks, but the javascript + // one still makes RegExes look good + token.info = 'javascript'; + // use trimEnd to move trailing `\n` outside if the JavaScript regex `/` block + token.content = `/${token.content.trimEnd()}/\n`; + return defaultRenderer(tokens, index, options, env, slf); + } + if (token.info.trim() === 'jison') { return `
diff --git a/packages/mermaid/src/docs/.vitepress/scripts/fetch-avatars.ts b/packages/mermaid/src/docs/.vitepress/scripts/fetch-avatars.ts index bbea31bc1e..cd78be782a 100644 --- a/packages/mermaid/src/docs/.vitepress/scripts/fetch-avatars.ts +++ b/packages/mermaid/src/docs/.vitepress/scripts/fetch-avatars.ts @@ -18,7 +18,11 @@ async function download(url: string, fileName: URL) { const image = await fetch(url); await writeFile(fileName, Buffer.from(await image.arrayBuffer())); } catch (error) { - console.error(error); + console.error('failed to load', url, error); + // Exit the build process if we are in CI + if (process.env.CI) { + throw error; + } } } @@ -26,10 +30,13 @@ async function fetchAvatars() { await mkdir(fileURLToPath(new URL(getAvatarPath('none'))).replace('none.png', ''), { recursive: true, }); + contributors = JSON.parse(await readFile(pathContributors, { encoding: 'utf-8' })); - for (const name of contributors) { - await download(`https://github.com/${name}.png?size=100`, getAvatarPath(name)); - } + let avatars = contributors.map((name) => { + download(`https://github.com/${name}.png?size=100`, getAvatarPath(name)); + }); + + await Promise.allSettled(avatars); } fetchAvatars(); diff --git a/packages/mermaid/src/docs/.vitepress/scripts/fetch-contributors.ts b/packages/mermaid/src/docs/.vitepress/scripts/fetch-contributors.ts index fd5409d0f6..da7621b299 100644 --- a/packages/mermaid/src/docs/.vitepress/scripts/fetch-contributors.ts +++ b/packages/mermaid/src/docs/.vitepress/scripts/fetch-contributors.ts @@ -1,6 +1,8 @@ // Adapted from https://github.dev/vitest-dev/vitest/blob/991ff33ab717caee85ef6cbe1c16dc514186b4cc/scripts/update-contributors.ts#L6 import { writeFile } from 'node:fs/promises'; +import { knut, plainTeamMembers } from '../teamMembers.js'; +import { existsSync } from 'node:fs'; const pathContributors = new URL('../contributor-names.json', import.meta.url); @@ -10,28 +12,40 @@ interface Contributor { async function fetchContributors() { const collaborators: string[] = []; - let page = 1; - let data: Contributor[] = []; - do { - const response = await fetch( - `https://api.github.com/repos/mermaid-js/mermaid/contributors?per_page=100&page=${page}`, - { - method: 'GET', - headers: { - 'content-type': 'application/json', - }, - } - ); - data = await response.json(); - collaborators.push(...data.map((i) => i.login)); - console.log(`Fetched page ${page}`); - page++; - } while (data.length === 100); + try { + let page = 1; + let data: Contributor[] = []; + do { + const response = await fetch( + `https://api.github.com/repos/mermaid-js/mermaid/contributors?per_page=100&page=${page}`, + { + method: 'GET', + headers: { + 'content-type': 'application/json', + }, + } + ); + data = await response.json(); + collaborators.push(...data.map((i) => i.login)); + console.log(`Fetched page ${page}`); + page++; + } while (data.length === 100); + } catch (e) { + /* contributors fetching failure must not hinder docs development */ + } return collaborators.filter((name) => !name.includes('[bot]')); } async function generate() { - const collaborators = await fetchContributors(); + if (existsSync(pathContributors)) { + // Only fetch contributors once, when running locally. + // In CI, the file won't exist, so it'll fetch every time as expected. + return; + } + // Will fetch all contributors only in CI to speed up local development. + const collaborators = process.env.CI + ? await fetchContributors() + : [knut, ...plainTeamMembers].map((m) => m.github); await writeFile(pathContributors, JSON.stringify(collaborators, null, 2) + '\n', 'utf8'); } diff --git a/packages/mermaid/src/docs/.vitepress/teamMembers.ts b/packages/mermaid/src/docs/.vitepress/teamMembers.ts new file mode 100644 index 0000000000..d95f49ed87 --- /dev/null +++ b/packages/mermaid/src/docs/.vitepress/teamMembers.ts @@ -0,0 +1,105 @@ +export interface Contributor { + name: string; + avatar: string; +} + +export interface SocialEntry { + icon: string | { svg: string }; + link: string; +} + +export interface CoreTeam { + name: string; + // required to download avatars from GitHub + github: string; + avatar?: string; + twitter?: string; + mastodon?: string; + sponsor?: string; + website?: string; + linkedIn?: string; + title?: string; + org?: string; + desc?: string; + links?: SocialEntry[]; +} + +export const knut: CoreTeam = { + github: 'knsv', + name: 'Knut Sveidqvist', + title: 'Creator', + twitter: 'knutsveidqvist', + sponsor: 'https://github.com/sponsors/knsv', +}; + +export const plainTeamMembers: CoreTeam[] = [ + { + github: 'NeilCuzon', + name: 'Neil Cuzon', + }, + { + github: 'tylerlong', + name: 'Tyler Liu', + }, + { + github: 'sidharthv96', + name: 'Sidharth Vinod', + twitter: 'sidv42', + mastodon: 'https://techhub.social/@sidv', + sponsor: 'https://github.com/sponsors/sidharthv96', + linkedIn: 'sidharth-vinod', + website: 'https://sidharth.dev', + }, + { + github: 'ashishjain0512', + name: 'Ashish Jain', + }, + { + github: 'mmorel-35', + name: 'Matthieu Morel', + linkedIn: 'matthieumorel35', + }, + { + github: 'aloisklink', + name: 'Alois Klink', + linkedIn: 'aloisklink', + website: 'https://aloisklink.com', + }, + { + github: 'pbrolin47', + name: 'Per Brolin', + }, + { + github: 'Yash-Singh1', + name: 'Yash Singh', + }, + { + github: 'GDFaber', + name: 'Marc Faber', + linkedIn: 'marc-faber', + }, + { + github: 'MindaugasLaganeckas', + name: 'Mindaugas Laganeckas', + }, + { + github: 'jgreywolf', + name: 'Justin Greywolf', + }, + { + github: 'IOrlandoni', + name: 'Nacho Orlandoni', + }, + { + github: 'huynhicode', + name: 'Steph Huynh', + }, + { + github: 'Yokozuna59', + name: 'Reda Al Sulais', + }, + { + github: 'nirname', + name: 'Nikolay Rozhkov', + }, +]; diff --git a/packages/mermaid/src/docs/.vitepress/theme/redirect.spec.ts b/packages/mermaid/src/docs/.vitepress/theme/redirect.spec.ts index 3d88913d1e..09c14d935b 100644 --- a/packages/mermaid/src/docs/.vitepress/theme/redirect.spec.ts +++ b/packages/mermaid/src/docs/.vitepress/theme/redirect.spec.ts @@ -19,8 +19,17 @@ test.each([ 'https://mermaid-js.github.io/mermaid/#/flowchart?another=test&id=my-id&one=more', // with multiple params 'syntax/flowchart.html#my-id', ], - ['https://mermaid-js.github.io/mermaid/#/n00b-advanced', 'config/n00b-advanced.html'], // without .md - ['https://mermaid-js.github.io/mermaid/#/n00b-advanced.md', 'config/n00b-advanced.html'], // with .md + ['https://mermaid-js.github.io/mermaid/#/n00b-advanced', 'config/advanced.html'], // without .md + ['https://mermaid-js.github.io/mermaid/#/n00b-advanced.md', 'config/advanced.html'], // with .md + + ['https://mermaid-js.github.io/mermaid/#/n00b-gettingstarted', 'intro/getting-started.html'], + ['https://mermaid-js.github.io/mermaid/#/n00b-gettingstarted.md', 'intro/getting-started.html'], + ['https://mermaid-js.github.io/mermaid/#/n00b-overview', 'intro/getting-started.html'], + ['https://mermaid-js.github.io/mermaid/#/n00b-overview.md', 'intro/getting-started.html'], + ['https://mermaid-js.github.io/mermaid/#/n00b-syntaxreference', 'intro/syntax-reference.html'], + ['https://mermaid-js.github.io/mermaid/#/n00b-syntaxreference.md', 'intro/syntax-reference.html'], + ['https://mermaid-js.github.io/mermaid/#/quickstart', 'intro/getting-started.html'], + ['https://mermaid-js.github.io/mermaid/#/quickstart.md', 'intro/getting-started.html'], [ 'https://mermaid-js.github.io/mermaid/#/flowchart?id=a-node-in-the-form-of-a-circle', // with id, without .md 'syntax/flowchart.html#a-node-in-the-form-of-a-circle', diff --git a/packages/mermaid/src/docs/.vitepress/theme/redirect.ts b/packages/mermaid/src/docs/.vitepress/theme/redirect.ts index 936d6f7e29..ca8e5bda18 100644 --- a/packages/mermaid/src/docs/.vitepress/theme/redirect.ts +++ b/packages/mermaid/src/docs/.vitepress/theme/redirect.ts @@ -24,11 +24,18 @@ const getBaseFile = (url: URL): Redirect => { return { path, id }; }; +/** + * Used to redirect old (pre-vitepress) documentation pages to corresponding new pages. + * The key is the old documentation ID, and the value is the new documentation path. + * No key should be added here as it already has all the old documentation IDs. + * If you are changing a documentation page, you should update the corresponding value here, and add an entry in the urlRedirectMap below. + */ const idRedirectMap: Record = { + // ID of the old documentation page: Path of the new documentation page '8.6.0_docs': '', accessibility: 'config/theming', breakingchanges: '', - c4c: 'syntax/c4c', + c4c: 'syntax/c4', classdiagram: 'syntax/classDiagram', configuration: 'config/configuration', demos: 'ecosystem/integrations', @@ -47,14 +54,14 @@ const idRedirectMap: Record = { mermaidcli: 'config/mermaidCLI', mindmap: 'syntax/mindmap', 'more-pages': '', - 'n00b-advanced': 'config/n00b-advanced', - 'n00b-gettingstarted': 'intro/n00b-gettingStarted', - 'n00b-overview': 'community/n00b-overview', - 'n00b-syntaxreference': '', + 'n00b-advanced': 'config/advanced', + 'n00b-gettingstarted': 'intro/getting-started', + 'n00b-overview': 'intro/getting-started', + 'n00b-syntaxreference': 'intro/syntax-reference', newdiagram: 'community/newDiagram', pie: 'syntax/pie', plugins: '', - quickstart: 'intro/n00b-gettingStarted', + quickstart: 'intro/getting-started', requirementdiagram: 'syntax/requirementDiagram', security: 'community/security', sequencediagram: 'syntax/sequenceDiagram', @@ -68,8 +75,21 @@ const idRedirectMap: Record = { 'user-journey': 'syntax/userJourney', }; +/** + * Used to redirect pages that have been moved in the vitepress site. + * No keys should be deleted from here. + * If you are changing a documentation page, you should update the corresponding value here, + * and update the entry in the idRedirectMap above if it was present + * (No need to add new keys in idRedirectMap). + */ const urlRedirectMap: Record = { + // Old URL: New URL '/misc/faq.html': 'configure/faq.html', + '/syntax/c4c.html': 'syntax/c4.html', + '/config/n00b-advanced.html': 'config/advanced', + '/intro/n00b-gettingStarted.html': 'intro/getting-started', + '/intro/n00b-syntaxReference.html': 'intro/syntax-reference', + '/community/n00b-overview.html': 'intro/getting-started', }; /** diff --git a/packages/mermaid/src/docs/community/code.md b/packages/mermaid/src/docs/community/code.md new file mode 100644 index 0000000000..93a5fcd953 --- /dev/null +++ b/packages/mermaid/src/docs/community/code.md @@ -0,0 +1,165 @@ +# Contributing Code + +The basic steps for contributing code are: + +```mermaid +graph LR + git[1. Checkout a git branch] --> codeTest[2. Write tests and code] --> doc[3. Update documentation] --> submit[4. Submit a PR] --> review[5. Review and merge] +``` + +1. **Create** and checkout a git branch and work on your code in the branch +2. Write and update **tests** (unit and perhaps even integration (e2e) tests) (If you do TDD/BDD, the order might be different.) +3. **Let users know** that things have changed or been added in the documents! This is often overlooked, but _critical_ +4. **Submit** your code as a _pull request_. +5. Maintainers will **review** your code. If there are no changes necessary, the PR will be merged. Otherwise, make the requested changes and repeat. + +## 1. Checkout a git branch + +Mermaid uses a [Git Flow](https://guides.github.com/introduction/flow/)–inspired approach to branching. + +Development is done in the `develop` branch. + +Once development is done we create a `release/vX.X.X` branch from `develop` for testing. + +Once the release happens we add a tag to the `release` branch and merge it with `master`. The live product and on-line documentation are what is in the `master` branch. + +**All new work should be based on the `develop` branch.** + +**When you are ready to do work, always, ALWAYS:** + +1. Make sure you have the most up-to-date version of the `develop` branch. (fetch or pull to update it) +2. Check out the `develop` branch +3. Create a new branch for your work. Please name the branch following our naming convention below. + +We use the following naming convention for branches: + +```txt + [feature | bug | chore | docs]/[issue number]_[short description using dashes ('-') or underscores ('_') instead of spaces] +``` + +You can always check current [configuration of labelling and branch prefixes](https://github.com/mermaid-js/mermaid/blob/develop/.github/pr-labeler.yml) + +- The first part is the **type** of change: a feature, bug, chore, or documentation change ('docs') +- followed by a _slash_ (which helps to group like types together in many git tools) +- followed by the **issue number** +- followed by an _underscore_ ('\_') +- followed by a short text description (but use dashes ('-') or underscores ('\_') instead of spaces) + +If your work is specific to a single diagram type, it is a good idea to put the diagram type at the start of the description. This will help us keep release notes organized: it will help us keep changes for a diagram type together. + +**Ex: A new feature described in issue 2945 that adds a new arrow type called 'florbs' to state diagrams** + +`feature/2945_state-diagram-new-arrow-florbs` + +**Ex: A bug described in issue 1123 that causes random ugly red text in multiple diagram types** +`bug/1123_fix_random_ugly_red_text` + +## 2. Write Tests + +Tests ensure that each function, module, or part of code does what it says it will do. This is critically +important when other changes are made to ensure that existing code is not broken (no regression). + +Just as important, the tests act as _specifications:_ they specify what the code does (or should do). +Whenever someone is new to a section of code, they should be able to read the tests to get a thorough understanding of what it does and why. + +If you are fixing a bug, you should add tests to ensure that your code has actually fixed the bug, to specify/describe what the code is doing, and to ensure the bug doesn't happen again. +(If there had been a test for the situation, the bug never would have happened in the first place.) +You may need to change existing tests if they were inaccurate. + +If you are adding a feature, you will definitely need to add tests. Depending on the size of your feature, you may need to add integration tests. + +### Unit Tests + +Unit tests are tests that test a single function or module. They are the easiest to write and the fastest to run. + +Unit tests are mandatory all code except the renderers. (The renderers are tested with integration tests.) + +We use [Vitest](https://vitest.dev) to run unit tests. + +You can use the following command to run the unit tests: + +```sh +pnpm test +``` + +When writing new tests, it's easier to have the tests automatically run as you make changes. You can do this by running the following command: + +```sh +pnpm test:watch +``` + +### Integration/End-to-End (e2e) tests + +These test the rendering and visual appearance of the diagrams. +This ensures that the rendering of that feature in the e2e will be reviewed in the release process going forward. Less chance that it breaks! + +To start working with the e2e tests: + +1. Run `pnpm dev` to start the dev server +2. Start **Cypress** by running `pnpm cypress:open`. + +The rendering tests are very straightforward to create. There is a function `imgSnapshotTest`, which takes a diagram in text form and the mermaid options, and it renders that diagram in Cypress. + +When running in CI it will take a snapshot of the rendered diagram and compare it with the snapshot from last build and flag it for review if it differs. + +This is what a rendering test looks like: + +```js +it('should render forks and joins', () => { + imgSnapshotTest( + ` + stateDiagram + state fork_state <<fork>> + [*] --> fork_state + fork_state --> State2 + fork_state --> State3 + + state join_state <<join>> + State2 --> join_state + State3 --> join_state + join_state --> State4 + State4 --> [*] + `, + { logLevel: 0 } + ); + cy.get('svg'); +}); +``` + +**_[TODO - running the tests against what is expected in development. ]_** + +**_[TODO - how to generate new screenshots]_** +.... + +## 3. Update Documentation + +If the users have no way to know that things have changed, then you haven't really _fixed_ anything for the users; you've just added to making Mermaid feel broken. +Likewise, if users don't know that there is a new feature that you've implemented, it will forever remain unknown and unused. + +The documentation has to be updated to users know that things have changed and added! +If you are adding a new feature, add `(v+)` in the title or description. It will be replaced automatically with the current version number when the release happens. + +eg: `# Feature Name (v+)` + +We know it can sometimes be hard to code _and_ write user documentation. + +Our documentation is managed in `packages/mermaid/src/docs`. Details on how to edit is in the [Contributing Documentation](#contributing-documentation) section. + +Create another issue specifically for the documentation. +You will need to help with the PR, but definitely ask for help if you feel stuck. +When it feels hard to write stuff out, explaining it to someone and having that person ask you clarifying questions can often be 80% of the work! + +When in doubt, write up and submit what you can. It can be clarified and refined later. (With documentation, something is better than nothing!) + +## 4. Submit your pull request + +**[TODO - PR titles should start with (fix | feat | ....)]** + +We make all changes via Pull Requests (PRs). As we have many Pull Requests from developers new to Mermaid, we have put in place a process wherein _knsv, Knut Sveidqvist_ is in charge of the final release process and the active maintainers are in charge of reviewing and merging most PRs. + +- PRs will be reviewed by active maintainers, who will provide feedback and request changes as needed. +- The maintainers will request a review from knsv, if necessary. +- Once the PR is approved, the maintainers will merge the PR into the `develop` branch. +- When a release is ready, the `release/x.x.x` branch will be created, extensively tested and knsv will be in charge of the release process. + +**Reminder: Pull Requests should be submitted to the develop branch.** diff --git a/packages/mermaid/src/docs/community/development.md b/packages/mermaid/src/docs/community/development.md index a5aa39539d..fc11a1f6c0 100644 --- a/packages/mermaid/src/docs/community/development.md +++ b/packages/mermaid/src/docs/community/development.md @@ -1,14 +1,8 @@ # Contributing to Mermaid -## Contents - -- [Technical Requirements and Setup](#technical-requirements-and-setup) -- [Contributing Code](#contributing-code) -- [Contributing Documentation](#contributing-documentation) -- [Questions or Suggestions?](#questions-or-suggestions) -- [Last Words](#last-words) - ---- +> The following documentation describes how to work with Mermaid in your host environment. +> There's also a [Docker installation guide](../community/docker-development.md) +> if you prefer to work in a Docker environment. So you want to help? That's great! @@ -16,340 +10,76 @@ So you want to help? That's great! Here are a few things to get you started on the right path. -## Technical Requirements and Setup +## Get the Source Code -### Technical Requirements +In GitHub, you first **fork** a repository when you are going to make changes and submit pull requests. -These are the tools we use for working with the code and documentation. - -- [volta](https://volta.sh/) to manage node versions. -- [Node.js](https://nodejs.org/en/). `volta install node` -- [pnpm](https://pnpm.io/) package manager. `volta install pnpm` -- [npx](https://docs.npmjs.com/cli/v8/commands/npx) the packaged executor in npm. This is needed [to install pnpm.](#2-install-pnpm) +Then you **clone** a copy to your local development machine (e.g. where you code) to make a copy with all the files to work with. -Follow [the setup steps below](#setup) to install them and verify they are working +[Fork mermaid](https://github.com/mermaid-js/mermaid/fork) to start contributing to the main project and its documentation, or [search for other repositories](https://github.com/orgs/mermaid-js/repositories). -### Setup +[Here is a GitHub document that gives an overview of the process.](https://docs.github.com/en/get-started/quickstart/fork-a-repo) -Follow these steps to set up the environment you need to work on code and/or documentation. +## Technical Requirements -#### 1. Fork and clone the repository +> The following documentation describes how to work with Mermaid in your host environment. +> There's also a [Docker installation guide](../community/docker-development.md) +> if you prefer to work in a Docker environment. -In GitHub, you first _fork_ a repository when you are going to make changes and submit pull requests. +These are the tools we use for working with the code and documentation: -Then you _clone_ a copy to your local development machine (e.g. where you code) to make a copy with all the files to work with. +- [volta](https://volta.sh/) to manage node versions. +- [Node.js](https://nodejs.org/en/). `volta install node` +- [pnpm](https://pnpm.io/) package manager. `volta install pnpm` +- [npx](https://docs.npmjs.com/cli/v8/commands/npx) the packaged executor in npm. This is needed [to install pnpm.](#install-packages) -[Here is a GitHub document that gives an overview of the process.](https://docs.github.com/en/get-started/quickstart/fork-a-repo) +Follow the setup steps below to install them and start the development. -#### 2. Install pnpm +## Setup and Launch -Once you have cloned the repository onto your development machine, change into the `mermaid` project folder so that you can install `pnpm`. You will need `npx` to install pnpm because volta doesn't support it yet. +### Switch to project -Ex: +Once you have cloned the repository onto your development machine, change into the `mermaid` project folder (the top level directory of the mermaid project repository) ```bash -# Change into the mermaid directory (the top level director of the mermaid project repository) cd mermaid -# npx is required for first install because volta does not support pnpm yet -npx pnpm install ``` -#### 3. Verify Everything Is Working +### Install packages -Once you have installed pnpm, you can run the `test` script to verify that pnpm is working _and_ that the repository has been cloned correctly: +Run `npx pnpm install`. You will need `npx` for this because volta doesn't support it yet. ```bash -pnpm test +npx pnpm install # npx is required for first install ``` -The `test` script and others are in the top-level `package.json` file. - -All tests should run successfully without any errors or failures. (You might see _lint_ or _formatting_ warnings; those are ok during this step.) - -### Docker - -If you are using docker and docker-compose, you have self-documented `run` bash script, which is a convenient alias for docker-compose commands: +### Launch ```bash -./run install # npx pnpm install -./run test # pnpm test -``` - -## Contributing Code - -The basic steps for contributing code are: - -```mermaid -graph LR - git[1. Checkout a git branch] --> codeTest[2. Write tests and code] --> doc[3. Update documentation] --> submit[4. Submit a PR] --> review[5. Review and merge] -``` - -1. **Create** and checkout a git branch and work on your code in the branch -2. Write and update **tests** (unit and perhaps even integration (e2e) tests) (If you do TDD/BDD, the order might be different.) -3. **Let users know** that things have changed or been added in the documents! This is often overlooked, but _critical_ -4. **Submit** your code as a _pull request_. -5. Maintainers will **review** your code. If there are no changes necessary, the PR will be merged. Otherwise, make the requested changes and repeat. - -### 1. Checkout a git branch - -Mermaid uses a [Git Flow](https://guides.github.com/introduction/flow/)–inspired approach to branching. - -Development is done in the `develop` branch. - -Once development is done we create a `release/vX.X.X` branch from `develop` for testing. - -Once the release happens we add a tag to the `release` branch and merge it with `master`. The live product and on-line documentation are what is in the `master` branch. - -**All new work should be based on the `develop` branch.** - -**When you are ready to do work, always, ALWAYS:** - -1. Make sure you have the most up-to-date version of the `develop` branch. (fetch or pull to update it) -2. Check out the `develop` branch -3. Create a new branch for your work. Please name the branch following our naming convention below. - -We use the follow naming convention for branches: - -```text - [feature | bug | chore | docs]/[issue number]_[short description using dashes ('-') or underscores ('_') instead of spaces] +npx pnpm run dev ``` -- The first part is the **type** of change: a feature, bug, chore, or documentation change ('docs') -- followed by a _slash_ (which helps to group like types together in many git tools) -- followed by the **issue number** -- followed by an _underscore_ ('\_') -- followed by a short text description (but use dashes ('-') or underscores ('\_') instead of spaces) - -If your work is specific to a single diagram type, it is a good idea to put the diagram type at the start of the description. This will help us keep release notes organized: it will help us keep changes for a diagram type together. - -**Ex: A new feature described in issue 2945 that adds a new arrow type called 'florbs' to state diagrams** - -`feature/2945_state-diagram-new-arrow-florbs` - -**Ex: A bug described in issue 1123 that causes random ugly red text in multiple diagram types** -`bug/1123_fix_random_ugly_red_text` - -### 2. Write Tests +Now you are ready to make your changes! Edit whichever files in `src` as required. -Tests ensure that each function, module, or part of code does what it says it will do. This is critically -important when other changes are made to ensure that existing code is not broken (no regression). +Open in your browser, after starting the dev server. +There is a list of demos that can be used to see and test your changes. -Just as important, the tests act as _specifications:_ they specify what the code does (or should do). -Whenever someone is new to a section of code, they should be able to read the tests to get a thorough understanding of what it does and why. +If you need a specific diagram, you can duplicate the `example.html` file in `/demos/dev` and add your own mermaid code to your copy. -If you are fixing a bug, you should add tests to ensure that your code has actually fixed the bug, to specify/describe what the code is doing, and to ensure the bug doesn't happen again. -(If there had been a test for the situation, the bug never would have happened in the first place.) -You may need to change existing tests if they were inaccurate. +That will be served at . +After making code changes, the dev server will rebuild the mermaid library. You will need to reload the browser page yourself to see the changes. (PRs for auto reload are welcome!) -If you are adding a feature, you will definitely need to add tests. Depending on the size of your feature, you may need to add integration tests. +## Verify Everything is Working -#### Unit Tests +You can run the `test` script to verify that pnpm is working _and_ that the repository has been cloned correctly: -Unit tests are tests that test a single function or module. They are the easiest to write and the fastest to run. - -Unit tests are mandatory all code except the renderers. (The renderers are tested with integration tests.) - -We use [Vitest](https://vitest.dev) to run unit tests. - -You can use the following command to run the unit tests: - -```sh +```bash pnpm test ``` -When writing new tests, it's easier to have the tests automatically run as you make changes. You can do this by running the following command: - -```sh -pnpm test:watch -``` - -#### Integration/End-to-End (e2e) tests - -These test the rendering and visual appearance of the diagrams. -This ensures that the rendering of that feature in the e2e will be reviewed in the release process going forward. Less chance that it breaks! - -To start working with the e2e tests: - -1. Run `pnpm dev` to start the dev server -2. Start **Cypress** by running `pnpm cypress:open`. - -The rendering tests are very straightforward to create. There is a function `imgSnapshotTest`, which takes a diagram in text form and the mermaid options, and it renders that diagram in Cypress. - -When running in CI it will take a snapshot of the rendered diagram and compare it with the snapshot from last build and flag it for review if it differs. - -This is what a rendering test looks like: - -```js -it('should render forks and joins', () => { - imgSnapshotTest( - ` - stateDiagram - state fork_state <<fork>> - [*] --> fork_state - fork_state --> State2 - fork_state --> State3 - - state join_state <<join>> - State2 --> join_state - State3 --> join_state - join_state --> State4 - State4 --> [*] - `, - { logLevel: 0 } - ); - cy.get('svg'); -}); -``` - -**_[TODO - running the tests against what is expected in development. ]_** - -**_[TODO - how to generate new screenshots]_** -.... - -### 3. Update Documentation - -If the users have no way to know that things have changed, then you haven't really _fixed_ anything for the users; you've just added to making Mermaid feel broken. -Likewise, if users don't know that there is a new feature that you've implemented, it will forever remain unknown and unused. - -The documentation has to be updated to users know that things have changed and added! - -We know it can sometimes be hard to code _and_ write user documentation. - -Our documentation is managed in `packages/mermaid/src/docs`. Details on how to edit is in the [Contributing Documentation](#contributing-documentation) section. - -Create another issue specifically for the documentation. -You will need to help with the PR, but definitely ask for help if you feel stuck. -When it feels hard to write stuff out, explaining it to someone and having that person ask you clarifying questions can often be 80% of the work! - -When in doubt, write up and submit what you can. It can be clarified and refined later. (With documentation, something is better than nothing!) - -### 4. Submit your pull request - -**[TODO - PR titles should start with (fix | feat | ....)]** - -We make all changes via Pull Requests (PRs). As we have many Pull Requests from developers new to Mermaid, we have put in place a process wherein _knsv, Knut Sveidqvist_ is in charge of the final release process and the active maintainers are in charge of reviewing and merging most PRs. - -- PRs will be reviewed by active maintainers, who will provide feedback and request changes as needed. -- The maintainers will request a review from knsv, if necessary. -- Once the PR is approved, the maintainers will merge the PR into the `develop` branch. -- When a release is ready, the `release/x.x.x` branch will be created, extensively tested and knsv will be in charge of the release process. - -**Reminder: Pull Requests should be submitted to the develop branch.** - -## Contributing Documentation - -**_[TODO: This section is still a WIP. It still needs MAJOR revision.]_** - -If it is not in the documentation, it's like it never happened. Wouldn't that be sad? With all the effort that was put into the feature? - -The docs are located in the `packages/mermaid/src/docs` folder and are written in Markdown. Just pick the right section and start typing. - -The contents of [mermaid.js.org](https://mermaid.js.org/) are based on the docs from the `master` branch. -Updates committed to the `master` branch are reflected in the [Mermaid Docs](https://mermaid.js.org/) once published. - -### How to Contribute to Documentation - -We are a little less strict here, it is OK to commit directly in the `develop` branch if you are a collaborator. - -The documentation is located in the `packages/mermaid/src/docs` directory and organized according to relevant subfolder. - -The `docs` folder will be automatically generated when committing to `packages/mermaid/src/docs` and **should not** be edited manually. - -```mermaid -flowchart LR - classDef default fill:#fff,color:black,stroke:black - - source["files in /packages/mermaid/src/docs\n(changes should be done here)"] -- automatic processing\nto generate the final documentation--> published["files in /docs\ndisplayed on the official documentation site"] - -``` - -You can use `note`, `tip`, `warning` and `danger` in triple backticks to add a note, tip, warning or danger box. -Do not use vitepress specific markdown syntax `::: warning` as it will not be processed correctly. - -```` -```note -Note content -``` - -```tip -Tip content -``` - -```warning -Warning content -``` - -```danger -Danger content -``` - -```` - -```note -If the change is _only_ to the documentation, you can get your changes published to the site quicker by making a PR to the `master` branch. -``` - -We encourage contributions to the documentation at [packages/mermaid/src/docs in the _develop_ branch](https://github.com/mermaid-js/mermaid/tree/develop/packages/mermaid/src/docs). - -**_DO NOT CHANGE FILES IN `/docs`_** - -### The official documentation site - -**[The mermaid documentation site](https://mermaid.js.org/) is powered by [Vitepress](https://vitepress.vuejs.org/).** - -To run the documentation site locally: - -1. Run `pnpm --filter mermaid run docs:dev` to start the dev server. (Or `pnpm docs:dev` inside the `packages/mermaid` directory.) -2. Open [http://localhost:3333/](http://localhost:3333/) in your browser. - -Markdown is used to format the text, for more information about Markdown [see the GitHub Markdown help page](https://help.github.com/en/github/writing-on-github/basic-writing-and-formatting-syntax). - -To edit Docs on your computer: - -_[TODO: need to keep this in sync with [check out a git branch in Contributing Code above](#1-checkout-a-git-branch) ]_ - -1. Create a fork of the develop branch to work on. -2. Find the Markdown file (.md) to edit in the `packages/mermaid/src/docs` directory. -3. Make changes or add new documentation. -4. Commit changes to your branch and push it to GitHub (which should create a new branch). -5. Create a Pull Request of your fork. - -To edit Docs on GitHub: - -1. Login to [GitHub.com](https://www.github.com). -2. Navigate to [packages/mermaid/src/docs](https://github.com/mermaid-js/mermaid/tree/develop/packages/mermaid/src/docs) in the mermaid-js repository. -3. To edit a file, click the pencil icon at the top-right of the file contents panel. -4. Describe what you changed in the **Propose file change** section, located at the bottom of the page. -5. Submit your changes by clicking the button **Propose file change** at the bottom (by automatic creation of a fork and a new branch). -6. Visit the Actions tab in Github, `https://github.com//mermaid/actions` and enable the actions for your fork. This will ensure that the documentation is built and updated in your fork. -7. Create a Pull Request of your newly forked branch by clicking the green **Create Pull Request** button. - -### Documentation organization: Sidebar navigation - -If you want to propose changes to how the documentation is _organized_, such as adding a new section or re-arranging or renaming a section, you must update the **sidebar navigation.** - -The sidebar navigation is defined in [the vitepress configuration file config.ts](../.vitepress/config.ts). - -## Questions or Suggestions? - -#### First search to see if someone has already asked (and hopefully been answered) or suggested the same thing. - -- Search in Discussions -- Search in open Issues -- Search in closed Issues - -If you find an open issue or discussion thread that is similar to your question but isn't answered, you can let us know that you are also interested in it. -Use the GitHub reactions to add a thumbs-up to the issue or discussion thread. - -This helps the team know the relative interest in something and helps them set priorities and assignments. - -Feel free to add to the discussion on the issue or topic. - -If you can't find anything that already addresses your question or suggestion, _open a new issue:_ - -Log in to [GitHub.com](https://www.github.com), open or append to an issue [using the GitHub issue tracker of the mermaid-js repository](https://github.com/mermaid-js/mermaid/issues?q=is%3Aissue+is%3Aopen+label%3A%22Area%3A+Documentation%22). +The `test` script and others are in the top-level `package.json` file. -### How to Contribute a Suggestion +All tests should run successfully without any errors or failures. (You might see _lint_ or _formatting_ warnings; those are ok during this step.) ## Last Words diff --git a/packages/mermaid/src/docs/community/docker-development.md b/packages/mermaid/src/docs/community/docker-development.md new file mode 100644 index 0000000000..89e08b3f70 --- /dev/null +++ b/packages/mermaid/src/docs/community/docker-development.md @@ -0,0 +1,104 @@ +# Contributing to Mermaid via Docker + +> The following documentation describes how to work with Mermaid in a Docker environment. +> There's also a [host installation guide](../community/development.md) +> if you prefer to work without a Docker environment. + +So you want to help? That's great! + +![Image of happy people jumping with excitement](https://media.giphy.com/media/BlVnrxJgTGsUw/giphy.gif) + +Here are a few things to get you started on the right path. + +## Get the Source Code + +In GitHub, you first **fork** a repository when you are going to make changes and submit pull requests. + +Then you **clone** a copy to your local development machine (e.g. where you code) to make a copy with all the files to work with. + +[Fork mermaid](https://github.com/mermaid-js/mermaid/fork) to start contributing to the main project and its documentation, or [search for other repositories](https://github.com/orgs/mermaid-js/repositories). + +[Here is a GitHub document that gives an overview of the process.](https://docs.github.com/en/get-started/quickstart/fork-a-repo) + +## Technical Requirements + +> The following documentation describes how to work with Mermaid in a Docker environment. +> There's also a [host installation guide](../community/development.md) +> if you prefer to work without a Docker environment. + +[Install Docker](https://docs.docker.com/engine/install/). And that is pretty much all you need. + +Optionally, to run GUI (Cypress) within Docker you will also need an X11 server installed. +You might already have it installed, so check this by running: + +```bash +echo $DISPLAY +``` + +If the `$DISPLAY` variable is not empty, then an X11 server is running. Otherwise you may need to install one. + +## Setup and Launch + +### Switch to project + +Once you have cloned the repository onto your development machine, change into the `mermaid` project folder (the top level directory of the mermaid project repository) + +```bash +cd mermaid +``` + +### Make `./run` executable + +For development using Docker there is a self-documented `run` bash script, which provides convenient aliases for `docker compose` commands. + +Ensure `./run` script is executable: + +```bash +chmod +x run +``` + +```tip +To get detailed help simply type `./run` or `./run help`. + +It also has short _Development quick start guide_ embedded. +``` + +### Install packages + +```bash +./run pnpm install # Install packages +``` + +### Launch + +```bash +./run dev +``` + +Now you are ready to make your changes! Edit whichever files in `src` as required. + +Open in your browser, after starting the dev server. +There is a list of demos that can be used to see and test your changes. + +If you need a specific diagram, you can duplicate the `example.html` file in `/demos/dev` and add your own mermaid code to your copy. + +That will be served at . +After making code changes, the dev server will rebuild the mermaid library. You will need to reload the browser page yourself to see the changes. (PRs for auto reload are welcome!) + +## Verify Everything is Working + +```bash +./run pnpm test +``` + +The `test` script and others are in the top-level `package.json` file. + +All tests should run successfully without any errors or failures. (You might see _lint_ or _formatting_ warnings; those are ok during this step.) + +## Last Words + +Don't get daunted if it is hard in the beginning. We have a great community with only encouraging words. So, if you get stuck, ask for help and hints in the Slack forum. If you want to show off something good, show it off there. + +[Join our Slack community if you want closer contact!](https://join.slack.com/t/mermaid-talk/shared_invite/enQtNzc4NDIyNzk4OTAyLWVhYjQxOTI2OTg4YmE1ZmJkY2Y4MTU3ODliYmIwOTY3NDJlYjA0YjIyZTdkMDMyZTUwOGI0NjEzYmEwODcwOTE) + +![Image of superhero wishing you good luck](https://media.giphy.com/media/l49JHz7kJvl6MCj3G/giphy.gif) diff --git a/packages/mermaid/src/docs/community/documentation.md b/packages/mermaid/src/docs/community/documentation.md new file mode 100644 index 0000000000..6a0df983d1 --- /dev/null +++ b/packages/mermaid/src/docs/community/documentation.md @@ -0,0 +1,92 @@ +# Contributing Documentation + +**_[TODO: This section is still a WIP. It still needs MAJOR revision.]_** + +If it is not in the documentation, it's like it never happened. Wouldn't that be sad? With all the effort that was put into the feature? + +The docs are located in the `packages/mermaid/src/docs` folder and are written in Markdown. Just pick the right section and start typing. + +The contents of [mermaid.js.org](https://mermaid.js.org/) are based on the docs from the `master` branch. +Updates committed to the `master` branch are reflected in the [Mermaid Docs](https://mermaid.js.org/) once published. + +## How to Contribute to Documentation + +We are a little less strict here, it is OK to commit directly in the `develop` branch if you are a collaborator. + +The documentation is located in the `packages/mermaid/src/docs` directory and organized according to relevant subfolder. + +The `docs` folder will be automatically generated when committing to `packages/mermaid/src/docs` and **should not** be edited manually. + +```mermaid +flowchart LR + classDef default fill:#fff,color:black,stroke:black + + source["files in /packages/mermaid/src/docs\n(changes should be done here)"] -- automatic processing\nto generate the final documentation--> published["files in /docs\ndisplayed on the official documentation site"] + +``` + +You can use `note`, `tip`, `warning` and `danger` in triple backticks to add a note, tip, warning or danger box. +Do not use vitepress specific markdown syntax `::: warning` as it will not be processed correctly. + +````markdown +```note +Note content +``` + +```tip +Tip content +``` + +```warning +Warning content +``` + +```danger +Danger content +``` +```` + +```note +If the change is _only_ to the documentation, you can get your changes published to the site quicker by making a PR to the `master` branch. In that case, your branch should be based on master, not develop. +``` + +We encourage contributions to the documentation at [packages/mermaid/src/docs in the _develop_ branch](https://github.com/mermaid-js/mermaid/tree/develop/packages/mermaid/src/docs). + +**_DO NOT CHANGE FILES IN `/docs`_** + +## The official documentation site + +**[The mermaid documentation site](https://mermaid.js.org/) is powered by [Vitepress](https://vitepress.vuejs.org/).** + +To run the documentation site locally: + +1. Run `pnpm --filter mermaid run docs:dev` to start the dev server. (Or `pnpm docs:dev` inside the `packages/mermaid` directory.) +2. Open [http://localhost:3333/](http://localhost:3333/) in your browser. + +Markdown is used to format the text, for more information about Markdown [see the GitHub Markdown help page](https://help.github.com/en/github/writing-on-github/basic-writing-and-formatting-syntax). + +To edit Docs on your computer: + +_[TODO: need to keep this in sync with [check out a git branch in Contributing Code above](#1-checkout-a-git-branch) ]_ + +1. Create a fork of the develop branch to work on. +2. Find the Markdown file (.md) to edit in the `packages/mermaid/src/docs` directory. +3. Make changes or add new documentation. +4. Commit changes to your branch and push it to GitHub (which should create a new branch). +5. Create a Pull Request from the branch of your fork. + +To edit Docs on GitHub: + +1. Login to [GitHub.com](https://www.github.com). +2. Navigate to [packages/mermaid/src/docs](https://github.com/mermaid-js/mermaid/tree/develop/packages/mermaid/src/docs) in the mermaid-js repository. +3. To edit a file, click the pencil icon at the top-right of the file contents panel. +4. Describe what you changed in the **Propose file change** section, located at the bottom of the page. +5. Submit your changes by clicking the button **Propose file change** at the bottom (by automatic creation of a fork and a new branch). +6. Visit the Actions tab in Github, `https://github.com//mermaid/actions` and enable the actions for your fork. This will ensure that the documentation is built and updated in your fork. +7. Create a Pull Request of your newly forked branch by clicking the green **Create Pull Request** button. + +## Documentation organization: Sidebar navigation + +If you want to propose changes to how the documentation is _organized_, such as adding a new section or re-arranging or renaming a section, you must update the **sidebar navigation.** + +The sidebar navigation is defined in [the vitepress configuration file config.ts](../.vitepress/config.ts). diff --git a/packages/mermaid/src/docs/community/n00b-overview.md b/packages/mermaid/src/docs/community/n00b-overview.md deleted file mode 100644 index e8e84641a3..0000000000 --- a/packages/mermaid/src/docs/community/n00b-overview.md +++ /dev/null @@ -1,68 +0,0 @@ -# Overview for Beginners - -**Explaining with a Diagram** - -A picture is worth a thousand words, a good diagram is undoubtedly worth more. They make understanding easier. - -## Creating and Maintaining Diagrams - -Anyone who has used Visio, or (God Forbid) Excel to make a Gantt Chart, knows how hard it is to create, edit and maintain good visualizations. - -Diagrams/Charts are significant but also become obsolete/inaccurate very fast. This catch-22 hobbles the productivity of teams. - -# Doc Rot in Diagrams - -Doc-Rot kills diagrams as quickly as it does text, but it takes hours in a desktop application to produce a diagram. - -Mermaid seeks to change using markdown-inspired syntax. The process is a quicker, less complicated, and more convenient way of going from concept to visualization. - -It is a relatively straightforward solution to a significant hurdle with the software teams. - -# Definition of Terms/ Dictionary - -**Mermaid text definitions can be saved for later reuse and editing.** - -> These are the Mermaid diagram definitions inside `
` tags, with the `class=mermaid`. - -```html -
-    graph TD
-    A[Client] --> B[Load Balancer]
-    B --> C[Server01]
-    B --> D[Server02]
-
-``` - -**render** - -> This is the core function of the Mermaid API. It reads all the `Mermaid Definitions` inside `div` tags and returns an SVG file, based on the definition. - -**Nodes** - -> These are the boxes that contain text or otherwise discrete pieces of each diagram, separated generally by arrows, except for Gantt Charts and User Journey Diagrams. They will be referred often in the instructions. Read for Diagram Specific [Syntax](../intro/n00b-syntaxReference.md) - -## Advantages of using Mermaid - -- Ease to generate, modify and render diagrams when you make them. -- The number of integrations and plugins it has. -- You can add it to your or companies website. -- Diagrams can be created through comments like this in a script: - -## The catch-22 of Diagrams and Charts: - -**Diagramming and charting is a large waste of developer's time, but not having diagrams ruins productivity.** - -Mermaid solves this by reducing the time and effort required to create diagrams and charts. - -Because, the text base for the diagrams allows it to be updated easily. Also, it can be made part of production scripts (and other pieces of code). So less time is spent on documenting, as a separate task. - -## Catching up with Development - -Being based on markdown, Mermaid can be used, not only by accomplished front-end developers, but by most computer savvy people to render diagrams, at much faster speeds. -In fact one can pick up the syntax for it quite easily from the examples given and there are many tutorials available in the internet. - -## Mermaid is for everyone. - -Video [Tutorials](https://mermaid.js.org/config/Tutorials.html) are also available for the mermaid [live editor](https://mermaid.live/). - -Alternatively you can use Mermaid [Plug-Ins](https://mermaid-js.github.io/mermaid/#/./integrations), with tools you already use, like Google Docs. diff --git a/packages/mermaid/src/docs/community/newDiagram.md b/packages/mermaid/src/docs/community/newDiagram.md index 75e17e4c9b..bc43475bfc 100644 --- a/packages/mermaid/src/docs/community/newDiagram.md +++ b/packages/mermaid/src/docs/community/newDiagram.md @@ -4,7 +4,7 @@ #### Grammar -This would be to define a jison grammar for the new diagram type. That should start with a way to identify that the text in the mermaid tag is a diagram of that type. Create a new folder under diagrams for your new diagram type and a parser folder in it. This leads us to step 2. +This would be to define a JISON grammar for the new diagram type. That should start with a way to identify that the text in the mermaid tag is a diagram of that type. Create a new folder under diagrams for your new diagram type and a parser folder in it. This leads us to step 2. For instance: @@ -13,7 +13,7 @@ For instance: #### Store data found during parsing -There are some jison specific sub steps here where the parser stores the data encountered when parsing the diagram, this data is later used by the renderer. You can during the parsing call a object provided to the parser by the user of the parser. This object can be called during parsing for storing data. +There are some jison specific sub steps here where the parser stores the data encountered when parsing the diagram, this data is later used by the renderer. You can during the parsing call an object provided to the parser by the user of the parser. This object can be called during parsing for storing data. ```jison statement @@ -30,7 +30,7 @@ In the extract of the grammar above, it is defined that a call to the setTitle m Make sure that the `parseError` function for the parser is defined and calling `mermaid.parseError`. This way a common way of detecting parse errors is provided for the end-user. ``` -For more info look in the example diagram type: +For more info look at the example diagram type: The `yy` object has the following function: @@ -49,15 +49,15 @@ parser.yy = db; ### Step 2: Rendering -Write a renderer that given the data found during parsing renders the diagram. To look at an example look at sequenceRenderer.js rather then the flowchart renderer as this is a more generic example. +Write a renderer that given the data found during parsing renders the diagram. To look at an example look at sequenceRenderer.js rather than the flowchart renderer as this is a more generic example. Place the renderer in the diagram folder. ### Step 3: Detection of the new diagram type -The second thing to do is to add the capability to detect the new diagram to type to the detectType in utils.js. The detection should return a key for the new diagram type. +The second thing to do is to add the capability to detect the new diagram to type to the detectType in `diagram-api/detectType.ts`. The detection should return a key for the new diagram type. [This key will be used to as the aria roledescription](#aria-roledescription), so it should be a word that clearly describes the diagram type. -For example, if your new diagram use a UML deployment diagram, a good key would be "UMLDeploymentDiagram" because assistive technologies such as a screen reader +For example, if your new diagram uses a UML deployment diagram, a good key would be "UMLDeploymentDiagram" because assistive technologies such as a screen reader would voice that as "U-M-L Deployment diagram." Another good key would be "deploymentDiagram" because that would be voiced as "Deployment Diagram." A bad key would be "deployment" because that would not sufficiently describe the diagram. Note that the diagram type key does not have to be the same as the diagram keyword chosen for the [grammar](#grammar), but it is helpful if they are the same. @@ -117,54 +117,7 @@ There are a few features that are common between the different types of diagrams - Themes, there is a common way to modify the styling of diagrams in Mermaid. - Comments should follow mermaid standards -Here some pointers on how to handle these different areas. - -#### [Directives](../config/directives.md) - -Here is example handling from flowcharts: -Jison: - -```jison -/* lexical grammar */ -%lex -%x open_directive -%x type_directive -%x arg_directive -%x close_directive - -\%\%\{ { this.begin('open_directive'); return 'open_directive'; } -((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; } -":" { this.popState(); this.begin('arg_directive'); return ':'; } -\}\%\% { this.popState(); this.popState(); return 'close_directive'; } -((?:(?!\}\%\%).|\n)*) return 'arg_directive'; - -/* language grammar */ - -/* ... */ - -directive - : openDirective typeDirective closeDirective separator - | openDirective typeDirective ':' argDirective closeDirective separator - ; - -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', 'flowchart'); } - ; -``` - -It is probably a good idea to keep the handling similar to this in your new diagram. The parseDirective function is provided by the mermaidAPI. +Here are some pointers on how to handle these different areas. ## Accessibility @@ -182,9 +135,9 @@ See [the definition of aria-roledescription](https://www.w3.org/TR/wai-aria-1.1/ ### accessible title and description -The syntax for accessible titles and descriptions is described in [the Accessibility documenation section.](../config/accessibility.md) +The syntax for accessible titles and descriptions is described in [the Accessibility documentation section.](../config/accessibility.md) -In a similar way to the directives, the jison syntax are quite similar between the diagrams. +As a design goal, the jison syntax should be similar between the diagrams. ```jison diff --git a/packages/mermaid/src/docs/community/questions-and-suggestions.md b/packages/mermaid/src/docs/community/questions-and-suggestions.md new file mode 100644 index 0000000000..6d6f80fb6d --- /dev/null +++ b/packages/mermaid/src/docs/community/questions-and-suggestions.md @@ -0,0 +1,20 @@ +# Questions or Suggestions? + +**_[TODO: This section is still a WIP. It still needs MAJOR revision.]_** + +## First search to see if someone has already asked (and hopefully been answered) or suggested the same thing. + +- Search in Discussions +- Search in open Issues +- Search in closed Issues + +If you find an open issue or discussion thread that is similar to your question but isn't answered, you can let us know that you are also interested in it. +Use the GitHub reactions to add a thumbs-up to the issue or discussion thread. + +This helps the team know the relative interest in something and helps them set priorities and assignments. + +Feel free to add to the discussion on the issue or topic. + +If you can't find anything that already addresses your question or suggestion, _open a new issue:_ + +Log in to [GitHub.com](https://www.github.com), open or append to an issue [using the GitHub issue tracker of the mermaid-js repository](https://github.com/mermaid-js/mermaid/issues?q=is%3Aissue+is%3Aopen+label%3A%22Area%3A+Documentation%22). diff --git a/packages/mermaid/src/docs/config/Tutorials.md b/packages/mermaid/src/docs/config/Tutorials.md index 875f152459..af7dbe672c 100644 --- a/packages/mermaid/src/docs/config/Tutorials.md +++ b/packages/mermaid/src/docs/config/Tutorials.md @@ -20,9 +20,13 @@ The definitions that can be generated the Live-Editor are also backwards-compati [Eddie Jaoude: Can you code your diagrams?](https://www.youtube.com/watch?v=9HZzKkAqrX8) +## Mermaid with OpenAI + +[Elle Neal: Mind Mapping with AI: An Accessible Approach for Neurodiverse Learners Tutorial:](https://medium.com/@elle.neal_71064/mind-mapping-with-ai-an-accessible-approach-for-neurodiverse-learners-1a74767359ff), [Demo:](https://databutton.com/v/jk9vrghc) + ## Mermaid with HTML -Examples are provided in [Getting Started](../intro/n00b-gettingStarted.md) +Examples are provided in [Getting Started](../intro/getting-started.md) **CodePen Examples:** diff --git a/packages/mermaid/src/docs/config/n00b-advanced.md b/packages/mermaid/src/docs/config/advanced.md similarity index 90% rename from packages/mermaid/src/docs/config/n00b-advanced.md rename to packages/mermaid/src/docs/config/advanced.md index 2932faa485..4ab4774285 100644 --- a/packages/mermaid/src/docs/config/n00b-advanced.md +++ b/packages/mermaid/src/docs/config/advanced.md @@ -1,4 +1,4 @@ -# Advanced n00b mermaid (Coming soon..) +# Advanced mermaid (Coming soon..) ## splitting mermaid code from html diff --git a/packages/mermaid/src/docs/config/configuration.md b/packages/mermaid/src/docs/config/configuration.md index d248944ddd..e52f2c6d5d 100644 --- a/packages/mermaid/src/docs/config/configuration.md +++ b/packages/mermaid/src/docs/config/configuration.md @@ -4,10 +4,28 @@ When mermaid starts, configuration is extracted to determine a configuration to - The default configuration - Overrides at the site level are set by the initialize call, and will be applied to all diagrams in the site/app. The term for this is the **siteConfig**. -- Directives - diagram authors can update select configuration parameters directly in the diagram code via directives. These are applied to the render config. +- Frontmatter (v+) - diagram authors can update select configuration parameters in the frontmatter of the diagram. These are applied to the render config. +- Directives (Deprecated by Frontmatter) - diagram authors can update select configuration parameters directly in the diagram code via directives. These are applied to the render config. **The render config** is configuration that is used when rendering by applying these configurations. +## Frontmatter config + +The entire mermaid configuration (except the secure configs) can be overridden by the diagram author in the frontmatter of the diagram. The frontmatter is a YAML block at the top of the diagram. + +```mermaid-example +--- +title: Hello Title +config: + theme: base + themeVariables: + primaryColor: "#00ff00" +--- +flowchart + Hello --> World + +``` + ## Theme configuration ## Starting mermaid diff --git a/packages/mermaid/src/docs/config/directives.md b/packages/mermaid/src/docs/config/directives.md index c85d1d245e..7763eb0732 100644 --- a/packages/mermaid/src/docs/config/directives.md +++ b/packages/mermaid/src/docs/config/directives.md @@ -1,5 +1,9 @@ # Directives +```warning +Directives are deprecated from v. Please use the `config` key in frontmatter to pass configuration. See [Configuration](./configuration.md) for more details. +``` + ## Directives Directives give a diagram author the capability to alter the appearance of a diagram before rendering by changing the applied configuration. @@ -112,7 +116,7 @@ The following code snippet changes `theme` to `forest`: `%%{init: { "theme": "forest" } }%%` -Possible theme values are: `default`,`base`, `dark`, `forest` and `neutral`. +Possible theme values are: `default`, `base`, `dark`, `forest` and `neutral`. Default Value is `default`. Example: @@ -231,7 +235,7 @@ Let us see an example: sequenceDiagram Alice->Bob: Hello Bob, how are you? -Bob->Alice: Fine, how did you mother like the book I suggested? And did you catch the new book about alien invasion? +Bob->Alice: Fine, how did your mother like the book I suggested? And did you catch the new book about alien invasion? Alice->Bob: Good. Bob->Alice: Cool ``` @@ -248,7 +252,7 @@ By applying that snippet to the diagram above, `wrap` will be enabled: %%{init: { "sequence": { "wrap": true, "width":300 } } }%% sequenceDiagram Alice->Bob: Hello Bob, how are you? -Bob->Alice: Fine, how did you mother like the book I suggested? And did you catch the new book about alien invasion? +Bob->Alice: Fine, how did your mother like the book I suggested? And did you catch the new book about alien invasion? Alice->Bob: Good. Bob->Alice: Cool ``` diff --git a/packages/mermaid/src/docs/config/theming.md b/packages/mermaid/src/docs/config/theming.md index 0e4571d15f..0e08532837 100644 --- a/packages/mermaid/src/docs/config/theming.md +++ b/packages/mermaid/src/docs/config/theming.md @@ -55,9 +55,9 @@ To make a custom theme, modify `themeVariables` via `init`. You will need to use the [base](#available-themes) theme as it is the only modifiable theme. -| Parameter | Description | Type | Properties | -| -------------- | ------------------------------------ | ------ | --------------------------------------------------------------------------------------------------- | -| themeVariables | Modifiable with the `init` directive | Object | `primaryColor`, `primaryTextColor`, `lineColor` ([see full list](#theme-variables-reference-table)) | +| Parameter | Description | Type | Properties | +| -------------- | ------------------------------------ | ------ | ----------------------------------------------------------------------------------- | +| themeVariables | Modifiable with the `init` directive | Object | `primaryColor`, `primaryTextColor`, `lineColor` ([see full list](#theme-variables)) | Example of modifying `themeVariables` using the `init` directive: diff --git a/packages/mermaid/src/docs/config/usage.md b/packages/mermaid/src/docs/config/usage.md index a072ae4081..ae3ad0f0aa 100644 --- a/packages/mermaid/src/docs/config/usage.md +++ b/packages/mermaid/src/docs/config/usage.md @@ -35,7 +35,7 @@ pnpm add mermaid **Hosting mermaid on a web page:** -> Note:This topic explored in greater depth in the [User Guide for Beginners](../intro/n00b-gettingStarted.md) +> Note: This topic is explored in greater depth in the [User Guide for Beginners](../intro/getting-started.md) The easiest way to integrate mermaid on a web page requires two elements: @@ -94,7 +94,7 @@ Mermaid can load multiple diagrams, in the same page. ## Enabling Click Event and Tags in Nodes -A `securityLevel` configuration has to first be cleared. `securityLevel` sets the level of trust for the parsed diagrams and limits click functionality. This was introduce in version 8.2 as a security improvement, aimed at preventing malicious use. +A `securityLevel` configuration has to first be cleared. `securityLevel` sets the level of trust for the parsed diagrams and limits click functionality. This was introduced in version 8.2 as a security improvement, aimed at preventing malicious use. **It is the site owner's responsibility to discriminate between trustworthy and untrustworthy user-bases and we encourage the use of discretion.** @@ -109,14 +109,14 @@ Values: - **strict**: (**default**) HTML tags in the text are encoded and click functionality is disabled. - **antiscript**: HTML tags in text are allowed (only script elements are removed) and click functionality is enabled. - **loose**: HTML tags in text are allowed and click functionality is enabled. -- **sandbox**: With this security level, all rendering takes place in a sandboxed iframe. This prevent any JavaScript from running in the context. This may hinder interactive functionality of the diagram, like scripts, popups in the sequence diagram, links to other tabs or targets, etc. +- **sandbox**: With this security level, all rendering takes place in a sandboxed iframe. This prevents any JavaScript from running in the context. This may hinder interactive functionality of the diagram, like scripts, popups in the sequence diagram, links to other tabs or targets, etc. ```note This changes the default behaviour of mermaid so that after upgrade to 8.2, unless the `securityLevel` is not changed, tags in flowcharts are encoded as tags and clicking is disabled. **sandbox** security level is still in the beta version. ``` -**If you are taking responsibility for the diagram source security you can set the `securityLevel` to a value of your choosing . This allows clicks and tags are allowed.** +**If you are taking responsibility for the diagram source security you can set the `securityLevel` to a value of your choosing. This allows clicks and tags are allowed.** **To change `securityLevel`, you have to call `mermaid.initialize`:** @@ -225,7 +225,7 @@ mermaid fully supports webpack. Here is a [working demo](https://github.com/merm The main idea of the API is to be able to call a render function with the graph definition as a string. The render function will render the graph and call a callback with the resulting SVG code. With this approach it is up to the site creator to fetch the graph definition from the site (perhaps from a textarea), render it and place the graph somewhere in the site. -The example below show an outline of how this could be used. The example just logs the resulting SVG to the JavaScript console. +The example below shows an example of how this could be used. The example just logs the resulting SVG to the JavaScript console. ```html +``` + +### Links Coloring + +You can adjust links' color by setting `linkColor` to one of those: + +- `source` - link will be of a source node color +- `target` - link will be of a target node color +- `gradient` - link color will be smoothly transient between source and target node colors +- hex code of color, like `#a1a1a1` + +### Node Alignment + +Graph layout can be changed by setting `nodeAlignment` to: + +- `justify` +- `center` +- `left` +- `right` diff --git a/packages/mermaid/src/docs/syntax/sequenceDiagram.md b/packages/mermaid/src/docs/syntax/sequenceDiagram.md index 0d54421299..cdcdaffeba 100644 --- a/packages/mermaid/src/docs/syntax/sequenceDiagram.md +++ b/packages/mermaid/src/docs/syntax/sequenceDiagram.md @@ -58,6 +58,31 @@ sequenceDiagram J->>A: Great! ``` +### Actor Creation and Destruction (v10.3.0+) + +It is possible to create and destroy actors by messages. To do so, add a create or destroy directive before the message. + +``` +create participant B +A --> B: Hello +``` + +Create directives support actor/participant distinction and aliases. The sender or the recipient of a message can be destroyed but only the recipient can be created. + +```mermaid-example +sequenceDiagram + Alice->>Bob: Hello Bob, how are you ? + Bob->>Alice: Fine, thank you. And you? + create participant Carl + Alice->>Carl: Hi Carl! + create actor D as Donald + Carl->>D: Hi! + destroy Carl + Alice-xCarl: We are too many + destroy Bob + Bob->>Alice: I agree +``` + ### 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: @@ -96,7 +121,7 @@ end end A->>J: Hello John, how are you? J->>A: Great! - A->>B: Hello Bob, how is Charly ? + A->>B: Hello Bob, how is Charly? B->>C: Hello Charly, how are you? ``` diff --git a/packages/mermaid/src/docs/syntax/stateDiagram.md b/packages/mermaid/src/docs/syntax/stateDiagram.md index 248146993e..f35796b132 100644 --- a/packages/mermaid/src/docs/syntax/stateDiagram.md +++ b/packages/mermaid/src/docs/syntax/stateDiagram.md @@ -304,7 +304,7 @@ where - the second _property_ is `color` and its _value_ is `white` - the third _property_ is `font-weight` and its _value_ is `bold` - the fourth _property_ is `stroke-width` and its _value_ is `2px` -- the fifth _property_ is `stroke` and its _value_ is `yello` +- the fifth _property_ is `stroke` and its _value_ is `yellow` ### Apply classDef styles to states diff --git a/packages/mermaid/src/docs/syntax/timeline.md b/packages/mermaid/src/docs/syntax/timeline.md index c9bc9161ee..201ab6b16f 100644 --- a/packages/mermaid/src/docs/syntax/timeline.md +++ b/packages/mermaid/src/docs/syntax/timeline.md @@ -172,9 +172,11 @@ let us look at same example, where we have disabled the multiColor option. ### 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 sections, 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. +You can customize the color scheme using the `cScale0` to `cScale11` theme variables, which will change the background colors. Mermaid allows you to set unique colors for up-to 12 sections, 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. +If you also want to change the foreground color of a section, you can do so use theme variables corresponding `cScaleLabel0` to `cScaleLabel11` variables. + 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: @@ -183,9 +185,9 @@ Now let's override the default values for the `cScale0` to `cScale2` variables: ```mermaid-example %%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': { - 'cScale0': '#ff0000', + 'cScale0': '#ff0000', 'cScaleLabel0': '#ffffff', 'cScale1': '#00ff00', - 'cScale2': '#0000ff' + 'cScale2': '#0000ff', 'cScaleLabel2': '#ffffff' } } }%% timeline title History of Social Media Platform diff --git a/packages/mermaid/src/mermaid.spec.ts b/packages/mermaid/src/mermaid.spec.ts index e27428545e..0b4437d742 100644 --- a/packages/mermaid/src/mermaid.spec.ts +++ b/packages/mermaid/src/mermaid.spec.ts @@ -3,6 +3,7 @@ import { mermaidAPI } from './mermaidAPI.js'; import './diagram-api/diagram-orchestration.js'; import { addDiagrams } from './diagram-api/diagram-orchestration.js'; import { beforeAll, describe, it, expect, vi } from 'vitest'; +import type { DiagramDefinition } from './diagram-api/types.js'; beforeAll(async () => { addDiagrams(); @@ -92,13 +93,16 @@ describe('when using mermaid and ', () => { it('should defer diagram load based on parameter', async () => { let loaded = false; - const dummyDiagram = { + const dummyDiagram: DiagramDefinition = { db: {}, renderer: () => { // do nothing }, - parser: () => { - // do nothing + parser: { + parse: (_text) => { + return; + }, + parser: { yy: {} }, }, styles: () => { // do nothing diff --git a/packages/mermaid/src/mermaid.ts b/packages/mermaid/src/mermaid.ts index 30297e7b5e..caf4a2b9b3 100644 --- a/packages/mermaid/src/mermaid.ts +++ b/packages/mermaid/src/mermaid.ts @@ -3,20 +3,18 @@ * functionality and to render the diagrams to svg code! */ import { dedent } from 'ts-dedent'; -import { MermaidConfig } from './config.type.js'; +import type { MermaidConfig } from './config.type.js'; import { log } from './logger.js'; import utils from './utils.js'; -import { mermaidAPI, ParseOptions, RenderResult } from './mermaidAPI.js'; -import { - registerLazyLoadedDiagrams, - loadRegisteredDiagrams, - detectType, -} from './diagram-api/detectType.js'; +import type { ParseOptions, RenderResult } from './mermaidAPI.js'; +import { mermaidAPI } from './mermaidAPI.js'; +import { registerLazyLoadedDiagrams, detectType } from './diagram-api/detectType.js'; +import { loadRegisteredDiagrams } from './diagram-api/loadDiagram.js'; import type { ParseErrorFunction } from './Diagram.js'; import { isDetailedError } from './utils.js'; import type { DetailedError } from './utils.js'; -import { ExternalDiagramDefinition } from './diagram-api/types.js'; -import { UnknownDiagramError } from './errors.js'; +import type { ExternalDiagramDefinition } from './diagram-api/types.js'; +import type { UnknownDiagramError } from './errors.js'; export type { MermaidConfig, diff --git a/packages/mermaid/src/mermaidAPI.spec.ts b/packages/mermaid/src/mermaidAPI.spec.ts index 11c16827b9..d7c16a1cfb 100644 --- a/packages/mermaid/src/mermaidAPI.spec.ts +++ b/packages/mermaid/src/mermaidAPI.spec.ts @@ -34,7 +34,7 @@ vi.mock('./diagrams/state/stateRenderer-v2.js'); // ------------------------------------- import mermaid from './mermaid.js'; -import { MermaidConfig } from './config.type.js'; +import type { MermaidConfig } from './config.type.js'; import mermaidAPI, { removeExistingElements } from './mermaidAPI.js'; import { @@ -733,59 +733,7 @@ describe('mermaidAPI', () => { const diagramText = `${diagramType}\n accTitle: ${a11yTitle}\n accDescr: ${a11yDescr}\n`; const expectedDiagramType = testedDiagram.expectedType; - it('aria-roledscription is set to the diagram type, addSVGa11yTitleDescription is called', async () => { - const a11yDiagramInfo_spy = vi.spyOn(accessibility, 'setA11yDiagramInfo'); - const a11yTitleDesc_spy = vi.spyOn(accessibility, 'addSVGa11yTitleDescription'); - await mermaidAPI.render(id, diagramText); - expect(a11yDiagramInfo_spy).toHaveBeenCalledWith( - expect.anything(), - expectedDiagramType - ); - expect(a11yTitleDesc_spy).toHaveBeenCalled(); - }); - }); - }); - }); - }); - - describe('render', () => { - // Be sure to add async before each test (anonymous) method - - // These are more like integration tests right now because nothing is mocked. - // But it is faster that a cypress test and there's no real reason to actually evaluate an image pixel by pixel. - - // render(id, text, cb?, svgContainingElement?) - - // Test all diagram types. Note that old flowchart 'graph' type will invoke the flowRenderer-v2. (See the flowchart v2 detector.) - // We have to have both the specific textDiagramType and the expected type name because the expected type may be slightly different than was is put in the diagram text (ex: in -v2 diagrams) - const diagramTypesAndExpectations = [ - { textDiagramType: 'C4Context', expectedType: 'c4' }, - { textDiagramType: 'classDiagram', expectedType: 'classDiagram' }, - { textDiagramType: 'classDiagram-v2', expectedType: 'classDiagram' }, - { textDiagramType: 'erDiagram', expectedType: 'er' }, - { textDiagramType: 'graph', expectedType: 'flowchart-v2' }, - { textDiagramType: 'flowchart', expectedType: 'flowchart-v2' }, - { textDiagramType: 'gitGraph', expectedType: 'gitGraph' }, - { textDiagramType: 'gantt', expectedType: 'gantt' }, - { textDiagramType: 'journey', expectedType: 'journey' }, - { textDiagramType: 'pie', expectedType: 'pie' }, - { textDiagramType: 'requirementDiagram', expectedType: 'requirement' }, - { textDiagramType: 'sequenceDiagram', expectedType: 'sequence' }, - { textDiagramType: 'stateDiagram-v2', expectedType: 'stateDiagram' }, - ]; - - describe('accessibility', () => { - const id = 'mermaid-fauxId'; - const a11yTitle = 'a11y title'; - const a11yDescr = 'a11y description'; - - diagramTypesAndExpectations.forEach((testedDiagram) => { - describe(`${testedDiagram.textDiagramType}`, () => { - const diagramType = testedDiagram.textDiagramType; - const diagramText = `${diagramType}\n accTitle: ${a11yTitle}\n accDescr: ${a11yDescr}\n`; - const expectedDiagramType = testedDiagram.expectedType; - - it('aria-roledscription is set to the diagram type, addSVGa11yTitleDescription is called', async () => { + it('should set aria-roledscription to the diagram type AND should call addSVGa11yTitleDescription', async () => { const a11yDiagramInfo_spy = vi.spyOn(accessibility, 'setA11yDiagramInfo'); const a11yTitleDesc_spy = vi.spyOn(accessibility, 'addSVGa11yTitleDescription'); await mermaidAPI.render(id, diagramText); diff --git a/packages/mermaid/src/mermaidAPI.ts b/packages/mermaid/src/mermaidAPI.ts index bb35391d06..e9ec3eca14 100644 --- a/packages/mermaid/src/mermaidAPI.ts +++ b/packages/mermaid/src/mermaidAPI.ts @@ -23,13 +23,14 @@ import { attachFunctions } from './interactionDb.js'; import { log, setLogLevel } from './logger.js'; import getStyles from './styles.js'; import theme from './themes/index.js'; -import utils, { directiveSanitizer } from './utils.js'; +import utils from './utils.js'; import DOMPurify from 'dompurify'; -import { MermaidConfig } from './config.type.js'; +import type { MermaidConfig } from './config.type.js'; import { evaluate } from './diagrams/common/common.js'; import isEmpty from 'lodash-es/isEmpty.js'; import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility.js'; import { parseDirective } from './directiveUtils.js'; +import { extractFrontMatter } from './diagram-api/frontmatter.js'; // diagram names that support classDef statements const CLASSDEF_DIAGRAMS = [ @@ -108,8 +109,7 @@ export interface RenderResult { async function parse(text: string, parseOptions?: ParseOptions): Promise { addDiagrams(); try { - const diagram = await getDiagramFromText(text); - diagram.parse(); + await getDiagramFromText(text); } catch (error) { if (parseOptions?.suppressErrors) { return false; @@ -285,7 +285,9 @@ export const cleanUpSvgCode = ( * TODO replace btoa(). Replace with buf.toString('base64')? */ export const putIntoIFrame = (svgCode = '', svgElement?: D3Element): string => { - const height = svgElement ? svgElement.viewBox.baseVal.height + 'px' : IFRAME_HEIGHT; + const height = svgElement?.viewBox?.baseVal?.height + ? svgElement.viewBox.baseVal.height + 'px' + : IFRAME_HEIGHT; const base64encodedSrc = btoa('' + svgCode + ''); return `