diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 000000000..e5b6d8d6a --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 000000000..ffda055e8 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", + "changelog": ["@changesets/changelog-github", { "repo": "mermaid-js/mermaid" }], + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "master", + "updateInternalDependencies": "patch", + "bumpVersionsWithWorkspaceProtocolOnly": true, + "ignore": ["@mermaid-js/docs", "@mermaid-js/webpack-test", "@mermaid-js/mermaid-example-diagram"] +} diff --git a/.cspell/code-terms.txt b/.cspell/code-terms.txt index 9d2f700fc..6d6dad045 100644 --- a/.cspell/code-terms.txt +++ b/.cspell/code-terms.txt @@ -120,6 +120,8 @@ SUBROUTINEEND SUBROUTINESTART Subschemas substr +SVGG +SVGSVG TAGEND TAGSTART techn diff --git a/.cspell/libraries.txt b/.cspell/libraries.txt index c185429b0..3bfec1d5f 100644 --- a/.cspell/libraries.txt +++ b/.cspell/libraries.txt @@ -58,6 +58,7 @@ rehype roughjs rscratch shiki +Slidev sparkline sphinxcontrib ssim diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index cfd22a293..c22166619 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -15,4 +15,4 @@ Make sure you - [ ] :book: have read the [contribution guidelines](https://mermaid.js.org/community/contributing.html) - [ ] :computer: have added necessary unit/e2e tests. - [ ] :notebook: have added documentation. Make sure [`MERMAID_RELEASE_VERSION`](https://mermaid.js.org/community/contributing.html#update-documentation) is used for all new features. -- [ ] :bookmark: targeted `develop` branch +- [ ] :butterfly: If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running `pnpm changeset` and following the prompts. Changesets that add features should be `minor` and those that fix bugs should be `patch`. Please prefix changeset messages with `feat:`, `fix:`, or `chore:`. diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml deleted file mode 100644 index 83138c3d4..000000000 --- a/.github/release-drafter.yml +++ /dev/null @@ -1,36 +0,0 @@ -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' - 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 -exclude-labels: - - 'Skip changelog' -no-changes-template: 'This release contains minor changes and bugfixes.' -template: | - # Release Notes - - $CHANGES - - 🎉 **Thanks to all contributors helping with this release!** 🎉 diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index ef170fb71..6a43791ed 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -2,20 +2,24 @@ name: autofix.ci # needed to securely identify the workflow on: pull_request: + branches-ignore: + - 'renovate/**' permissions: contents: read +concurrency: ${{ github.workflow }}-${{ github.ref }} + jobs: autofix: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 # uses version from "packageManager" field in package.json - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: pnpm node-version-file: '.node-version' @@ -38,4 +42,4 @@ jobs: working-directory: ./packages/mermaid run: pnpm run docs:build - - uses: autofix-ci/action@ff86a557419858bb967097bfc916833f5647fa8c + - uses: autofix-ci/action@ff86a557419858bb967097bfc916833f5647fa8c # main diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 0ce778957..eb0c4594a 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -8,6 +8,8 @@ on: pull_request: merge_group: +concurrency: ${{ github.workflow }}-${{ github.ref }} + permissions: contents: read @@ -16,12 +18,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: pnpm node-version-file: '.node-version' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index c6e96912e..000000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Build - -on: - push: {} - merge_group: - pull_request: - types: - - opened - - synchronize - - ready_for_review - -permissions: - contents: read - -jobs: - build-mermaid: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - # uses version from "packageManager" field in package.json - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - cache: pnpm - node-version-file: '.node-version' - - - name: Install Packages - run: | - pnpm install --frozen-lockfile - env: - CYPRESS_CACHE_FOLDER: .cache/Cypress - - - name: Run Build - run: pnpm run build - - - name: Upload Mermaid Build as Artifact - uses: actions/upload-artifact@v4 - with: - name: mermaid-build - path: packages/mermaid/dist - - - name: Upload Mermaid Mindmap Build as Artifact - uses: actions/upload-artifact@v4 - with: - name: mermaid-mindmap-build - path: packages/mermaid-mindmap/dist diff --git a/.github/workflows/check-readme-in-sync.yml b/.github/workflows/check-readme-in-sync.yml index ad6df66b5..5c940c087 100644 --- a/.github/workflows/check-readme-in-sync.yml +++ b/.github/workflows/check-readme-in-sync.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Check for difference in README.md and docs/README.md run: | diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml deleted file mode 100644 index 012fbf19d..000000000 --- a/.github/workflows/checks.yml +++ /dev/null @@ -1,26 +0,0 @@ -on: - push: - merge_group: - pull_request: - types: - - opened - - synchronize - - ready_for_review - -name: Static analysis on Test files - -jobs: - check-tests: - runs-on: ubuntu-latest - name: check tests - if: github.repository_owner == 'mermaid-js' - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - uses: testomatio/check-tests@stable - with: - framework: cypress - tests: './cypress/e2e/**/**.spec.js' - token: ${{ secrets.GITHUB_TOKEN }} - has-tests-label: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 764ec598c..65962ce64 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -11,6 +11,9 @@ on: - synchronize - ready_for_review +permissions: # added using https://github.com/step-security/secure-repo + contents: read + jobs: analyze: name: Analyze @@ -29,11 +32,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 with: config-file: ./.github/codeql/codeql-config.yml languages: ${{ matrix.language }} @@ -45,7 +48,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v3 + uses: github/codeql-action/autobuild@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -59,4 +62,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 0d4a01360..521735e6e 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -15,6 +15,6 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' - uses: actions/checkout@v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: 'Dependency Review' - uses: actions/dependency-review-action@v4 + uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4 diff --git a/.github/workflows/e2e-applitools.yml b/.github/workflows/e2e-applitools.yml index 5e5407a23..6da65afe5 100644 --- a/.github/workflows/e2e-applitools.yml +++ b/.github/workflows/e2e-applitools.yml @@ -11,6 +11,8 @@ on: default: master description: 'Parent branch to use for PRs' +concurrency: ${{ github.workflow }}-${{ github.ref }} + permissions: contents: read @@ -30,13 +32,13 @@ jobs: run: | echo "::error,title=Not using Applitools::APPLITOOLS_API_KEY is empty, disabling Applitools for this run." - - uses: actions/checkout@v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 # uses version from "packageManager" field in package.json - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version-file: '.node-version' @@ -52,7 +54,7 @@ jobs: APPLITOOLS_SERVER_URL: 'https://eyesapi.applitools.com' - name: Cypress run - uses: cypress-io/github-action@v4 + uses: cypress-io/github-action@d79d2d530a66e641eb4a5f227e13bc985c60b964 # v4.2.2 id: cypress with: start: pnpm run dev diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 2600b3fb8..2b91d078e 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -2,11 +2,15 @@ name: E2E on: push: - branches-ignore: - - 'gh-readonly-queue/**' + branches: + - develop + - master + - release/** pull_request: merge_group: +concurrency: ${{ github.workflow }}-${{ github.ref }} + permissions: contents: read @@ -32,15 +36,15 @@ jobs: image: cypress/browsers:node-20.11.0-chrome-121.0.6167.85-1-ff-120.0-edge-121.0.2277.83-1 options: --user 1001 steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version-file: '.node-version' - name: Cache snapshots id: cache-snapshot - uses: actions/cache@v4 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: save-always: true path: ./cypress/snapshots @@ -49,13 +53,13 @@ jobs: # If a snapshot for a given Hash is not found, we checkout that commit, run the tests and cache the snapshots. - name: Switch to base branch if: ${{ steps.cache-snapshot.outputs.cache-hit != 'true' }} - uses: actions/checkout@v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: ref: ${{ env.targetHash }} - name: Install dependencies if: ${{ steps.cache-snapshot.outputs.cache-hit != 'true' }} - uses: cypress-io/github-action@v6 + uses: cypress-io/github-action@df7484c5ba85def7eef30db301afa688187bc378 # v6.7.2 with: # just perform install runTests: false @@ -78,26 +82,26 @@ jobs: matrix: containers: [1, 2, 3, 4] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 # uses version from "packageManager" field in package.json - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version-file: '.node-version' # These cached snapshots are downloaded, providing the reference snapshots. - name: Cache snapshots id: cache-snapshot - uses: actions/cache/restore@v4 + uses: actions/cache/restore@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: ./cypress/snapshots key: ${{ runner.os }}-snapshots-${{ env.targetHash }} - name: Install dependencies - uses: cypress-io/github-action@v6 + uses: cypress-io/github-action@df7484c5ba85def7eef30db301afa688187bc378 # v6.7.2 with: runTests: false @@ -113,7 +117,7 @@ jobs: # Install NPM dependencies, cache them correctly # and run all Cypress tests - name: Cypress run - uses: cypress-io/github-action@v6 + uses: cypress-io/github-action@df7484c5ba85def7eef30db301afa688187bc378 # v6.7.2 id: cypress # If CYPRESS_RECORD_KEY is set, run in parallel on all containers # Otherwise (e.g. if running from fork), we run on a single container only @@ -137,7 +141,7 @@ jobs: ARGOS_PARALLEL_INDEX: ${{ matrix.containers }} - name: Upload Coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 # 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: diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index 129bd62b6..87a6e958b 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -4,11 +4,17 @@ on: issues: types: [opened] +permissions: # added using https://github.com/step-security/secure-repo + contents: read + jobs: triage: + permissions: + issues: write # for andymckay/labeler to label issues + pull-requests: write # for andymckay/labeler to label PRs runs-on: ubuntu-latest steps: - - uses: andymckay/labeler@1.0.4 + - uses: andymckay/labeler@e6c4322d0397f3240f0e7e30a33b5c5df2d39e90 # 1.0.4 with: repo-token: '${{ secrets.GITHUB_TOKEN }}' add-labels: 'Status: Triage' diff --git a/.github/workflows/link-checker.yml b/.github/workflows/link-checker.yml index bf54d7df2..0a2b74dfe 100644 --- a/.github/workflows/link-checker.yml +++ b/.github/workflows/link-checker.yml @@ -19,6 +19,9 @@ on: # * is a special character in YAML so you have to quote this string - cron: '30 8 * * *' +permissions: # added using https://github.com/step-security/secure-repo + contents: read + jobs: link-checker: runs-on: ubuntu-latest @@ -26,17 +29,17 @@ jobs: # lychee only uses the GITHUB_TOKEN to avoid rate-limiting contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Restore lychee cache - uses: actions/cache@v4 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: .lycheecache key: cache-lychee-${{ github.sha }} restore-keys: cache-lychee- - name: Link Checker - uses: lycheeverse/lychee-action@v1.9.3 + uses: lycheeverse/lychee-action@c053181aa0c3d17606addfe97a9075a32723548a # v1.9.3 with: args: >- --config .github/lychee.toml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 632cd6ddc..9cb58f0ea 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -4,12 +4,10 @@ on: push: merge_group: pull_request: - types: - - opened - - synchronize - - ready_for_review workflow_dispatch: +concurrency: ${{ github.workflow }}-${{ github.ref }} + permissions: contents: write @@ -17,13 +15,13 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 # uses version from "packageManager" field in package.json - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: pnpm node-version-file: '.node-version' @@ -82,3 +80,10 @@ jobs: working-directory: ./packages/mermaid continue-on-error: ${{ github.event_name == 'push' }} run: pnpm run docs:verify + + - uses: testomatio/check-tests@stable + with: + framework: cypress + tests: './cypress/e2e/**/**.spec.js' + token: ${{ secrets.GITHUB_TOKEN }} + has-tests-label: true diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index 096590346..c9faaa062 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -22,7 +22,7 @@ jobs: pull-requests: write # write permission is required to label PRs steps: - name: Label PR - uses: release-drafter/release-drafter@v6 + uses: release-drafter/release-drafter@3f0f87098bd6b5c5b9a36d49c41d998ea58f9348 # v6.0.0 with: config-name: pr-labeler.yml disable-autolabeler: false diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 4ff5f4117..ecb411b5c 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -23,12 +23,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: pnpm node-version-file: '.node-version' @@ -37,13 +37,13 @@ jobs: run: pnpm install --frozen-lockfile - name: Setup Pages - uses: actions/configure-pages@v4 + uses: actions/configure-pages@1f0c5cde4bc74cd7e1254d0cb4de8d49e9068c7d # v4.0.0 - name: Run Build run: pnpm --filter mermaid run docs:build:vitepress - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 with: path: packages/mermaid/src/vitepress/.vitepress/dist @@ -56,4 +56,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 diff --git a/.github/workflows/release-draft.yml b/.github/workflows/release-draft.yml deleted file mode 100644 index 657bc767a..000000000 --- a/.github/workflows/release-draft.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Draft Release - -on: - push: - branches: - - master - -permissions: - contents: read - -jobs: - draft-release: - runs-on: ubuntu-latest - permissions: - contents: write # write permission is required to create a GitHub release - pull-requests: read # required to read PR titles/labels - steps: - - name: Draft Release - uses: release-drafter/release-drafter@v6 - with: - disable-autolabeler: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-preview-publish.yml b/.github/workflows/release-preview-publish.yml index 91e3ac981..a9332d9a1 100644 --- a/.github/workflows/release-preview-publish.yml +++ b/.github/workflows/release-preview-publish.yml @@ -9,14 +9,14 @@ jobs: publish-preview: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: pnpm node-version-file: '.node-version' diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml deleted file mode 100644 index 4dcf709c0..000000000 --- a/.github/workflows/release-publish.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Publish release - -on: - release: - types: [published] - -jobs: - publish: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: fregante/setup-git-user@v2 - - - uses: pnpm/action-setup@v4 - # uses version from "packageManager" field in package.json - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - cache: pnpm - node-version-file: '.node-version' - - - name: Install Packages - run: | - pnpm install --frozen-lockfile - npm i json --global - env: - CYPRESS_CACHE_FOLDER: .cache/Cypress - - - name: Prepare release - run: | - VERSION=${GITHUB_REF:10} - echo "Preparing release $VERSION" - git checkout -t origin/release/$VERSION - npm version --no-git-tag-version --allow-same-version $VERSION - git add package.json - git commit -nm "Bump version $VERSION" - git checkout -t origin/master - git merge -m "Release $VERSION" --no-ff release/$VERSION - git push --no-verify - - - name: Publish - run: | - npm set //registry.npmjs.org/:_authToken $NPM_TOKEN - npm publish - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..91153084e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,44 @@ +name: Release + +on: + push: + branches: + - master + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +permissions: # added using https://github.com/step-security/secure-repo + contents: read + +jobs: + release: + permissions: + contents: write # for changesets/action to push to the repo + pull-requests: write # for changesets/action to create PRs + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + + - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 + + - name: Setup Node.js + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 + with: + cache: pnpm + node-version-file: '.node-version' + + - name: Install Packages + run: pnpm install --frozen-lockfile + + - name: Create Release Pull Request or Publish to npm + id: changesets + uses: changesets/action@aba318e9165b45b7948c60273e0b72fce0a64eb9 # v1.4.7 + with: + version: pnpm changeset:version + publish: pnpm changeset:publish + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_CONFIG_PROVENANCE: true diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 000000000..0dee2e666 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,37 @@ +name: Scorecard supply-chain security +on: + branch_protection_rule: + push: + branches: + - develop + schedule: + - cron: 29 15 * * 0 +permissions: read-all +jobs: + analysis: + name: Scorecard analysis + permissions: + id-token: write + security-events: write + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + persist-credentials: false + - name: Run analysis + uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + - name: Upload artifact + uses: actions/upload-artifact@97a0fba1372883ab732affbe8f94b823f91727db # v3.pre.node20 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + - name: Upload to code-scanning + uses: github/codeql-action/upload-sarif@f0f3afee809481da311ca3a6ff1ff51d81dbeb24 # v3.26.4 + with: + sarif_file: results.sarif diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a0b284a68..375d5fada 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,13 +9,13 @@ jobs: unit-test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 # uses version from "packageManager" field in package.json - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: pnpm node-version-file: '.node-version' @@ -39,7 +39,7 @@ jobs: pnpm exec vitest run ./packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts --coverage - name: Upload Coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 # Run step only pushes to develop and pull_requests if: ${{ github.event_name == 'pull_request' || github.ref == 'refs/heads/develop' }} with: diff --git a/.github/workflows/unlock-reopened-issues.yml b/.github/workflows/unlock-reopened-issues.yml index 4c5379729..b854eeb4b 100644 --- a/.github/workflows/unlock-reopened-issues.yml +++ b/.github/workflows/unlock-reopened-issues.yml @@ -8,6 +8,6 @@ jobs: triage: runs-on: ubuntu-latest steps: - - uses: Dunning-Kruger/unlock-issues@v1 + - uses: Dunning-Kruger/unlock-issues@b06b7f7e5c3f2eaa1c6d5d89f40930e4d6d9699e # v1 with: repo-token: '${{ secrets.GITHUB_TOKEN }}' diff --git a/.github/workflows/update-browserlist.yml b/.github/workflows/update-browserlist.yml index f8f7696cd..1b26271f7 100644 --- a/.github/workflows/update-browserlist.yml +++ b/.github/workflows/update-browserlist.yml @@ -8,18 +8,18 @@ jobs: update-browser-list: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 - run: npx update-browserslist-db@latest - name: Commit changes - uses: EndBug/add-and-commit@v9 + uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4 with: author_name: ${{ github.actor }} author_email: ${{ github.actor }}@users.noreply.github.com message: 'chore: update browsers list' push: false - name: Create Pull Request - uses: peter-evans/create-pull-request@v6 + uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0 with: branch: update-browserslist title: Update Browserslist diff --git a/README.md b/README.md index 8d5eebfeb..4a092f137 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ Try Live Editor previews of future releases: diff --git a/cypress/integration/rendering/flowchart-handDrawn.spec.js b/cypress/integration/rendering/flowchart-handDrawn.spec.js index d2e3edde0..10d6dde87 100644 --- a/cypress/integration/rendering/flowchart-handDrawn.spec.js +++ b/cypress/integration/rendering/flowchart-handDrawn.spec.js @@ -12,7 +12,7 @@ describe('Flowchart HandDrawn', () => { `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, flowchart: { htmlLabels: false }, fontFamily: 'courier', } @@ -30,7 +30,7 @@ describe('Flowchart HandDrawn', () => { `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, flowchart: { htmlLabels: true }, fontFamily: 'courier', } @@ -47,7 +47,7 @@ describe('Flowchart HandDrawn', () => { C -->|Two| E[iPhone] C -->|Three| F[Car] `, - { look: 'handDrawn', handDrawnSeed: 0, fontFamily: 'courier' } + { look: 'handDrawn', handDrawnSeed: 1, fontFamily: 'courier' } ); }); @@ -62,7 +62,7 @@ describe('Flowchart HandDrawn', () => { C -->|Two| E[\\iPhone\\] C -->|Three| F[Car] `, - { look: 'handDrawn', handDrawnSeed: 0, fontFamily: 'courier' } + { look: 'handDrawn', handDrawnSeed: 1, fontFamily: 'courier' } ); }); @@ -78,7 +78,7 @@ describe('Flowchart HandDrawn', () => { classDef processHead fill:#888888,color:white,font-weight:bold,stroke-width:3px,stroke:#001f3f class 1A,1B,D,E processHead `, - { look: 'handDrawn', handDrawnSeed: 0, fontFamily: 'courier' } + { look: 'handDrawn', handDrawnSeed: 1, fontFamily: 'courier' } ); }); @@ -107,7 +107,7 @@ describe('Flowchart HandDrawn', () => { 35(SAM.CommonFA.PopulationFME)-->39(SAM.CommonFA.ChargeDetails) 36(SAM.CommonFA.PremetricCost)-->39(SAM.CommonFA.ChargeDetails) `, - { look: 'handDrawn', handDrawnSeed: 0, fontFamily: 'courier' } + { look: 'handDrawn', handDrawnSeed: 1, fontFamily: 'courier' } ); }); @@ -178,7 +178,7 @@ describe('Flowchart HandDrawn', () => { 9a072290_1ec3_e711_8c5a_005056ad0002-->d6072290_1ec3_e711_8c5a_005056ad0002 9a072290_1ec3_e711_8c5a_005056ad0002-->71082290_1ec3_e711_8c5a_005056ad0002 `, - { look: 'handDrawn', handDrawnSeed: 0, fontFamily: 'courier' } + { look: 'handDrawn', handDrawnSeed: 1, fontFamily: 'courier' } ); }); @@ -187,7 +187,7 @@ describe('Flowchart HandDrawn', () => { ` graph TB;subgraph "number as labels";1;end; `, - { look: 'handDrawn', handDrawnSeed: 0, fontFamily: 'courier' } + { look: 'handDrawn', handDrawnSeed: 1, fontFamily: 'courier' } ); }); @@ -199,7 +199,7 @@ describe('Flowchart HandDrawn', () => { a1-->a2 end `, - { look: 'handDrawn', handDrawnSeed: 0, fontFamily: 'courier' } + { look: 'handDrawn', handDrawnSeed: 1, fontFamily: 'courier' } ); }); @@ -211,7 +211,7 @@ describe('Flowchart HandDrawn', () => { a1-->a2 end `, - { look: 'handDrawn', handDrawnSeed: 0, fontFamily: 'courier' } + { look: 'handDrawn', handDrawnSeed: 1, fontFamily: 'courier' } ); }); @@ -246,7 +246,7 @@ describe('Flowchart HandDrawn', () => { style foo fill:#F99,stroke-width:2px,stroke:#F0F,color:darkred style bar fill:#999,stroke-width:10px,stroke:#0F0,color:blue `, - { look: 'handDrawn', handDrawnSeed: 0, fontFamily: 'courier' } + { look: 'handDrawn', handDrawnSeed: 1, fontFamily: 'courier' } ); }); @@ -348,7 +348,7 @@ describe('Flowchart HandDrawn', () => { sid-7CE72B24-E0C1-46D3-8132-8BA66BE05AA7-->sid-4DA958A0-26D9-4D47-93A7-70F39FD7D51A; sid-7CE72B24-E0C1-46D3-8132-8BA66BE05AA7-->sid-4FC27B48-A6F9-460A-A675-021F5854FE22; `, - { look: 'handDrawn', handDrawnSeed: 0, fontFamily: 'courier' } + { look: 'handDrawn', handDrawnSeed: 1, fontFamily: 'courier' } ); }); @@ -364,7 +364,7 @@ describe('Flowchart HandDrawn', () => { `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, listUrl: false, listId: 'color styling', fontFamily: 'courier', @@ -390,7 +390,7 @@ describe('Flowchart HandDrawn', () => { `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, listUrl: false, listId: 'color styling', fontFamily: 'courier', @@ -411,7 +411,7 @@ describe('Flowchart HandDrawn', () => { `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, flowchart: { htmlLabels: false }, fontFamily: 'courier', } @@ -435,7 +435,7 @@ describe('Flowchart HandDrawn', () => { `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, flowchart: { htmlLabels: false }, fontFamily: 'courier', } @@ -457,7 +457,7 @@ describe('Flowchart HandDrawn', () => { `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, flowchart: { htmlLabels: false }, fontFamily: 'courier', } @@ -471,7 +471,7 @@ describe('Flowchart HandDrawn', () => { `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, flowchart: { htmlLabels: false }, fontFamily: 'courier', } @@ -485,7 +485,7 @@ describe('Flowchart HandDrawn', () => { `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, flowchart: { htmlLabels: false }, fontFamily: 'courier', } @@ -500,7 +500,7 @@ describe('Flowchart HandDrawn', () => { `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, flowchart: { htmlLabels: false }, fontFamily: 'courier', } @@ -527,7 +527,7 @@ describe('Flowchart HandDrawn', () => { class A someclass;`, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, flowchart: { htmlLabels: false }, fontFamily: 'courier', } @@ -544,7 +544,7 @@ describe('Flowchart HandDrawn', () => { C -->|Two| E[iPhone] C -->|Three| F[fa:fa-car Car] `, - { look: 'handDrawn', handDrawnSeed: 0, flowchart: { nodeSpacing: 50 }, fontFamily: 'courier' } + { look: 'handDrawn', handDrawnSeed: 1, flowchart: { nodeSpacing: 50 }, fontFamily: 'courier' } ); }); @@ -560,7 +560,7 @@ describe('Flowchart HandDrawn', () => { `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, flowchart: { rankSpacing: '100' }, fontFamily: 'courier', } @@ -578,7 +578,7 @@ describe('Flowchart HandDrawn', () => { `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, flowchart: { htmlLabels: false }, fontFamily: 'courier', } @@ -603,7 +603,7 @@ describe('Flowchart HandDrawn', () => { click E "notes://do-your-thing/id" "other protocol test" click F "javascript:alert('test')" "script test" `, - { look: 'handDrawn', handDrawnSeed: 0, securityLevel: 'loose', fontFamily: 'courier' } + { look: 'handDrawn', handDrawnSeed: 1, securityLevel: 'loose', fontFamily: 'courier' } ); }); @@ -623,7 +623,7 @@ describe('Flowchart HandDrawn', () => { click B "index.html#link-clicked" "link test" click D testClick "click test" `, - { look: 'handDrawn', handDrawnSeed: 0, flowchart: { htmlLabels: true } } + { look: 'handDrawn', handDrawnSeed: 1, flowchart: { htmlLabels: true } } ); }); @@ -645,7 +645,7 @@ describe('Flowchart HandDrawn', () => { `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, flowchart: { htmlLabels: false }, fontFamily: 'courier', } @@ -664,7 +664,7 @@ describe('Flowchart HandDrawn', () => { class A myClass1 class D myClass2 `, - { look: 'handDrawn', handDrawnSeed: 0, flowchart: { htmlLabels: true } } + { look: 'handDrawn', handDrawnSeed: 1, flowchart: { htmlLabels: true } } ); }); @@ -682,7 +682,7 @@ describe('Flowchart HandDrawn', () => { `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, flowchart: { htmlLabels: false }, fontFamily: 'courier', } @@ -711,7 +711,7 @@ describe('Flowchart HandDrawn', () => { `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, flowchart: { htmlLabels: false }, fontFamily: 'courier', } @@ -728,7 +728,7 @@ describe('Flowchart HandDrawn', () => { `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, flowchart: { htmlLabels: false }, fontFamily: 'courier', } @@ -752,7 +752,7 @@ describe('Flowchart HandDrawn', () => { `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, flowchart: { htmlLabels: false }, fontFamily: 'courier', } @@ -769,7 +769,7 @@ describe('Flowchart HandDrawn', () => { C -->|Two| E[iPhone] C -->|Three| F[fa:fa-car Car] `, - { look: 'handDrawn', handDrawnSeed: 0, flowchart: { diagramPadding: 0 } } + { look: 'handDrawn', handDrawnSeed: 1, flowchart: { diagramPadding: 0 } } ); }); @@ -778,7 +778,7 @@ describe('Flowchart HandDrawn', () => { `graph TD A[Christmas] `, - { look: 'handDrawn', handDrawnSeed: 0 } + { look: 'handDrawn', handDrawnSeed: 1 } ); }); @@ -796,7 +796,7 @@ describe('Flowchart HandDrawn', () => { C -----> E4 C ======> E5 `, - { look: 'handDrawn', handDrawnSeed: 0 } + { look: 'handDrawn', handDrawnSeed: 1 } ); }); it('FDH36: should render escaped without html labels', () => { @@ -804,7 +804,7 @@ describe('Flowchart HandDrawn', () => { `graph TD a["Haiya"]-->b `, - { look: 'handDrawn', handDrawnSeed: 0, htmlLabels: false, flowchart: { htmlLabels: false } } + { look: 'handDrawn', handDrawnSeed: 1, htmlLabels: false, flowchart: { htmlLabels: false } } ); }); it('FDH37: should render non-escaped with html labels', () => { @@ -814,7 +814,7 @@ describe('Flowchart HandDrawn', () => { `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose', @@ -830,7 +830,7 @@ describe('Flowchart HandDrawn', () => { C -->|Two| E[iPhone] C -->|Three| F[fa:fa-car Car] `, - { look: 'handDrawn', handDrawnSeed: 0, flowchart: { useMaxWidth: true } } + { look: 'handDrawn', handDrawnSeed: 1, flowchart: { useMaxWidth: true } } ); cy.get('svg').should((svg) => { expect(svg).to.have.attr('width', '100%'); @@ -853,7 +853,7 @@ describe('Flowchart HandDrawn', () => { C -->|Two| E[iPhone] C -->|Three| F[fa:fa-car Car] `, - { look: 'handDrawn', handDrawnSeed: 0, flowchart: { useMaxWidth: false } } + { look: 'handDrawn', handDrawnSeed: 1, flowchart: { useMaxWidth: false } } ); cy.get('svg').should((svg) => { // const height = parseFloat(svg.attr('height')); @@ -874,7 +874,7 @@ describe('Flowchart HandDrawn', () => { `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose', @@ -904,7 +904,7 @@ describe('Flowchart HandDrawn', () => { `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose', @@ -919,7 +919,7 @@ graph TD `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose', @@ -937,7 +937,7 @@ graph TD `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose', @@ -977,7 +977,7 @@ graph TD `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose', @@ -999,7 +999,7 @@ graph TD `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose', @@ -1016,7 +1016,7 @@ graph TD `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose', @@ -1027,12 +1027,12 @@ graph TD imgSnapshotTest( ` graph TD - classDef default fill:#a34,stroke:#000,stroke-width:4px,color:#fff + classDef default fill:#a34,stroke:#000,stroke-width:4px,color:#fff hello --> default `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose', @@ -1051,7 +1051,7 @@ graph TD `, { look: 'handDrawn', - handDrawnSeed: 0, + handDrawnSeed: 1, flowchart: { htmlLabels: true }, securityLevel: 'loose', } diff --git a/cypress/integration/rendering/flowchart-v2.spec.js b/cypress/integration/rendering/flowchart-v2.spec.js index c2fd0b011..452cdb5a0 100644 --- a/cypress/integration/rendering/flowchart-v2.spec.js +++ b/cypress/integration/rendering/flowchart-v2.spec.js @@ -99,7 +99,7 @@ describe('Flowchart v2', () => { const style = svg.attr('style'); expect(style).to.match(/^max-width: [\d.]+px;$/); const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join('')); - expect(maxWidthValue).to.be.within(446 * 0.95 - 1, 446 * 1.05); + expect(maxWidthValue).to.be.within(417 * 0.95, 417 * 1.05); }); }); it('8: should render a flowchart when useMaxWidth is false', () => { @@ -118,7 +118,7 @@ describe('Flowchart v2', () => { const width = parseFloat(svg.attr('width')); // use within because the absolute value can be slightly different depending on the environment Âą5% // expect(height).to.be.within(446 * 0.95, 446 * 1.05); - expect(width).to.be.within(446 * 0.95 - 1, 446 * 1.05); + expect(width).to.be.within(417 * 0.95, 417 * 1.05); expect(svg).to.not.have.attr('style'); }); }); diff --git a/cypress/platform/flowchart-refactor.html b/cypress/platform/flowchart-refactor.html index 034e79a52..6d9ce423f 100644 --- a/cypress/platform/flowchart-refactor.html +++ b/cypress/platform/flowchart-refactor.html @@ -822,7 +822,7 @@ flowchart LR - \ No newline at end of file + diff --git a/cypress/platform/viewer.js b/cypress/platform/viewer.js index 0b480b8bc..77da253c2 100644 --- a/cypress/platform/viewer.js +++ b/cypress/platform/viewer.js @@ -1,7 +1,7 @@ -import mermaid from './mermaid.esm.mjs'; -import { layouts } from './mermaid-layout-elk.esm.mjs'; import externalExample from './mermaid-example-diagram.esm.mjs'; +import layouts from './mermaid-layout-elk.esm.mjs'; import zenUml from './mermaid-zenuml.esm.mjs'; +import mermaid from './mermaid.esm.mjs'; function b64ToUtf8(str) { return decodeURIComponent(escape(window.atob(str))); diff --git a/docs/config/setup/interfaces/mermaid.LayoutLoaderDefinition.md b/docs/config/setup/interfaces/mermaid.LayoutLoaderDefinition.md index cb8e1a00b..90a64187c 100644 --- a/docs/config/setup/interfaces/mermaid.LayoutLoaderDefinition.md +++ b/docs/config/setup/interfaces/mermaid.LayoutLoaderDefinition.md @@ -16,7 +16,7 @@ #### Defined in -[packages/mermaid/src/rendering-util/render.ts:9](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L9) +[packages/mermaid/src/rendering-util/render.ts:24](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L24) --- @@ -26,7 +26,7 @@ #### Defined in -[packages/mermaid/src/rendering-util/render.ts:8](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L8) +[packages/mermaid/src/rendering-util/render.ts:23](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L23) --- @@ -36,4 +36,4 @@ #### Defined in -[packages/mermaid/src/rendering-util/render.ts:7](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L7) +[packages/mermaid/src/rendering-util/render.ts:22](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L22) diff --git a/docs/config/setup/interfaces/mermaid.Mermaid.md b/docs/config/setup/interfaces/mermaid.Mermaid.md index a340c7a97..09fab149c 100644 --- a/docs/config/setup/interfaces/mermaid.Mermaid.md +++ b/docs/config/setup/interfaces/mermaid.Mermaid.md @@ -28,7 +28,7 @@ page. #### Defined in -[packages/mermaid/src/mermaid.ts:432](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L432) +[packages/mermaid/src/mermaid.ts:435](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L435) --- @@ -59,7 +59,7 @@ A graph definition key #### Defined in -[packages/mermaid/src/mermaid.ts:434](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L434) +[packages/mermaid/src/mermaid.ts:437](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L437) --- @@ -89,7 +89,7 @@ Use [initialize](mermaid.Mermaid.md#initialize) and [run](mermaid.Mermaid.md#run #### Defined in -[packages/mermaid/src/mermaid.ts:427](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L427) +[packages/mermaid/src/mermaid.ts:430](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L430) --- @@ -116,56 +116,13 @@ This function should be called before the run function. #### Defined in -[packages/mermaid/src/mermaid.ts:431](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L431) - ---- - -### internalHelpers - -â€ĸ **internalHelpers**: `Object` - -Internal helpers for mermaid - -**`Deprecated`** - -- This should not be used by external packages, as the definitions will change without notice. - -#### Type declaration - -| Name | Type | -| :--------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `common` | { `evaluate`: (`val?`: `string` \| `boolean`) => `boolean` ; `getMax`: (...`values`: `number`\[]) => `number` ; `getMin`: (...`values`: `number`\[]) => `number` ; `getRows`: (`s?`: `string`) => `string`\[] ; `getUrl`: (`useAbsolute`: `boolean`) => `string` ; `hasBreaks`: (`text`: `string`) => `boolean` ; `lineBreakRegex`: `RegExp` ; `removeScript`: (`txt`: `string`) => `string` ; `sanitizeText`: (`text`: `string`, `config`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => `string` ; `sanitizeTextOrArray`: (`a`: `string` \| `string`\[] \| `string`\[]\[], `config`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => `string` \| `string`\[] ; `splitBreaks`: (`text`: `string`) => `string`\[] } | -| `common.evaluate` | (`val?`: `string` \| `boolean`) => `boolean` | -| `common.getMax` | (...`values`: `number`\[]) => `number` | -| `common.getMin` | (...`values`: `number`\[]) => `number` | -| `common.getRows` | (`s?`: `string`) => `string`\[] | -| `common.getUrl` | (`useAbsolute`: `boolean`) => `string` | -| `common.hasBreaks` | (`text`: `string`) => `boolean` | -| `common.lineBreakRegex` | `RegExp` | -| `common.removeScript` | (`txt`: `string`) => `string` | -| `common.sanitizeText` | (`text`: `string`, `config`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => `string` | -| `common.sanitizeTextOrArray` | (`a`: `string` \| `string`\[] \| `string`\[]\[], `config`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => `string` \| `string`\[] | -| `common.splitBreaks` | (`text`: `string`) => `string`\[] | -| `getConfig` | () => [`MermaidConfig`](mermaid.MermaidConfig.md) | -| `insertCluster` | (`elem`: `any`, `node`: `any`) => `any` | -| `insertEdge` | (`elem`: `any`, `edge`: `any`, `clusterDb`: `any`, `diagramType`: `any`, `startNode`: `any`, `endNode`: `any`, `id`: `any`) => { `originalPath`: `any` ; `updatedPath`: `any` } | -| `insertEdgeLabel` | (`elem`: `any`, `edge`: `any`) => `Promise`<`any`> | -| `insertMarkers` | (`elem`: `any`, `markerArray`: `any`, `type`: `any`, `id`: `any`) => `void` | -| `insertNode` | (`elem`: `any`, `node`: `any`, `dir`: `any`) => `Promise`<`any`> | -| `interpolateToCurve` | (`interpolate`: `undefined` \| `string`, `defaultCurve`: `CurveFactory`) => `CurveFactory` | -| `labelHelper` | (`parent`: `any`, `node`: `any`, `_classes`: `any`) => `Promise`<{ `bbox`: `any` ; `halfPadding`: `number` ; `label`: `any` = labelEl; `shapeSvg`: `any` }> | -| `log` | `Record`<`LogLevel`, (...`data`: `any`\[]) => `void`(`message?`: `any`, ...`optionalParams`: `any`\[]) => `void`> | -| `positionEdgeLabel` | (`edge`: `any`, `paths`: `any`) => `void` | - -#### Defined in - -[packages/mermaid/src/mermaid.ts:439](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L439) +[packages/mermaid/src/mermaid.ts:434](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L434) --- ### mermaidAPI -â€ĸ **mermaidAPI**: `Readonly`<{ `defaultConfig`: [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.defaultConfig; `getConfig`: () => [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.getConfig; `getDiagramFromText`: (`text`: `string`, `metadata`: `Pick`<`DiagramMetadata`, `"title"`>) => `Promise`<`Diagram`> ; `getSiteConfig`: () => [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.getSiteConfig; `globalReset`: () => `void` ; `initialize`: (`options`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => `void` ; `parse`: (`text`: `string`, `parseOptions`: [`ParseOptions`](mermaid.ParseOptions.md) & { `suppressErrors`: `true` }) => `Promise`<[`ParseResult`](mermaid.ParseResult.md) | `false`>(`text`: `string`, `parseOptions?`: [`ParseOptions`](mermaid.ParseOptions.md)) => `Promise`<[`ParseResult`](mermaid.ParseResult.md)> ; `render`: (`id`: `string`, `text`: `string`, `svgContainingElement?`: `Element`) => `Promise`<[`RenderResult`](mermaid.RenderResult.md)> ; `reset`: () => `void` ; `setConfig`: (`conf`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.setConfig; `updateSiteConfig`: (`conf`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.updateSiteConfig }> +â€ĸ **mermaidAPI**: `Readonly`<{ `defaultConfig`: [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.defaultConfig; `getConfig`: () => [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.getConfig; `getDiagramFromText`: (`text`: `string`, `metadata`: `Pick`<`DiagramMetadata`, `"title"`>) => `Promise`<`Diagram`> ; `getSiteConfig`: () => [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.getSiteConfig; `globalReset`: () => `void` ; `initialize`: (`userOptions`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => `void` ; `parse`: (`text`: `string`, `parseOptions`: [`ParseOptions`](mermaid.ParseOptions.md) & { `suppressErrors`: `true` }) => `Promise`<[`ParseResult`](mermaid.ParseResult.md) | `false`>(`text`: `string`, `parseOptions?`: [`ParseOptions`](mermaid.ParseOptions.md)) => `Promise`<[`ParseResult`](mermaid.ParseResult.md)> ; `render`: (`id`: `string`, `text`: `string`, `svgContainingElement?`: `Element`) => `Promise`<[`RenderResult`](mermaid.RenderResult.md)> ; `reset`: () => `void` ; `setConfig`: (`conf`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.setConfig; `updateSiteConfig`: (`conf`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.updateSiteConfig }> **`Deprecated`** @@ -173,7 +130,7 @@ Use [parse](mermaid.Mermaid.md#parse) and [render](mermaid.Mermaid.md#render) in #### Defined in -[packages/mermaid/src/mermaid.ts:421](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L421) +[packages/mermaid/src/mermaid.ts:424](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L424) --- @@ -223,7 +180,7 @@ Error if the diagram is invalid and parseOptions.suppressErrors is false or not #### Defined in -[packages/mermaid/src/mermaid.ts:422](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L422) +[packages/mermaid/src/mermaid.ts:425](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L425) --- @@ -233,7 +190,7 @@ Error if the diagram is invalid and parseOptions.suppressErrors is false or not #### Defined in -[packages/mermaid/src/mermaid.ts:416](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L416) +[packages/mermaid/src/mermaid.ts:419](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L419) --- @@ -261,7 +218,7 @@ Used to register external diagram types. #### Defined in -[packages/mermaid/src/mermaid.ts:430](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L430) +[packages/mermaid/src/mermaid.ts:433](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L433) --- @@ -285,7 +242,7 @@ Used to register external diagram types. #### Defined in -[packages/mermaid/src/mermaid.ts:429](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L429) +[packages/mermaid/src/mermaid.ts:432](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L432) --- @@ -311,7 +268,7 @@ Used to register external diagram types. #### Defined in -[packages/mermaid/src/mermaid.ts:423](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L423) +[packages/mermaid/src/mermaid.ts:426](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L426) --- @@ -359,7 +316,7 @@ Renders the mermaid diagrams #### Defined in -[packages/mermaid/src/mermaid.ts:428](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L428) +[packages/mermaid/src/mermaid.ts:431](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L431) --- @@ -394,7 +351,7 @@ to it (eg. dart interop wrapper). (Initially there is no parseError member of me #### Defined in -[packages/mermaid/src/mermaid.ts:433](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L433) +[packages/mermaid/src/mermaid.ts:436](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L436) --- @@ -404,4 +361,4 @@ to it (eg. dart interop wrapper). (Initially there is no parseError member of me #### Defined in -[packages/mermaid/src/mermaid.ts:415](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L415) +[packages/mermaid/src/mermaid.ts:418](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L418) diff --git a/docs/config/setup/interfaces/mermaid.RenderOptions.md b/docs/config/setup/interfaces/mermaid.RenderOptions.md new file mode 100644 index 000000000..9319cb3b1 --- /dev/null +++ b/docs/config/setup/interfaces/mermaid.RenderOptions.md @@ -0,0 +1,19 @@ +> **Warning** +> +> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT. +> +> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/setup/interfaces/mermaid.RenderOptions.md](../../../../packages/mermaid/src/docs/config/setup/interfaces/mermaid.RenderOptions.md). + +# Interface: RenderOptions + +[mermaid](../modules/mermaid.md).RenderOptions + +## Properties + +### algorithm + +â€ĸ `Optional` **algorithm**: `string` + +#### Defined in + +[packages/mermaid/src/rendering-util/render.ts:8](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L8) diff --git a/docs/config/setup/interfaces/mermaid.RunOptions.md b/docs/config/setup/interfaces/mermaid.RunOptions.md index 6fccdc454..aae004d6d 100644 --- a/docs/config/setup/interfaces/mermaid.RunOptions.md +++ b/docs/config/setup/interfaces/mermaid.RunOptions.md @@ -18,7 +18,7 @@ The nodes to render. If this is set, `querySelector` will be ignored. #### Defined in -[packages/mermaid/src/mermaid.ts:45](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L45) +[packages/mermaid/src/mermaid.ts:48](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L48) --- @@ -44,7 +44,7 @@ A callback to call after each diagram is rendered. #### Defined in -[packages/mermaid/src/mermaid.ts:49](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L49) +[packages/mermaid/src/mermaid.ts:52](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L52) --- @@ -56,7 +56,7 @@ The query selector to use when finding elements to render. Default: `".mermaid"` #### Defined in -[packages/mermaid/src/mermaid.ts:41](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L41) +[packages/mermaid/src/mermaid.ts:44](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L44) --- @@ -68,4 +68,4 @@ If `true`, errors will be logged to the console, but not thrown. Default: `false #### Defined in -[packages/mermaid/src/mermaid.ts:53](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L53) +[packages/mermaid/src/mermaid.ts:56](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L56) diff --git a/docs/config/setup/modules/defaultConfig.md b/docs/config/setup/modules/defaultConfig.md index 0f0ace33c..0a3e15855 100644 --- a/docs/config/setup/modules/defaultConfig.md +++ b/docs/config/setup/modules/defaultConfig.md @@ -14,7 +14,7 @@ #### Defined in -[packages/mermaid/src/defaultConfig.ts:279](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L279) +[packages/mermaid/src/defaultConfig.ts:266](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L266) --- diff --git a/docs/config/setup/modules/mermaid.md b/docs/config/setup/modules/mermaid.md index ac1bbece0..bdaeb05e1 100644 --- a/docs/config/setup/modules/mermaid.md +++ b/docs/config/setup/modules/mermaid.md @@ -20,11 +20,22 @@ - [MermaidConfig](../interfaces/mermaid.MermaidConfig.md) - [ParseOptions](../interfaces/mermaid.ParseOptions.md) - [ParseResult](../interfaces/mermaid.ParseResult.md) +- [RenderOptions](../interfaces/mermaid.RenderOptions.md) - [RenderResult](../interfaces/mermaid.RenderResult.md) - [RunOptions](../interfaces/mermaid.RunOptions.md) ## Type Aliases +### InternalHelpers + +ÆŦ **InternalHelpers**: typeof `internalHelpers` + +#### Defined in + +[packages/mermaid/src/internals.ts:33](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/internals.ts#L33) + +--- + ### ParseErrorFunction ÆŦ **ParseErrorFunction**: (`err`: `string` | [`DetailedError`](../interfaces/mermaid.DetailedError.md) | `unknown`, `hash?`: `any`) => `void` @@ -48,6 +59,26 @@ [packages/mermaid/src/Diagram.ts:10](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/Diagram.ts#L10) +--- + +### SVG + +ÆŦ **SVG**: `d3.Selection`<`SVGSVGElement`, `unknown`, `Element` | `null`, `unknown`> + +#### Defined in + +[packages/mermaid/src/diagram-api/types.ts:130](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L130) + +--- + +### SVGGroup + +ÆŦ **SVGGroup**: `d3.Selection`<`SVGGElement`, `unknown`, `Element` | `null`, `unknown`> + +#### Defined in + +[packages/mermaid/src/diagram-api/types.ts:132](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L132) + ## Variables ### default @@ -56,4 +87,4 @@ #### Defined in -[packages/mermaid/src/mermaid.ts:442](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L442) +[packages/mermaid/src/mermaid.ts:440](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L440) diff --git a/docs/ecosystem/integrations-community.md b/docs/ecosystem/integrations-community.md index 6b9190a9d..75f20dd1a 100644 --- a/docs/ecosystem/integrations-community.md +++ b/docs/ecosystem/integrations-community.md @@ -56,8 +56,10 @@ To add an integration to this list, see the [Integrations - create page](./integ - [SVG diagram generator](https://github.com/SimonKenyonShepard/mermaidjs-github-svg-generator) - [GitLab](https://docs.gitlab.com/ee/user/markdown.html#diagrams-and-flowcharts) ✅ - [Mermaid Plugin for JetBrains IDEs](https://plugins.jetbrains.com/plugin/20146-mermaid) +- [MonsterWriter](https://www.monsterwriter.com/) ✅ - [Joplin](https://joplinapp.org) ✅ - [LiveBook](https://livebook.dev) ✅ +- [Slidev](https://sli.dev) ✅ - [Tuleap](https://docs.tuleap.org/user-guide/writing-in-tuleap.html#graphs) ✅ - [Mermaid Flow Visual Editor](https://www.mermaidflow.app) ✅ - [Mermerd](https://github.com/KarnerTh/mermerd) @@ -133,7 +135,7 @@ Communication tools and platforms ### Wikis - [DokuWiki](https://dokuwiki.org) - - [ComboStrap](https://combostrap.com/mermaid) + - [ComboStrap](https://combostrap.com/utility/create-diagram-with-mermaid-vh3ab9yj) - [Mermaid Plugin](https://www.dokuwiki.org/plugin:mermaid) - [Foswiki](https://foswiki.org) - [Mermaid Plugin](https://foswiki.org/Extensions/MermaidPlugin) diff --git a/docs/ecosystem/mermaid-chart.md b/docs/ecosystem/mermaid-chart.md index f1ca85af0..9b3440a0a 100644 --- a/docs/ecosystem/mermaid-chart.md +++ b/docs/ecosystem/mermaid-chart.md @@ -12,7 +12,7 @@ Try the Ultimate AI, Mermaid, and Visual Diagramming Suite by creating an accoun
-
Mermaid Chart - A smarter way to create diagrams | Product Hunt +Mermaid Chart - A smarter way to create diagrams | Product Hunt ## About diff --git a/docs/intro/syntax-reference.md b/docs/intro/syntax-reference.md index 00330f21d..f736840e6 100644 --- a/docs/intro/syntax-reference.md +++ b/docs/intro/syntax-reference.md @@ -83,3 +83,139 @@ Allows for the limited reconfiguration of a diagram just before it is rendered. ### [Theme Manipulation](../config/theming.md) An application of using Directives to change [Themes](../config/theming.md). `Theme` is a value within Mermaid's configuration that dictates the color scheme for diagrams. + +### Layout and look + +We've restructured how Mermaid renders diagrams, enabling new features like selecting layout and look. **Currently, this is supported for flowcharts and state diagrams**, with plans to extend support to all diagram types. + +### Selecting Diagram Looks + +Mermaid offers a variety of styles or “looks” for your diagrams, allowing you to tailor the visual appearance to match your specific needs or preferences. Whether you prefer a hand-drawn or classic style, you can easily customize your diagrams. + +**Available Looks:** + +``` +â€ĸ Hand-Drawn Look: For a more personal, creative touch, the hand-drawn look brings a sketch-like quality to your diagrams. This style is perfect for informal settings or when you want to add a bit of personality to your diagrams. +â€ĸ Classic Look: If you prefer the traditional Mermaid style, the classic look maintains the original appearance that many users are familiar with. It’s great for consistency across projects or when you want to keep the familiar aesthetic. +``` + +**How to Select a Look:** + +You can select a look by adding the look parameter in the metadata section of your Mermaid diagram code. Here’s an example: + +```mermaid-example +--- +config: + look: handDrawn + theme: neutral +--- +flowchart LR + A[Start] --> B{Decision} + B -->|Yes| C[Continue] + B -->|No| D[Stop] +``` + +```mermaid +--- +config: + look: handDrawn + theme: neutral +--- +flowchart LR + A[Start] --> B{Decision} + B -->|Yes| C[Continue] + B -->|No| D[Stop] +``` + +#### Selecting Layout Algorithms + +In addition to customizing the look of your diagrams, Mermaid Chart now allows you to choose different layout algorithms to better organize and present your diagrams, especially when dealing with more complex structures. The layout algorithm dictates how nodes and edges are arranged on the page. + +#### Supported Layout Algorithms: + +``` +â€ĸ Dagre (default): This is the classic layout algorithm that has been used in Mermaid for a long time. It provides a good balance of simplicity and visual clarity, making it ideal for most diagrams. +â€ĸ ELK: For those who need more sophisticated layout capabilities, especially when working with large or intricate diagrams, the ELK (Eclipse Layout Kernel) layout offers advanced options. It provides a more optimized arrangement, potentially reducing overlapping and improving readability. This is not included out the box but needs to be added when integrating mermaid for sites/applications that want to have elk support. +``` + +#### How to Select a Layout Algorithm: + +You can specify the layout algorithm directly in the metadata section of your Mermaid diagram code. Here’s an example: + +```mermaid-example +--- +config: + layout: elk + look: handDrawn + theme: dark +--- +flowchart TB + A[Start] --> B{Decision} + B -->|Yes| C[Continue] + B -->|No| D[Stop] +``` + +```mermaid +--- +config: + layout: elk + look: handDrawn + theme: dark +--- +flowchart TB + A[Start] --> B{Decision} + B -->|Yes| C[Continue] + B -->|No| D[Stop] +``` + +In this example, the `layout: elk` line configures the diagram to use the ELK layout algorithm, along with the hand drawn look and forest theme. + +#### Customizing ELK Layout: + +When using the ELK layout, you can further refine the diagram’s configuration, such as how nodes are placed and whether parallel edges should be combined: + +- To combine parallel edges, use mergeEdges: true | false. +- To configure node placement, use nodePlacementStrategy with the following options: + - SIMPLE + - NETWORK_SIMPLEX + - LINEAR_SEGMENTS + - BRANDES_KOEPF (default) + +**Example configuration:** + +``` +--- +config: + layout: elk + elk: + mergeEdges: true + nodePlacementStrategy: LINEAR_SEGMENTS +--- +flowchart LR + A[Start] --> B{Choose Path} + B -->|Option 1| C[Path 1] + B -->|Option 2| D[Path 2] + +#### Using Dagre Layout with Classic Look: +``` + +Another example: + +``` +--- +config: + layout: dagre + look: classic + theme: default +--- + +flowchart LR +A[Start] --> B{Choose Path} +B -->|Option 1| C[Path 1] +B -->|Option 2| D[Path 2] + +``` + +These options give you the flexibility to create diagrams that not only look great but are also arranged to best suit your data’s structure and flow. + +When integrating Mermaid, you can include look and layout configuration with the initialize call. This is also where you add the loading of elk. diff --git a/docs/news/blog.md b/docs/news/blog.md index 65fa9246e..372247b86 100644 --- a/docs/news/blog.md +++ b/docs/news/blog.md @@ -6,6 +6,48 @@ # Blog +## [Mermaid v11 is out!](https://www.mermaidchart.com/blog/posts/mermaid-v11/) + +23 August 2024 ¡ 2 mins + +Mermaid v11 introduces advanced layout options, new diagram types, and enhanced customization features, thanks to the incredible contributions from our community. + +## [Mermaid Innovation - Introducing New Looks for Mermaid Diagrams](https://www.mermaidchart.com/blog/posts/mermaid-innovation-introducing-new-looks-for-mermaid-diagrams/) + +6 August 2024 ¡3 mins + +Discover the fresh new and unique Neo and Hand-Drawn looks for Mermaid Diagrams, while still offering the classic look you love. + +## [The Mermaid Chart Plugin for Jira: A How-To User Guide](https://www.mermaidchart.com/blog/posts/the-mermaid-chart-plugin-for-jira-a-how-to-user-guide/) + +31 July 2024 ¡ 5 mins + +The Mermaid Chart plugin for Jira has arrived! + +## [Mermaid AI Is Here to Change the Game For Diagram Creation](https://www.mermaidchart.com/blog/posts/mermaid-ai-is-here-to-change-the-game-for-diagram-creation/) + +22 July 2024 ¡ 5 mins + +The Mermaid AI chat interface + +## [How to Make a Sequence Diagram with Mermaid Chart](https://www.mermaidchart.com/blog/posts/how-to-make-a-sequence-diagram-in-mermaid-chart-step-by-step-guide/) + +8 July 2024 ¡ 6 mins + +Sequence diagrams are important for communicating complex systems in a clear and concise manner. + +## [How to Use the New “Comments” Feature in Mermaid Chart](https://www.mermaidchart.com/blog/posts/how-to-use-the-new-comments-feature-in-mermaid-chart/) + +2 July 2024 ¡ 3 mins + +How to Use the New Comments Feature in Mermaid Chart + +## [How to Use the official Mermaid Chart for Confluence app](https://www.mermaidchart.com/blog/posts/how-to-use-the-official-mermaid-chart-for-confluence-app/) + +21 May 2024 ¡ 4 mins + +It doesn’t matter if you’re a data enthusiast, software engineer, or visual storyteller; our Confluence app can allow you to embed Mermaid Chart diagrams — and dynamically edit them — within your Confluence pages. + ## [How to Choose the Right Documentation Software](https://www.mermaidchart.com/blog/posts/how-to-choose-the-right-documentation-software/) 7 May 2024 ¡ 5 mins diff --git a/docs/syntax/gantt.md b/docs/syntax/gantt.md index 31bac5e29..cdaf0c2ac 100644 --- a/docs/syntax/gantt.md +++ b/docs/syntax/gantt.md @@ -172,7 +172,7 @@ The `title` is an _optional_ string to be displayed at the top of the Gantt char The `excludes` is an _optional_ attribute that accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays". These date will be marked on the graph, and be excluded from the duration calculation of tasks. Meaning that if there are excluded dates during a task interval, the number of 'skipped' days will be added to the end of the task to ensure the duration is as specified in the code. -#### Weekend (v\+) +#### Weekend (v\11.0.0+) When excluding weekends, it is possible to configure the weekends to be either Friday and Saturday or Saturday and Sunday. By default weekends are Saturday and Sunday. To define the weekend start day, there is an _optional_ attribute `weekend` that can be added in a new line followed by either `friday` or `saturday`. diff --git a/docs/syntax/gitgraph.md b/docs/syntax/gitgraph.md index 9403f2a33..340a31695 100644 --- a/docs/syntax/gitgraph.md +++ b/docs/syntax/gitgraph.md @@ -918,7 +918,7 @@ Usage example: commit ``` -### Bottom to Top (`BT:`) (v\+) +### Bottom to Top (`BT:`) (v11.0.0+) In `BT` (**Bottom-to-Top**) orientation, the commits run from bottom to top of the graph and branches are arranged side-by-side. diff --git a/docs/syntax/packet.md b/docs/syntax/packet.md index 3fe7b119e..5eab81910 100644 --- a/docs/syntax/packet.md +++ b/docs/syntax/packet.md @@ -4,7 +4,7 @@ > > ## Please edit the corresponding file in [/packages/mermaid/src/docs/syntax/packet.md](../../packages/mermaid/src/docs/syntax/packet.md). -# Packet Diagram (v\+) +# Packet Diagram (v11.0.0+) ## Introduction diff --git a/docs/syntax/sequenceDiagram.md b/docs/syntax/sequenceDiagram.md index 0f0d63213..435ac7583 100644 --- a/docs/syntax/sequenceDiagram.md +++ b/docs/syntax/sequenceDiagram.md @@ -208,18 +208,18 @@ Messages can be of two displayed either solid or with a dotted line. There are ten types of arrows currently supported: -| Type | Description | -| -------- | ------------------------------------------------------------------------ | -| `->` | Solid line without arrow | -| `-->` | Dotted line without arrow | -| `->>` | Solid line with arrowhead | -| `-->>` | Dotted line with arrowhead | -| `<<->>` | Solid line with bidirectional arrowheads (v\+) | -| `<<-->>` | Dotted line with bidirectional arrowheads (v\+) | -| `-x` | Solid line with a cross at the end | -| `--x` | Dotted line with a cross at the end. | -| `-)` | Solid line with an open arrow at the end (async) | -| `--)` | Dotted line with a open arrow at the end (async) | +| Type | Description | +| -------- | ---------------------------------------------------- | +| `->` | Solid line without arrow | +| `-->` | Dotted line without arrow | +| `->>` | Solid line with arrowhead | +| `-->>` | Dotted line with arrowhead | +| `<<->>` | Solid line with bidirectional arrowheads (v11.0.0+) | +| `<<-->>` | Dotted line with bidirectional arrowheads (v11.0.0+) | +| `-x` | Solid line with a cross at the end | +| `--x` | Dotted line with a cross at the end. | +| `-)` | Solid line with an open arrow at the end (async) | +| `--)` | Dotted line with a open arrow at the end (async) | ## Activations diff --git a/package.json b/package.json index 4e17d516c..f0045f7ae 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "10.2.4", "description": "Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.", "type": "module", - "packageManager": "pnpm@9.4.0+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a", + "packageManager": "pnpm@9.7.1+sha512.faf344af2d6ca65c4c5c8c2224ea77a81a5e8859cbc4e06b1511ddce2f0151512431dd19e6aff31f2c6a8f5f2aced9bd2273e1fed7dd4de1868984059d2c4247", "keywords": [ "diagram", "markdown", @@ -24,7 +24,9 @@ "dev": "tsx .esbuild/server.ts", "dev:vite": "tsx .vite/server.ts", "dev:coverage": "pnpm coverage:cypress:clean && VITE_COVERAGE=true pnpm dev:vite", - "release": "pnpm build", + "copy-readme": "cpy './README.*' ./packages/mermaid/ --cwd=.", + "changeset:version": "changeset version && pnpm build && pnpm --filter mermaid run docs:release-version && pnpm --filter mermaid run docs:build && git add --all", + "changeset:publish": "pnpm copy-readme && changeset publish", "lint": "eslint --quiet --stats --cache --cache-strategy content . && pnpm lint:jison && prettier --cache --check .", "lint:fix": "eslint --cache --cache-strategy content --fix . && prettier --write . && tsx scripts/fixCSpell.ts", "lint:jison": "tsx ./scripts/jison/lint.mts", @@ -40,7 +42,6 @@ "test": "pnpm lint && vitest run", "test:watch": "vitest --watch", "test:coverage": "vitest --coverage", - "prepublishOnly": "pnpm build && pnpm test", "prepare": "husky install && pnpm build", "pre-commit": "lint-staged" }, @@ -63,6 +64,8 @@ "devDependencies": { "@applitools/eyes-cypress": "^3.44.4", "@argos-ci/cypress": "^2.1.0", + "@changesets/changelog-github": "^0.5.0", + "@changesets/cli": "^2.27.7", "@cspell/eslint-plugin": "^8.8.4", "@cypress/code-coverage": "^3.12.30", "@eslint/js": "^9.4.0", @@ -82,6 +85,7 @@ "chokidar": "^3.6.0", "concurrently": "^8.2.2", "cors": "^2.8.5", + "cpy-cli": "^5.0.0", "cross-env": "^7.0.3", "cspell": "^8.6.0", "cypress": "^13.11.0", diff --git a/packages/mermaid-example-diagram/package.json b/packages/mermaid-example-diagram/package.json index b899e077b..28cb54b9e 100644 --- a/packages/mermaid-example-diagram/package.json +++ b/packages/mermaid-example-diagram/package.json @@ -1,6 +1,7 @@ { "name": "@mermaid-js/mermaid-example-diagram", "version": "9.3.0", + "private": true, "description": "Example of external diagram module for MermaidJS.", "module": "dist/mermaid-example-diagram.core.mjs", "types": "dist/detector.d.ts", @@ -18,9 +19,7 @@ "example", "mermaid" ], - "scripts": { - "prepublishOnly": "pnpm -w run build" - }, + "scripts": {}, "repository": { "type": "git", "url": "https://github.com/mermaid-js/mermaid" diff --git a/packages/mermaid-flowchart-elk/package.json b/packages/mermaid-flowchart-elk/package.json deleted file mode 100644 index dc3af195c..000000000 --- a/packages/mermaid-flowchart-elk/package.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "name": "@mermaid-js/flowchart-elk", - "version": "1.0.0-rc.1", - "description": "Flowchart plugin for mermaid with ELK layout", - "module": "dist/mermaid-flowchart-elk.core.mjs", - "types": "dist/packages/mermaid-flowchart-elk/src/detector.d.ts", - "type": "module", - "exports": { - ".": { - "import": "./dist/mermaid-flowchart-elk.core.mjs", - "types": "./dist/packages/mermaid-flowchart-elk/src/detector.d.ts" - }, - "./*": "./*" - }, - "keywords": [ - "diagram", - "markdown", - "flowchart", - "elk", - "mermaid" - ], - "scripts": { - "prepublishOnly": "pnpm -w run build" - }, - "repository": { - "type": "git", - "url": "https://github.com/mermaid-js/mermaid" - }, - "author": "Knut Sveidqvist", - "license": "MIT", - "dependencies": { - "d3": "^7.9.0", - "dagre-d3-es": "7.0.10", - "elkjs": "^0.9.2", - "khroma": "^2.1.0" - }, - "devDependencies": { - "concurrently": "^8.2.2", - "mermaid": "workspace:^", - "rimraf": "^5.0.5" - }, - "files": [ - "dist" - ] -} diff --git a/packages/mermaid-flowchart-elk/src/detector.spec.ts b/packages/mermaid-flowchart-elk/src/detector.spec.ts deleted file mode 100644 index 94ac22d26..000000000 --- a/packages/mermaid-flowchart-elk/src/detector.spec.ts +++ /dev/null @@ -1,75 +0,0 @@ -import plugin from './detector.js'; -import { describe, it } from 'vitest'; - -const { detector } = plugin; - -describe('flowchart-elk detector', () => { - it('should fail for dagre-d3', () => { - expect( - detector('flowchart', { - flowchart: { - defaultRenderer: 'dagre-d3', - }, - }) - ).toBe(false); - }); - it('should fail for dagre-wrapper', () => { - expect( - detector('flowchart', { - flowchart: { - defaultRenderer: 'dagre-wrapper', - }, - }) - ).toBe(false); - }); - it('should succeed for elk', () => { - expect( - detector('flowchart', { - flowchart: { - defaultRenderer: 'elk', - }, - }) - ).toBe(true); - expect( - detector('graph', { - flowchart: { - defaultRenderer: 'elk', - }, - }) - ).toBe(true); - }); - - // The error from the issue was reproduced with mindmap, so this is just an example - // what matters is the keyword somewhere inside graph definition - it('should check only the beginning of the line in search of keywords', () => { - expect( - detector('mindmap ["Descendant node in flowchart"]', { - flowchart: { - defaultRenderer: 'elk', - }, - }) - ).toBe(false); - - expect( - detector('mindmap ["Descendant node in graph"]', { - flowchart: { - defaultRenderer: 'elk', - }, - }) - ).toBe(false); - }); - - it('should detect flowchart-elk', () => { - expect(detector('flowchart-elk')).toBe(true); - }); - - it('should not detect class with defaultRenderer set to elk', () => { - expect( - detector('class', { - flowchart: { - defaultRenderer: 'elk', - }, - }) - ).toBe(false); - }); -}); diff --git a/packages/mermaid-flowchart-elk/src/detector.ts b/packages/mermaid-flowchart-elk/src/detector.ts deleted file mode 100644 index 3b168bd61..000000000 --- a/packages/mermaid-flowchart-elk/src/detector.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { - ExternalDiagramDefinition, - DiagramDetector, - DiagramLoader, -} from '../../mermaid/src/diagram-api/types.js'; - -const id = 'flowchart-elk'; - -const detector: DiagramDetector = (txt, config): boolean => { - if ( - // If diagram explicitly states flowchart-elk - /^\s*flowchart-elk/.test(txt) || - // If a flowchart/graph diagram has their default renderer set to elk - (/^\s*(flowchart|graph)/.test(txt) && config?.flowchart?.defaultRenderer === 'elk') - ) { - return true; - } - return false; -}; - -const loader: DiagramLoader = async () => { - const { diagram } = await import('./diagram-definition.js'); - return { id, diagram }; -}; - -const plugin: ExternalDiagramDefinition = { - id, - detector, - loader, -}; - -export default plugin; diff --git a/packages/mermaid-flowchart-elk/src/diagram-definition.ts b/packages/mermaid-flowchart-elk/src/diagram-definition.ts deleted file mode 100644 index 4a6b5b076..000000000 --- a/packages/mermaid-flowchart-elk/src/diagram-definition.ts +++ /dev/null @@ -1,12 +0,0 @@ -// @ts-ignore: JISON typing missing -import parser from '../../mermaid/src/diagrams/flowchart/parser/flow.jison'; -import db from '../../mermaid/src/diagrams/flowchart/flowDb.js'; -import styles from '../../mermaid/src/diagrams/flowchart/styles.js'; -import renderer from './flowRenderer-elk.js'; - -export const diagram = { - db, - renderer, - parser, - styles, -}; diff --git a/packages/mermaid-flowchart-elk/src/flowRenderer-elk.js b/packages/mermaid-flowchart-elk/src/flowRenderer-elk.js deleted file mode 100644 index fc540073a..000000000 --- a/packages/mermaid-flowchart-elk/src/flowRenderer-elk.js +++ /dev/null @@ -1,888 +0,0 @@ -import { select, line, curveLinear } from 'd3'; -import { insertNode } from '../../mermaid/src/dagre-wrapper/nodes.js'; -import insertMarkers from '../../mermaid/src/dagre-wrapper/markers.js'; -import { insertEdgeLabel } from '../../mermaid/src/dagre-wrapper/edges.js'; -import { findCommonAncestor } from './render-utils.js'; -import { labelHelper } from '../../mermaid/src/dagre-wrapper/shapes/util.js'; -import { getConfig } from '../../mermaid/src/config.js'; -import { log } from '../../mermaid/src/logger.js'; -import utils from '../../mermaid/src/utils.js'; -import { setupGraphViewbox } from '../../mermaid/src/setupGraphViewbox.js'; -import common from '../../mermaid/src/diagrams/common/common.js'; -import { interpolateToCurve, getStylesFromArray } from '../../mermaid/src/utils.js'; -import ELK from 'elkjs/lib/elk.bundled.js'; -import { getLineFunctionsWithOffset } from '../../mermaid/src/utils/lineWithOffset.js'; -import { addEdgeMarkers } from '../../mermaid/src/dagre-wrapper/edgeMarker.js'; - -const elk = new ELK(); - -let portPos = {}; - -const conf = {}; -export const setConf = function (cnf) { - const keys = Object.keys(cnf); - for (const key of keys) { - conf[key] = cnf[key]; - } -}; - -let nodeDb = {}; - -// /** -// * Function that adds the vertices found during parsing to the graph to be rendered. -// * -// * @param vert Object containing the vertices. -// * @param g The graph that is to be drawn. -// * @param svgId -// * @param root -// * @param doc -// * @param diagObj -// */ -export const addVertices = async function (vert, svgId, root, doc, diagObj, parentLookupDb, graph) { - const svg = root.select(`[id="${svgId}"]`); - const nodes = svg.insert('g').attr('class', 'nodes'); - const keys = [...vert.keys()]; - - // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition - await Promise.all( - keys.map(async function (id) { - const vertex = vert.get(id); - - /** - * Variable for storing the classes for the vertex - * - * @type {string} - */ - let classStr = 'default'; - if (vertex.classes.length > 0) { - classStr = vertex.classes.join(' '); - } - classStr = classStr + ' flowchart-label'; - const styles = getStylesFromArray(vertex.styles); - - // Use vertex id as text in the box if no text is provided by the graph definition - let vertexText = vertex.text !== undefined ? vertex.text : vertex.id; - - // We create a SVG label, either by delegating to addHtmlLabel or manually - const labelData = { width: 0, height: 0 }; - - const ports = [ - { - id: vertex.id + '-west', - layoutOptions: { - 'port.side': 'WEST', - }, - }, - { - id: vertex.id + '-east', - layoutOptions: { - 'port.side': 'EAST', - }, - }, - { - id: vertex.id + '-south', - layoutOptions: { - 'port.side': 'SOUTH', - }, - }, - { - id: vertex.id + '-north', - layoutOptions: { - 'port.side': 'NORTH', - }, - }, - ]; - - let radius = 0; - let _shape = ''; - let layoutOptions = {}; - // Set the shape based parameters - switch (vertex.type) { - case 'round': - radius = 5; - _shape = 'rect'; - break; - case 'square': - _shape = 'rect'; - break; - case 'diamond': - _shape = 'question'; - layoutOptions = { - portConstraints: 'FIXED_SIDE', - }; - break; - case 'hexagon': - _shape = 'hexagon'; - break; - case 'odd': - _shape = 'rect_left_inv_arrow'; - break; - case 'lean_right': - _shape = 'lean_right'; - break; - case 'lean_left': - _shape = 'lean_left'; - break; - case 'trapezoid': - _shape = 'trapezoid'; - break; - case 'inv_trapezoid': - _shape = 'inv_trapezoid'; - break; - case 'odd_right': - _shape = 'rect_left_inv_arrow'; - break; - case 'circle': - _shape = 'circle'; - break; - case 'ellipse': - _shape = 'ellipse'; - break; - case 'stadium': - _shape = 'stadium'; - break; - case 'subroutine': - _shape = 'subroutine'; - break; - case 'cylinder': - _shape = 'cylinder'; - break; - case 'group': - _shape = 'rect'; - break; - case 'doublecircle': - _shape = 'doublecircle'; - break; - default: - _shape = 'rect'; - } - - // Add the node - const node = { - labelStyle: styles.labelStyle, - shape: _shape, - labelText: vertexText, - labelType: vertex.labelType, - rx: radius, - ry: radius, - class: classStr, - style: styles.style, - id: vertex.id, - link: vertex.link, - linkTarget: vertex.linkTarget, - tooltip: diagObj.db.getTooltip(vertex.id) || '', - domId: diagObj.db.lookUpDomId(vertex.id), - haveCallback: vertex.haveCallback, - width: vertex.type === 'group' ? 500 : undefined, - dir: vertex.dir, - type: vertex.type, - props: vertex.props, - padding: getConfig().flowchart.padding, - }; - let boundingBox; - let nodeEl; - - // Add the element to the DOM - if (node.type !== 'group') { - nodeEl = await insertNode(nodes, node, vertex.dir); - boundingBox = nodeEl.node().getBBox(); - } else { - const { shapeSvg, bbox } = await labelHelper(nodes, node, undefined, true); - labelData.width = bbox.width; - labelData.wrappingWidth = getConfig().flowchart.wrappingWidth; - labelData.height = bbox.height; - labelData.labelNode = shapeSvg.node(); - node.labelData = labelData; - } - // const { shapeSvg, bbox } = await labelHelper(svg, node, undefined, true); - - const data = { - id: vertex.id, - ports: vertex.type === 'diamond' ? ports : [], - // labelStyle: styles.labelStyle, - // shape: _shape, - layoutOptions, - labelText: vertexText, - labelData, - // labels: [{ text: vertexText }], - // rx: radius, - // ry: radius, - // class: classStr, - // style: styles.style, - // link: vertex.link, - // linkTarget: vertex.linkTarget, - // tooltip: diagObj.db.getTooltip(vertex.id) || '', - domId: diagObj.db.lookUpDomId(vertex.id), - // haveCallback: vertex.haveCallback, - width: boundingBox?.width, - height: boundingBox?.height, - // dir: vertex.dir, - type: vertex.type, - // props: vertex.props, - // padding: getConfig().flowchart.padding, - // boundingBox, - el: nodeEl, - parent: parentLookupDb.parentById[vertex.id], - }; - // if (!Object.keys(parentLookupDb.childrenById).includes(vertex.id)) { - // graph.children.push({ - // ...data, - // }); - // } - nodeDb[node.id] = data; - // log.trace('setNode', { - // labelStyle: styles.labelStyle, - // shape: _shape, - // labelText: vertexText, - // rx: radius, - // ry: radius, - // class: classStr, - // style: styles.style, - // id: vertex.id, - // domId: diagObj.db.lookUpDomId(vertex.id), - // width: vertex.type === 'group' ? 500 : undefined, - // type: vertex.type, - // dir: vertex.dir, - // props: vertex.props, - // padding: getConfig().flowchart.padding, - // parent: parentLookupDb.parentById[vertex.id], - // }); - }) - ); - return graph; -}; - -const getNextPosition = (position, edgeDirection, graphDirection) => { - const portPos = { - TB: { - in: { - north: 'north', - }, - out: { - south: 'west', - west: 'east', - east: 'south', - }, - }, - LR: { - in: { - west: 'west', - }, - out: { - east: 'south', - south: 'north', - north: 'east', - }, - }, - RL: { - in: { - east: 'east', - }, - out: { - west: 'north', - north: 'south', - south: 'west', - }, - }, - BT: { - in: { - south: 'south', - }, - out: { - north: 'east', - east: 'west', - west: 'north', - }, - }, - }; - portPos.TD = portPos.TB; - return portPos[graphDirection][edgeDirection][position]; - // return 'south'; -}; - -const getNextPort = (node, edgeDirection, graphDirection) => { - log.info('getNextPort', { node, edgeDirection, graphDirection }); - if (!portPos[node]) { - switch (graphDirection) { - case 'TB': - case 'TD': - portPos[node] = { - inPosition: 'north', - outPosition: 'south', - }; - break; - case 'BT': - portPos[node] = { - inPosition: 'south', - outPosition: 'north', - }; - break; - case 'RL': - portPos[node] = { - inPosition: 'east', - outPosition: 'west', - }; - break; - case 'LR': - portPos[node] = { - inPosition: 'west', - outPosition: 'east', - }; - break; - } - } - const result = edgeDirection === 'in' ? portPos[node].inPosition : portPos[node].outPosition; - - if (edgeDirection === 'in') { - portPos[node].inPosition = getNextPosition( - portPos[node].inPosition, - edgeDirection, - graphDirection - ); - } else { - portPos[node].outPosition = getNextPosition( - portPos[node].outPosition, - edgeDirection, - graphDirection - ); - } - return result; -}; - -const getEdgeStartEndPoint = (edge, dir) => { - let source = edge.start; - let target = edge.end; - - // Save the original source and target - const sourceId = source; - const targetId = target; - - const startNode = nodeDb[source]; - const endNode = nodeDb[target]; - - if (!startNode || !endNode) { - return { source, target }; - } - - if (startNode.type === 'diamond') { - source = `${source}-${getNextPort(source, 'out', dir)}`; - } - - if (endNode.type === 'diamond') { - target = `${target}-${getNextPort(target, 'in', dir)}`; - } - - // Add the edge to the graph - return { source, target, sourceId, targetId }; -}; - -/** - * Add edges to graph based on parsed graph definition - * - * @param {object} edges The edges to add to the graph - * @param {object} g The graph object - * @param cy - * @param diagObj - * @param graph - * @param svg - */ -export const addEdges = function (edges, diagObj, graph, svg) { - log.info('abc78 edges = ', edges); - const labelsEl = svg.insert('g').attr('class', 'edgeLabels'); - let linkIdCnt = {}; - let dir = diagObj.db.getDirection(); - let defaultStyle; - let defaultLabelStyle; - - if (edges.defaultStyle !== undefined) { - const defaultStyles = getStylesFromArray(edges.defaultStyle); - defaultStyle = defaultStyles.style; - defaultLabelStyle = defaultStyles.labelStyle; - } - - edges.forEach(function (edge) { - // Identify Link - const linkIdBase = 'L-' + edge.start + '-' + edge.end; - // count the links from+to the same node to give unique id - if (linkIdCnt[linkIdBase] === undefined) { - linkIdCnt[linkIdBase] = 0; - log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]); - } else { - linkIdCnt[linkIdBase]++; - log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]); - } - let linkId = linkIdBase + '-' + linkIdCnt[linkIdBase]; - log.info('abc78 new link id to be used is', linkIdBase, linkId, linkIdCnt[linkIdBase]); - const linkNameStart = 'LS-' + edge.start; - const linkNameEnd = 'LE-' + edge.end; - - const edgeData = { style: '', labelStyle: '' }; - edgeData.minlen = edge.length || 1; - //edgeData.id = 'id' + cnt; - - // Set link type for rendering - if (edge.type === 'arrow_open') { - edgeData.arrowhead = 'none'; - } else { - edgeData.arrowhead = 'normal'; - } - - // Check of arrow types, placed here in order not to break old rendering - edgeData.arrowTypeStart = 'arrow_open'; - edgeData.arrowTypeEnd = 'arrow_open'; - - /* eslint-disable no-fallthrough */ - switch (edge.type) { - case 'double_arrow_cross': - edgeData.arrowTypeStart = 'arrow_cross'; - case 'arrow_cross': - edgeData.arrowTypeEnd = 'arrow_cross'; - break; - case 'double_arrow_point': - edgeData.arrowTypeStart = 'arrow_point'; - case 'arrow_point': - edgeData.arrowTypeEnd = 'arrow_point'; - break; - case 'double_arrow_circle': - edgeData.arrowTypeStart = 'arrow_circle'; - case 'arrow_circle': - edgeData.arrowTypeEnd = 'arrow_circle'; - break; - } - - let style = ''; - let labelStyle = ''; - - switch (edge.stroke) { - case 'normal': - style = 'fill:none;'; - if (defaultStyle !== undefined) { - style = defaultStyle; - } - if (defaultLabelStyle !== undefined) { - labelStyle = defaultLabelStyle; - } - edgeData.thickness = 'normal'; - edgeData.pattern = 'solid'; - break; - case 'dotted': - edgeData.thickness = 'normal'; - edgeData.pattern = 'dotted'; - edgeData.style = 'fill:none;stroke-width:2px;stroke-dasharray:3;'; - break; - case 'thick': - edgeData.thickness = 'thick'; - edgeData.pattern = 'solid'; - edgeData.style = 'stroke-width: 3.5px;fill:none;'; - break; - } - if (edge.style !== undefined) { - const styles = getStylesFromArray(edge.style); - style = styles.style; - labelStyle = styles.labelStyle; - } - - edgeData.style = edgeData.style += style; - edgeData.labelStyle = edgeData.labelStyle += labelStyle; - - if (edge.interpolate !== undefined) { - edgeData.curve = interpolateToCurve(edge.interpolate, curveLinear); - } else if (edges.defaultInterpolate !== undefined) { - edgeData.curve = interpolateToCurve(edges.defaultInterpolate, curveLinear); - } else { - edgeData.curve = interpolateToCurve(conf.curve, curveLinear); - } - - if (edge.text === undefined) { - if (edge.style !== undefined) { - edgeData.arrowheadStyle = 'fill: #333'; - } - } else { - edgeData.arrowheadStyle = 'fill: #333'; - edgeData.labelpos = 'c'; - } - - edgeData.labelType = edge.labelType; - edgeData.label = edge.text.replace(common.lineBreakRegex, '\n'); - - if (edge.style === undefined) { - edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none;'; - } - - edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:'); - - edgeData.id = linkId; - edgeData.classes = 'flowchart-link ' + linkNameStart + ' ' + linkNameEnd; - - const labelEl = insertEdgeLabel(labelsEl, edgeData); - - // calculate start and end points of the edge, note that the source and target - // can be modified for shapes that have ports - const { source, target, sourceId, targetId } = getEdgeStartEndPoint(edge, dir); - log.debug('abc78 source and target', source, target); - // Add the edge to the graph - graph.edges.push({ - id: 'e' + edge.start + edge.end, - sources: [source], - targets: [target], - sourceId, - targetId, - labelEl: labelEl, - labels: [ - { - width: edgeData.width, - height: edgeData.height, - orgWidth: edgeData.width, - orgHeight: edgeData.height, - text: edgeData.label, - layoutOptions: { - 'edgeLabels.inline': 'true', - 'edgeLabels.placement': 'CENTER', - }, - }, - ], - edgeData, - }); - }); - return graph; -}; - -// TODO: break out and share with dagre wrapper. The current code in dagre wrapper also adds -// adds the line to the graph, but we don't need that here. This is why we can't use the dagre -// wrapper directly for this -/** - * Add the markers to the edge depending on the type of arrow is - * @param svgPath - * @param edgeData - * @param diagramType - * @param arrowMarkerAbsolute - * @param id - */ -const addMarkersToEdge = function (svgPath, edgeData, diagramType, arrowMarkerAbsolute, id) { - let url = ''; - // Check configuration for absolute path - if (arrowMarkerAbsolute) { - url = - window.location.protocol + - '//' + - window.location.host + - window.location.pathname + - window.location.search; - url = url.replace(/\(/g, '\\('); - url = url.replace(/\)/g, '\\)'); - } - - // look in edge data and decide which marker to use - addEdgeMarkers(svgPath, edgeData, url, id, diagramType); -}; - -/** - * Returns the all the styles from classDef statements in the graph definition. - * - * @param text - * @param diagObj - * @returns {Map} ClassDef styles - */ -export const getClasses = function (text, diagObj) { - log.info('Extracting classes'); - return diagObj.db.getClasses(); -}; - -const addSubGraphs = function (db) { - const parentLookupDb = { parentById: {}, childrenById: {} }; - const subgraphs = db.getSubGraphs(); - log.info('Subgraphs - ', subgraphs); - subgraphs.forEach(function (subgraph) { - subgraph.nodes.forEach(function (node) { - parentLookupDb.parentById[node] = subgraph.id; - if (parentLookupDb.childrenById[subgraph.id] === undefined) { - parentLookupDb.childrenById[subgraph.id] = []; - } - parentLookupDb.childrenById[subgraph.id].push(node); - }); - }); - - subgraphs.forEach(function (subgraph) { - const data = { id: subgraph.id }; - if (parentLookupDb.parentById[subgraph.id] !== undefined) { - data.parent = parentLookupDb.parentById[subgraph.id]; - } - }); - return parentLookupDb; -}; - -const calcOffset = function (src, dest, parentLookupDb) { - const ancestor = findCommonAncestor(src, dest, parentLookupDb); - if (ancestor === undefined || ancestor === 'root') { - return { x: 0, y: 0 }; - } - - const ancestorOffset = nodeDb[ancestor].offset; - return { x: ancestorOffset.posX, y: ancestorOffset.posY }; -}; - -const insertEdge = function (edgesEl, edge, edgeData, diagObj, parentLookupDb, id) { - const offset = calcOffset(edge.sourceId, edge.targetId, parentLookupDb); - - const src = edge.sections[0].startPoint; - const dest = edge.sections[0].endPoint; - const segments = edge.sections[0].bendPoints ? edge.sections[0].bendPoints : []; - - const segPoints = segments.map((segment) => [segment.x + offset.x, segment.y + offset.y]); - const points = [ - [src.x + offset.x, src.y + offset.y], - ...segPoints, - [dest.x + offset.x, dest.y + offset.y], - ]; - - const { x, y } = getLineFunctionsWithOffset(edge.edgeData); - const curve = line().x(x).y(y).curve(curveLinear); - const edgePath = edgesEl - .insert('path') - .attr('d', curve(points)) - .attr('class', 'path ' + edgeData.classes) - .attr('fill', 'none'); - Object.entries(edgeData).forEach(([key, value]) => { - if (key !== 'classes') { - edgePath.attr(key, value); - } - }); - const edgeG = edgesEl.insert('g').attr('class', 'edgeLabel'); - const edgeWithLabel = select(edgeG.node().appendChild(edge.labelEl)); - const box = edgeWithLabel.node().firstChild.getBoundingClientRect(); - edgeWithLabel.attr('width', box.width); - edgeWithLabel.attr('height', box.height); - - edgeG.attr( - 'transform', - `translate(${edge.labels[0].x + offset.x}, ${edge.labels[0].y + offset.y})` - ); - addMarkersToEdge(edgePath, edgeData, diagObj.type, diagObj.arrowMarkerAbsolute, id); -}; - -/** - * Recursive function that iterates over an array of nodes and inserts the children of each node. - * It also recursively populates the inserts the children of the children and so on. - * @param nodeArray - * @param parentLookupDb - */ -const insertChildren = (nodeArray, parentLookupDb) => { - nodeArray.forEach((node) => { - // Check if we have reached the end of the tree - if (!node.children) { - node.children = []; - } - // Check if the node has children - const childIds = parentLookupDb.childrenById[node.id]; - // If the node has children, add them to the node - if (childIds) { - childIds.forEach((childId) => { - node.children.push(nodeDb[childId]); - }); - } - // Recursive call - insertChildren(node.children, parentLookupDb); - }); -}; - -/** - * Draws a flowchart in the tag with id: id based on the graph definition in text. - * - * @param text - * @param id - */ - -export const draw = async function (text, id, _version, diagObj) { - const { securityLevel, flowchart: conf } = getConfig(); - nodeDb = {}; - portPos = {}; - const renderEl = select('body').append('div').attr('style', 'height:400px').attr('id', 'cy'); - let graph = { - id: 'root', - layoutOptions: { - 'elk.hierarchyHandling': 'INCLUDE_CHILDREN', - 'elk.layered.spacing.edgeNodeBetweenLayers': conf?.nodeSpacing ? `${conf.nodeSpacing}` : '30', - // 'elk.layered.mergeEdges': 'true', - 'elk.direction': 'DOWN', - // 'elk.ports.sameLayerEdges': true, - // 'nodePlacement.strategy': 'SIMPLE', - }, - children: [], - edges: [], - }; - log.info('Drawing flowchart using v3 renderer', elk); - - // Set the direction, - // Fetch the default direction, use TD if none was found - let dir = diagObj.db.getDirection(); - switch (dir) { - case 'BT': - graph.layoutOptions['elk.direction'] = 'UP'; - break; - case 'TB': - graph.layoutOptions['elk.direction'] = 'DOWN'; - break; - case 'LR': - graph.layoutOptions['elk.direction'] = 'RIGHT'; - break; - case 'RL': - graph.layoutOptions['elk.direction'] = 'LEFT'; - break; - } - - // Find the root dom node to ne used in rendering - // Handle root and document for when rendering in sandbox mode - let sandboxElement; - if (securityLevel === 'sandbox') { - sandboxElement = select('#i' + id); - } - const root = - securityLevel === 'sandbox' - ? select(sandboxElement.nodes()[0].contentDocument.body) - : select('body'); - const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document; - - const svg = root.select(`[id="${id}"]`); - // Define the supported markers for the diagram - const markers = ['point', 'circle', 'cross']; - - // Add the marker definitions to the svg as marker tags - insertMarkers(svg, markers, diagObj.type, id); - - // Fetch the vertices/nodes and edges/links from the parsed graph definition - const vert = diagObj.db.getVertices(); - - // Setup nodes from the subgraphs with type group, these will be used - // as nodes with children in the subgraph - let subG; - const subGraphs = diagObj.db.getSubGraphs(); - log.info('Subgraphs - ', subGraphs); - for (let i = subGraphs.length - 1; i >= 0; i--) { - subG = subGraphs[i]; - diagObj.db.addVertex( - subG.id, - { text: subG.title, type: subG.labelType }, - 'group', - undefined, - subG.classes, - subG.dir - ); - } - - // debugger; - // Add an element in the svg to be used to hold the subgraphs container - // elements - const subGraphsEl = svg.insert('g').attr('class', 'subgraphs'); - - // Create the lookup db for the subgraphs and their children to used when creating - // the tree structured graph - const parentLookupDb = addSubGraphs(diagObj.db); - - // Add the nodes to the graph, this will entail creating the actual nodes - // in order to get the size of the node. You can't get the size of a node - // that is not in the dom so we need to add it to the dom, get the size - // we will position the nodes when we get the layout from elkjs - graph = await addVertices(vert, id, root, doc, diagObj, parentLookupDb, graph); - - // Time for the edges, we start with adding an element in the node to hold the edges - const edgesEl = svg.insert('g').attr('class', 'edges edgePath'); - // Fetch the edges form the parsed graph definition - const edges = diagObj.db.getEdges(); - - // Add the edges to the graph, this will entail creating the actual edges - graph = addEdges(edges, diagObj, graph, svg); - - // Iterate through all nodes and add the top level nodes to the graph - const nodes = Object.keys(nodeDb); - nodes.forEach((nodeId) => { - const node = nodeDb[nodeId]; - if (!node.parent) { - graph.children.push(node); - } - // Subgraph - if (parentLookupDb.childrenById[nodeId] !== undefined) { - node.labels = [ - { - text: node.labelText, - layoutOptions: { - 'nodeLabels.placement': '[H_CENTER, V_TOP, INSIDE]', - }, - width: node.labelData.width, - height: node.labelData.height, - // width: 100, - // height: 100, - }, - ]; - delete node.x; - delete node.y; - delete node.width; - delete node.height; - } - }); - - insertChildren(graph.children, parentLookupDb); - log.info('after layout', JSON.stringify(graph, null, 2)); - const g = await elk.layout(graph); - drawNodes(0, 0, g.children, svg, subGraphsEl, diagObj, 0); - utils.insertTitle(svg, 'flowchartTitleText', conf.titleTopMargin, diagObj.db.getDiagramTitle()); - log.info('after layout', g); - g.edges?.map((edge) => { - insertEdge(edgesEl, edge, edge.edgeData, diagObj, parentLookupDb, id); - }); - setupGraphViewbox({}, svg, conf.diagramPadding, conf.useMaxWidth); - // Remove element after layout - renderEl.remove(); -}; - -const drawNodes = (relX, relY, nodeArray, svg, subgraphsEl, diagObj, depth) => { - nodeArray.forEach(function (node) { - if (node) { - nodeDb[node.id].offset = { - posX: node.x + relX, - posY: node.y + relY, - x: relX, - y: relY, - depth, - width: node.width, - height: node.height, - }; - if (node.type === 'group') { - const subgraphEl = subgraphsEl.insert('g').attr('class', 'subgraph'); - subgraphEl - .insert('rect') - .attr('class', 'subgraph subgraph-lvl-' + (depth % 5) + ' node') - .attr('x', node.x + relX) - .attr('y', node.y + relY) - .attr('width', node.width) - .attr('height', node.height); - const label = subgraphEl.insert('g').attr('class', 'label'); - const labelCentering = getConfig().flowchart.htmlLabels ? node.labelData.width / 2 : 0; - label.attr( - 'transform', - `translate(${node.labels[0].x + relX + node.x + labelCentering}, ${ - node.labels[0].y + relY + node.y + 3 - })` - ); - label.node().appendChild(node.labelData.labelNode); - - log.info('Id (UGH)= ', node.type, node.labels); - } else { - log.info('Id (UGH)= ', node.id); - node.el.attr( - 'transform', - `translate(${node.x + relX + node.width / 2}, ${node.y + relY + node.height / 2})` - ); - } - } - }); - nodeArray.forEach(function (node) { - if (node && node.type === 'group') { - drawNodes(relX + node.x, relY + node.y, node.children, svg, subgraphsEl, diagObj, depth + 1); - } - }); -}; - -export default { - getClasses, - draw, -}; diff --git a/packages/mermaid-flowchart-elk/src/render-utils.spec.ts b/packages/mermaid-flowchart-elk/src/render-utils.spec.ts deleted file mode 100644 index 046ed43c1..000000000 --- a/packages/mermaid-flowchart-elk/src/render-utils.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { TreeData } from './render-utils.js'; -import { findCommonAncestor } from './render-utils.js'; -describe('when rendering a flowchart using elk ', () => { - let lookupDb: TreeData; - beforeEach(() => { - lookupDb = { - parentById: { - B4: 'inner', - B5: 'inner', - C4: 'inner2', - C5: 'inner2', - B2: 'Ugge', - B3: 'Ugge', - inner: 'Ugge', - inner2: 'Ugge', - B6: 'outer', - }, - childrenById: { - inner: ['B4', 'B5'], - inner2: ['C4', 'C5'], - Ugge: ['B2', 'B3', 'inner', 'inner2'], - outer: ['B6'], - }, - }; - }); - it('to find parent of siblings in a subgraph', () => { - expect(findCommonAncestor('B4', 'B5', lookupDb)).toBe('inner'); - }); - it('to find an uncle', () => { - expect(findCommonAncestor('B4', 'B2', lookupDb)).toBe('Ugge'); - }); - it('to find a cousin', () => { - expect(findCommonAncestor('B4', 'C4', lookupDb)).toBe('Ugge'); - }); - it('to find a grandparent', () => { - expect(findCommonAncestor('B4', 'B6', lookupDb)).toBe('root'); - }); - it('to find ancestor of siblings in the root', () => { - expect(findCommonAncestor('B1', 'outer', lookupDb)).toBe('root'); - }); -}); diff --git a/packages/mermaid-flowchart-elk/src/render-utils.ts b/packages/mermaid-flowchart-elk/src/render-utils.ts deleted file mode 100644 index ebdc01cf7..000000000 --- a/packages/mermaid-flowchart-elk/src/render-utils.ts +++ /dev/null @@ -1,25 +0,0 @@ -export interface TreeData { - parentById: Record; - childrenById: Record; -} - -export const findCommonAncestor = (id1: string, id2: string, treeData: TreeData) => { - const { parentById } = treeData; - const visited = new Set(); - let currentId = id1; - while (currentId) { - visited.add(currentId); - if (currentId === id2) { - return currentId; - } - currentId = parentById[currentId]; - } - currentId = id2; - while (currentId) { - if (visited.has(currentId)) { - return currentId; - } - currentId = parentById[currentId]; - } - return 'root'; -}; diff --git a/packages/mermaid-flowchart-elk/src/styles.ts b/packages/mermaid-flowchart-elk/src/styles.ts deleted file mode 100644 index 60659df45..000000000 --- a/packages/mermaid-flowchart-elk/src/styles.ts +++ /dev/null @@ -1,143 +0,0 @@ -/** Returns the styles given options */ -export interface FlowChartStyleOptions { - arrowheadColor: string; - border2: string; - clusterBkg: string; - clusterBorder: string; - edgeLabelBackground: string; - fontFamily: string; - lineColor: string; - mainBkg: string; - nodeBorder: string; - nodeTextColor: string; - tertiaryColor: string; - textColor: string; - titleColor: string; - [key: string]: string; -} - -const genSections = (options: FlowChartStyleOptions) => { - let sections = ''; - - for (let i = 0; i < 5; i++) { - sections += ` - .subgraph-lvl-${i} { - fill: ${options[`surface${i}`]}; - stroke: ${options[`surfacePeer${i}`]}; - } - `; - } - return sections; -}; - -const getStyles = (options: FlowChartStyleOptions) => - `.label { - font-family: ${options.fontFamily}; - color: ${options.nodeTextColor || options.textColor}; - } - .cluster-label text { - fill: ${options.titleColor}; - } - .cluster-label span { - color: ${options.titleColor}; - } - - .label text,span { - fill: ${options.nodeTextColor || options.textColor}; - color: ${options.nodeTextColor || options.textColor}; - } - - .node rect, - .node circle, - .node ellipse, - .node polygon, - .node path { - fill: ${options.mainBkg}; - stroke: ${options.nodeBorder}; - stroke-width: 1px; - } - - .node .label { - text-align: center; - } - .node.clickable { - cursor: pointer; - } - - .arrowheadPath { - fill: ${options.arrowheadColor}; - } - - .edgePath .path { - stroke: ${options.lineColor}; - stroke-width: 2.0px; - } - - .flowchart-link { - stroke: ${options.lineColor}; - fill: none; - } - - .edgeLabel { - background-color: ${options.edgeLabelBackground}; - rect { - opacity: 0.85; - background-color: ${options.edgeLabelBackground}; - fill: ${options.edgeLabelBackground}; - } - text-align: center; - } - - .cluster rect { - fill: ${options.clusterBkg}; - stroke: ${options.clusterBorder}; - stroke-width: 1px; - } - - .cluster text { - fill: ${options.titleColor}; - } - - .cluster span { - color: ${options.titleColor}; - } - /* .cluster div { - color: ${options.titleColor}; - } */ - - div.mermaidTooltip { - position: absolute; - text-align: center; - max-width: 200px; - padding: 2px; - font-family: ${options.fontFamily}; - font-size: 12px; - background: ${options.tertiaryColor}; - border: 1px solid ${options.border2}; - border-radius: 2px; - pointer-events: none; - z-index: 100; - } - - .flowchartTitleText { - text-anchor: middle; - font-size: 18px; - fill: ${options.textColor}; - } - .subgraph { - stroke-width:2; - rx:3; - } - // .subgraph-lvl-1 { - // fill:#ccc; - // // stroke:black; - // } - - .flowchart-label text { - text-anchor: middle; - } - - ${genSections(options)} -`; - -export default getStyles; diff --git a/packages/mermaid-flowchart-elk/tsconfig.json b/packages/mermaid-flowchart-elk/tsconfig.json deleted file mode 100644 index 5a41d0603..000000000 --- a/packages/mermaid-flowchart-elk/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "rootDir": "../..", - "outDir": "./dist", - "types": ["vitest/importMeta", "vitest/globals"] - }, - "include": ["./src/**/*.ts"], - "typeRoots": ["./src/types"] -} diff --git a/packages/mermaid-layout-elk/CHANGELOG.md b/packages/mermaid-layout-elk/CHANGELOG.md new file mode 100644 index 000000000..e1ec1d2dd --- /dev/null +++ b/packages/mermaid-layout-elk/CHANGELOG.md @@ -0,0 +1,16 @@ +# @mermaid-js/layout-elk + +## 0.1.2 + +### Patch Changes + +- [#5761](https://github.com/mermaid-js/mermaid/pull/5761) [`b34dfe8`](https://github.com/mermaid-js/mermaid/commit/b34dfe8f45eded31da10965ced7ea40fde1ca76c) Thanks [@sidharthv96](https://github.com/sidharthv96)! - Fix type file path + +## 0.1.1 + +### Patch Changes + +- [#5758](https://github.com/mermaid-js/mermaid/pull/5758) [`501a55d`](https://github.com/mermaid-js/mermaid/commit/501a55d8f225901ba345c498dec4298490a0196e) Thanks [@sidharthv96](https://github.com/sidharthv96)! - fix: Types path + +- Updated dependencies [[`5deaef4`](https://github.com/mermaid-js/mermaid/commit/5deaef456e74d796866431c26f69360e4e74dbff)]: + - mermaid@11.0.2 diff --git a/packages/mermaid-layout-elk/README.md b/packages/mermaid-layout-elk/README.md new file mode 100644 index 000000000..c32803292 --- /dev/null +++ b/packages/mermaid-layout-elk/README.md @@ -0,0 +1,72 @@ +# @mermaid-js/layout-elk + +This package provides a layout engine for Mermaid based on the [ELK](https://www.eclipse.org/elk/) layout engine. + +> [!NOTE] +> The ELK Layout engine will not be available in all providers that support mermaid by default. +> The websites will have to install the `@mermaid-js/layout-elk` package to use the ELK layout engine. + +## Usage + +``` +flowchart-elk TD + A --> B + A --> C +``` + +``` +--- +config: + layout: elk +--- + +flowchart TD + A --> B + A --> C +``` + +``` +--- +config: + layout: elk.stress +--- + +flowchart TD + A --> B + A --> C +``` + +### With bundlers + +```sh +npm install @mermaid-js/layout-elk +``` + +```ts +import mermaid from 'mermaid'; +import elkLayouts from '@mermaid-js/layout-elk'; + +mermaid.registerLayoutLoaders(elkLayouts); +``` + +### With CDN + +```html + +``` + +## Supported layouts + +- `elk`: The default layout, which is `elk.layered`. +- `elk.layered`: Layered layout +- `elk.stress`: Stress layout +- `elk.force`: Force layout +- `elk.mrtree`: Multi-root tree layout +- `elk.sporeOverlap`: Spore overlap layout + + diff --git a/packages/mermaid-layout-elk/package.json b/packages/mermaid-layout-elk/package.json index 4f8054682..5fa491bc8 100644 --- a/packages/mermaid-layout-elk/package.json +++ b/packages/mermaid-layout-elk/package.json @@ -1,14 +1,14 @@ { "name": "@mermaid-js/layout-elk", - "version": "0.0.1", + "version": "0.1.2", "description": "ELK layout engine for mermaid", "module": "dist/mermaid-layout-elk.core.mjs", - "types": "dist/packages/mermaid-layout-elk/src/index.d.ts", + "types": "dist/layouts.d.ts", "type": "module", "exports": { ".": { "import": "./dist/mermaid-layout-elk.core.mjs", - "types": "./dist/packages/mermaid-layout-elk/src/index.d.ts" + "types": "./dist/layouts.d.ts" }, "./*": "./*" }, @@ -18,9 +18,7 @@ "elk", "mermaid" ], - "scripts": { - "prepublishOnly": "pnpm -w run build" - }, + "scripts": {}, "repository": { "type": "git", "url": "https://github.com/mermaid-js/mermaid" @@ -31,11 +29,15 @@ ], "license": "MIT", "dependencies": { - "elkjs": "^0.9.3", - "d3": "^7.9.0" + "d3": "^7.9.0", + "elkjs": "^0.9.3" + }, + "devDependencies": { + "@types/d3": "^7.4.3", + "mermaid": "workspace:^" }, "peerDependencies": { - "mermaid": "workspace:^" + "mermaid": "^11.0.0" }, "files": [ "dist" diff --git a/packages/mermaid-layout-elk/src/layouts.ts b/packages/mermaid-layout-elk/src/layouts.ts index a6075386b..dc48ccbfa 100644 --- a/packages/mermaid-layout-elk/src/layouts.ts +++ b/packages/mermaid-layout-elk/src/layouts.ts @@ -3,7 +3,7 @@ import type { LayoutLoaderDefinition } from 'mermaid'; const loader = async () => await import(`./render.js`); const algos = ['elk.stress', 'elk.force', 'elk.mrtree', 'elk.sporeOverlap']; -export const layouts: LayoutLoaderDefinition[] = [ +const layouts: LayoutLoaderDefinition[] = [ { name: 'elk', loader, @@ -15,3 +15,5 @@ export const layouts: LayoutLoaderDefinition[] = [ algorithm: algo, })), ]; + +export default layouts; diff --git a/packages/mermaid-layout-elk/src/render.ts b/packages/mermaid-layout-elk/src/render.ts index 81453f47f..117ca6276 100644 --- a/packages/mermaid-layout-elk/src/render.ts +++ b/packages/mermaid-layout-elk/src/render.ts @@ -1,466 +1,750 @@ -// @ts-nocheck File not ready to check types import { curveLinear } from 'd3'; import ELK from 'elkjs/lib/elk.bundled.js'; -import mermaid, { type LayoutData } from 'mermaid'; +import type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from 'mermaid'; import { type TreeData, findCommonAncestor } from './find-common-ancestor.js'; -const { - common, - getConfig, - insertCluster, - insertEdge, - insertEdgeLabel, - insertMarkers, - insertNode, - interpolateToCurve, - labelHelper, - log, - positionEdgeLabel, -} = mermaid.internalHelpers; -// import { insertEdge } from '../../mermaid/src/rendering-util/rendering-elements/edges.js'; -const nodeDb = {}; -const portPos = {}; -const clusterDb = {}; +export const render = async ( + data4Layout: LayoutData, + svg: SVG, + { + common, + getConfig, + insertCluster, + insertEdge, + insertEdgeLabel, + insertMarkers, + insertNode, + interpolateToCurve, + labelHelper, + log, + positionEdgeLabel, + }: InternalHelpers, + { algorithm }: RenderOptions +) => { + const nodeDb: Record = {}; + const clusterDb: Record = {}; -export const addVertex = async (nodeEl, graph, nodeArr, node) => { - const labelData = { width: 0, height: 0 }; - // const ports = [ - // { - // id: node.id + '-west', - // layoutOptions: { - // 'port.side': 'WEST', - // }, - // }, - // { - // id: node.id + '-east', - // layoutOptions: { - // 'port.side': 'EAST', - // }, - // }, - // { - // id: node.id + '-south', - // layoutOptions: { - // 'port.side': 'SOUTH', - // }, - // }, - // { - // id: node.id + '-north', - // layoutOptions: { - // 'port.side': 'NORTH', - // }, - // }, - // ]; + const addVertex = async (nodeEl: any, graph: { children: any[] }, nodeArr: any, node: any) => { + const labelData: any = { width: 0, height: 0 }; - let boundingBox; - const child = { - ...node, - // ports: node.shape === 'diamond' ? ports : [], - }; - graph.children.push(child); - nodeDb[node.id] = child; + let boundingBox; + const child = { + ...node, + }; + graph.children.push(child); + nodeDb[node.id] = child; - // Add the element to the DOM - if (!node.isGroup) { - const childNodeEl = await insertNode(nodeEl, node, node.dir); - boundingBox = childNodeEl.node().getBBox(); - child.domId = childNodeEl; - child.width = boundingBox.width; - child.height = boundingBox.height; - } else { - // A subgraph - child.children = []; - await addVertices(nodeEl, nodeArr, child, node.id); - - if (node.label) { - const { shapeSvg, bbox } = await labelHelper(nodeEl, node, undefined, true); - labelData.width = bbox.width; - labelData.wrappingWidth = getConfig().flowchart.wrappingWidth; - // Give some padding for elk - labelData.height = bbox.height - 2; - labelData.labelNode = shapeSvg.node(); - // We need the label hight to be able to size the subgraph; - shapeSvg.remove(); + // Add the element to the DOM + if (!node.isGroup) { + const childNodeEl = await insertNode(nodeEl, node, node.dir); + boundingBox = childNodeEl.node().getBBox(); + child.domId = childNodeEl; + child.width = boundingBox.width; + child.height = boundingBox.height; } else { - // Subgraph without label - labelData.width = 0; - labelData.height = 0; - } - child.labelData = labelData; - child.domId = nodeEl; - } -}; + // A subgraph + child.children = []; + await addVertices(nodeEl, nodeArr, child, node.id); -export const addVertices = async function (nodeEl, nodeArr, graph, parentId) { - const siblings = nodeArr.filter((node) => node.parentId === parentId); - log.info('addVertices APA12', siblings, parentId); - // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition - await Promise.all( - siblings.map(async (node) => { - await addVertex(nodeEl, graph, nodeArr, node); - }) - ); - return graph; -}; - -const drawNodes = async (relX, relY, nodeArray, svg, subgraphsEl, depth) => { - await Promise.all( - nodeArray.map(async function (node) { - if (node) { - nodeDb[node.id] = node; - nodeDb[node.id].offset = { - posX: node.x + relX, - posY: node.y + relY, - x: relX, - y: relY, - depth, - width: Math.max(node.width, node.labels ? node.labels[0]?.width || 0 : 0), - height: node.height, - }; - if (node.isGroup) { - log.debug('Id abc88 subgraph = ', node.id, node.x, node.y, node.labelData); - const subgraphEl = subgraphsEl.insert('g').attr('class', 'subgraph'); - // TODO use faster way of cloning - const clusterNode = JSON.parse(JSON.stringify(node)); - clusterNode.x = node.offset.posX + node.width / 2; - clusterNode.y = node.offset.posY + node.height / 2; - await insertCluster(subgraphEl, clusterNode); - - log.debug('Id (UIO)= ', node.id, node.width, node.shape, node.labels); - } else { - log.info( - 'Id NODE = ', - node.id, - node.x, - node.y, - relX, - relY, - node.domId.node(), - `translate(${node.x + relX + node.width / 2}, ${node.y + relY + node.height / 2})` - ); - node.domId.attr( - 'transform', - `translate(${node.x + relX + node.width / 2}, ${node.y + relY + node.height / 2})` - ); - } + if (node.label) { + // @ts-ignore TODO: fix this + const { shapeSvg, bbox } = await labelHelper(nodeEl, node, undefined, true); + labelData.width = bbox.width; + labelData.wrappingWidth = getConfig().flowchart!.wrappingWidth; + // Give some padding for elk + labelData.height = bbox.height - 2; + labelData.labelNode = shapeSvg.node(); + // We need the label hight to be able to size the subgraph; + shapeSvg.remove(); + } else { + // Subgraph without label + labelData.width = 0; + labelData.height = 0; } - }) - ); - - await Promise.all( - nodeArray.map(async function (node) { - if (node?.isGroup) { - await drawNodes(relX + node.x, relY + node.y, node.children, svg, subgraphsEl, depth + 1); - } - }) - ); -}; - -const getNextPort = (node, edgeDirection, graphDirection) => { - log.info('getNextPort abc88', { node, edgeDirection, graphDirection }); - if (!portPos[node]) { - switch (graphDirection) { - case 'TB': - case 'TD': - portPos[node] = { - inPosition: 'north', - outPosition: 'south', - }; - break; - case 'BT': - portPos[node] = { - inPosition: 'south', - outPosition: 'north', - }; - break; - case 'RL': - portPos[node] = { - inPosition: 'east', - outPosition: 'west', - }; - break; - case 'LR': - portPos[node] = { - inPosition: 'west', - outPosition: 'east', - }; - break; + child.labelData = labelData; + child.domId = nodeEl; } - } - const result = edgeDirection === 'in' ? portPos[node].inPosition : portPos[node].outPosition; + }; - if (edgeDirection === 'in') { - portPos[node].inPosition = getNextPosition( - portPos[node].inPosition, - edgeDirection, - graphDirection + const addVertices = async function ( + nodeEl: any, + nodeArr: any[], + graph: { + id: string; + layoutOptions: { + 'elk.hierarchyHandling': string; + 'elk.algorithm': any; + 'nodePlacement.strategy': any; + 'elk.layered.mergeEdges': any; + 'elk.direction': string; + 'spacing.baseValue': number; + }; + children: never[]; + edges: never[]; + }, + parentId?: undefined + ) { + const siblings = nodeArr.filter((node: { parentId: any }) => node.parentId === parentId); + log.info('addVertices APA12', siblings, parentId); + // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition + await Promise.all( + siblings.map(async (node: any) => { + await addVertex(nodeEl, graph, nodeArr, node); + }) ); - } else { - portPos[node].outPosition = getNextPosition( - portPos[node].outPosition, - edgeDirection, - graphDirection - ); - } - return result; -}; + return graph; + }; -const addSubGraphs = (nodeArr): TreeData => { - const parentLookupDb: TreeData = { parentById: {}, childrenById: {} }; - const subgraphs = nodeArr.filter((node) => node.isGroup); - log.info('Subgraphs - ', subgraphs); - subgraphs.forEach((subgraph) => { - const children = nodeArr.filter((node) => node.parentId === subgraph.id); - children.forEach((node) => { - parentLookupDb.parentById[node.id] = subgraph.id; - if (parentLookupDb.childrenById[subgraph.id] === undefined) { - parentLookupDb.childrenById[subgraph.id] = []; - } - parentLookupDb.childrenById[subgraph.id].push(node); - }); - }); + const drawNodes = async ( + relX: number, + relY: number, + nodeArray: any[], + svg: any, + subgraphsEl: SVGGroup, + depth: number + ) => { + await Promise.all( + nodeArray.map(async function (node: { + id: string | number; + x: any; + y: any; + width: number; + labels: { width: any }[]; + height: number; + isGroup: any; + labelData: any; + offset: { posX: number; posY: number }; + shape: any; + domId: { node: () => any; attr: (arg0: string, arg1: string) => void }; + }) { + if (node) { + nodeDb[node.id] = node; + nodeDb[node.id].offset = { + posX: node.x + relX, + posY: node.y + relY, + x: relX, + y: relY, + depth, + width: Math.max(node.width, node.labels ? node.labels[0]?.width || 0 : 0), + height: node.height, + }; + if (node.isGroup) { + log.debug('Id abc88 subgraph = ', node.id, node.x, node.y, node.labelData); + const subgraphEl = subgraphsEl.insert('g').attr('class', 'subgraph'); + // TODO use faster way of cloning + const clusterNode = JSON.parse(JSON.stringify(node)); + clusterNode.x = node.offset.posX + node.width / 2; + clusterNode.y = node.offset.posY + node.height / 2; + await insertCluster(subgraphEl, clusterNode); - subgraphs.forEach(function (subgraph) { - const data = { id: subgraph.id }; - if (parentLookupDb.parentById[subgraph.id] !== undefined) { - data.parent = parentLookupDb.parentById[subgraph.id]; - } - }); - return parentLookupDb; -}; - -const getEdgeStartEndPoint = (edge, dir) => { - let source = edge.start; - let target = edge.end; - - // Save the original source and target - const sourceId = source; - const targetId = target; - - const startNode = nodeDb[edge.start.id]; - const endNode = nodeDb[edge.end.id]; - - if (!startNode || !endNode) { - return { source, target }; - } - - if (startNode.shape === 'diamond') { - source = `${source}-${getNextPort(source, 'out', dir)}`; - } - - if (endNode.shape === 'diamond') { - target = `${target}-${getNextPort(target, 'in', dir)}`; - } - - // Add the edge to the graph - return { source, target, sourceId, targetId }; -}; - -const calcOffset = function (src: string, dest: string, parentLookupDb: TreeData) { - const ancestor = findCommonAncestor(src, dest, parentLookupDb); - if (ancestor === undefined || ancestor === 'root') { - return { x: 0, y: 0 }; - } - - const ancestorOffset = nodeDb[ancestor].offset; - return { x: ancestorOffset.posX, y: ancestorOffset.posY }; -}; - -/** - * Add edges to graph based on parsed graph definition - */ -export const addEdges = async function (dataForLayout, graph, svg) { - log.info('abc78 DAGA edges = ', dataForLayout); - const edges = dataForLayout.edges; - const labelsEl = svg.insert('g').attr('class', 'edgeLabels'); - const linkIdCnt = {}; - const dir = dataForLayout.direction || 'DOWN'; - let defaultStyle; - let defaultLabelStyle; - - await Promise.all( - edges.map(async function (edge) { - // Identify Link - const linkIdBase = edge.id; // 'L-' + edge.start + '-' + edge.end; - // count the links from+to the same node to give unique id - if (linkIdCnt[linkIdBase] === undefined) { - linkIdCnt[linkIdBase] = 0; - log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]); - } else { - linkIdCnt[linkIdBase]++; - log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]); - } - const linkId = linkIdBase + '_' + linkIdCnt[linkIdBase]; - edge.id = linkId; - log.info('abc78 new link id to be used is', linkIdBase, linkId, linkIdCnt[linkIdBase]); - const linkNameStart = 'LS_' + edge.start; - const linkNameEnd = 'LE_' + edge.end; - - const edgeData = { style: '', labelStyle: '' }; - edgeData.minlen = edge.length || 1; - edge.text = edge.label; - // Set link type for rendering - if (edge.type === 'arrow_open') { - edgeData.arrowhead = 'none'; - } else { - edgeData.arrowhead = 'normal'; - } - - // Check of arrow types, placed here in order not to break old rendering - edgeData.arrowTypeStart = 'arrow_open'; - edgeData.arrowTypeEnd = 'arrow_open'; - - /* eslint-disable no-fallthrough */ - switch (edge.type) { - case 'double_arrow_cross': - edgeData.arrowTypeStart = 'arrow_cross'; - case 'arrow_cross': - edgeData.arrowTypeEnd = 'arrow_cross'; - break; - case 'double_arrow_point': - edgeData.arrowTypeStart = 'arrow_point'; - case 'arrow_point': - edgeData.arrowTypeEnd = 'arrow_point'; - break; - case 'double_arrow_circle': - edgeData.arrowTypeStart = 'arrow_circle'; - case 'arrow_circle': - edgeData.arrowTypeEnd = 'arrow_circle'; - break; - } - - let style = ''; - let labelStyle = ''; - - switch (edge.stroke) { - case 'normal': - style = 'fill:none;'; - if (defaultStyle !== undefined) { - style = defaultStyle; + log.debug('Id (UIO)= ', node.id, node.width, node.shape, node.labels); + } else { + log.info( + 'Id NODE = ', + node.id, + node.x, + node.y, + relX, + relY, + node.domId.node(), + `translate(${node.x + relX + node.width / 2}, ${node.y + relY + node.height / 2})` + ); + node.domId.attr( + 'transform', + `translate(${node.x + relX + node.width / 2}, ${node.y + relY + node.height / 2})` + ); } - if (defaultLabelStyle !== undefined) { - labelStyle = defaultLabelStyle; - } - edgeData.thickness = 'normal'; - edgeData.pattern = 'solid'; - break; - case 'dotted': - edgeData.thickness = 'normal'; - edgeData.pattern = 'dotted'; - edgeData.style = 'fill:none;stroke-width:2px;stroke-dasharray:3;'; - break; - case 'thick': - edgeData.thickness = 'thick'; - edgeData.pattern = 'solid'; - edgeData.style = 'stroke-width: 3.5px;fill:none;'; - break; - } - - edgeData.style = edgeData.style += style; - edgeData.labelStyle = edgeData.labelStyle += labelStyle; - - const conf = getConfig(); - if (edge.interpolate !== undefined) { - edgeData.curve = interpolateToCurve(edge.interpolate, curveLinear); - } else if (edges.defaultInterpolate !== undefined) { - edgeData.curve = interpolateToCurve(edges.defaultInterpolate, curveLinear); - } else { - edgeData.curve = interpolateToCurve(conf.curve, curveLinear); - } - - if (edge.text === undefined) { - if (edge.style !== undefined) { - edgeData.arrowheadStyle = 'fill: #333'; } - } else { - edgeData.arrowheadStyle = 'fill: #333'; - edgeData.labelpos = 'c'; - } + }) + ); - edgeData.labelType = edge.labelType; - edgeData.label = (edge?.text || '').replace(common.lineBreakRegex, '\n'); + await Promise.all( + nodeArray.map(async function (node: { isGroup: any; x: any; y: any; children: any }) { + if (node?.isGroup) { + await drawNodes(relX + node.x, relY + node.y, node.children, svg, subgraphsEl, depth + 1); + } + }) + ); + }; - if (edge.style === undefined) { - edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none;'; - } - - edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:'); - - edgeData.id = linkId; - edgeData.classes = 'flowchart-link ' + linkNameStart + ' ' + linkNameEnd; - - const labelEl = await insertEdgeLabel(labelsEl, edgeData); - - // calculate start and end points of the edge, note that the source and target - // can be modified for shapes that have ports - const { source, target, sourceId, targetId } = getEdgeStartEndPoint(edge, dir); - log.debug('abc78 source and target', source, target); - // Add the edge to the graph - graph.edges.push({ - id: 'e' + edge.start + edge.end, - ...edge, - sources: [source], - targets: [target], - sourceId, - targetId, - labelEl: labelEl, - labels: [ - { - width: edgeData.width, - height: edgeData.height, - orgWidth: edgeData.width, - orgHeight: edgeData.height, - text: edgeData.label, - layoutOptions: { - 'edgeLabels.inline': 'true', - 'edgeLabels.placement': 'CENTER', - }, - }, - ], - edgeData, + const addSubGraphs = (nodeArr: any[]): TreeData => { + const parentLookupDb: TreeData = { parentById: {}, childrenById: {} }; + const subgraphs = nodeArr.filter((node: { isGroup: any }) => node.isGroup); + log.info('Subgraphs - ', subgraphs); + subgraphs.forEach((subgraph: { id: string }) => { + const children = nodeArr.filter((node: { parentId: any }) => node.parentId === subgraph.id); + children.forEach((node: any) => { + parentLookupDb.parentById[node.id] = subgraph.id; + if (parentLookupDb.childrenById[subgraph.id] === undefined) { + parentLookupDb.childrenById[subgraph.id] = []; + } + parentLookupDb.childrenById[subgraph.id].push(node); }); - }) - ); - return graph; -}; + }); -function dir2ElkDirection(dir) { - switch (dir) { - case 'LR': - return 'RIGHT'; - case 'RL': - return 'LEFT'; - case 'TB': - return 'DOWN'; - case 'BT': - return 'UP'; - default: - return 'DOWN'; - } -} + subgraphs.forEach(function (subgraph: { id: string | number }) { + const data: any = { id: subgraph.id }; + if (parentLookupDb.parentById[subgraph.id] !== undefined) { + data.parent = parentLookupDb.parentById[subgraph.id]; + } + }); + return parentLookupDb; + }; -function setIncludeChildrenPolicy(nodeId: string, ancestorId: string) { - const node = nodeDb[nodeId]; + const getEdgeStartEndPoint = (edge: any) => { + const source: any = edge.start; + const target: any = edge.end; - if (!node) { - return; - } - if (node?.layoutOptions === undefined) { - node.layoutOptions = {}; - } - node.layoutOptions['elk.hierarchyHandling'] = 'INCLUDE_CHILDREN'; - if (node.id !== ancestorId) { - setIncludeChildrenPolicy(node.parentId, ancestorId); - } -} + // Save the original source and target + const sourceId = source; + const targetId = target; -export const render = async (data4Layout: LayoutData, svg, element, algorithm) => { + const startNode = nodeDb[edge.start.id]; + const endNode = nodeDb[edge.end.id]; + + if (!startNode || !endNode) { + return { source, target }; + } + + // Add the edge to the graph + return { source, target, sourceId, targetId }; + }; + + const calcOffset = function (src: string, dest: string, parentLookupDb: TreeData) { + const ancestor = findCommonAncestor(src, dest, parentLookupDb); + if (ancestor === undefined || ancestor === 'root') { + return { x: 0, y: 0 }; + } + + const ancestorOffset = nodeDb[ancestor].offset; + return { x: ancestorOffset.posX, y: ancestorOffset.posY }; + }; + + /** + * Add edges to graph based on parsed graph definition + */ + const addEdges = async function ( + dataForLayout: { edges: any; direction: string }, + graph: { + id?: string; + layoutOptions?: { + 'elk.hierarchyHandling': string; + 'elk.algorithm': any; + 'nodePlacement.strategy': any; + 'elk.layered.mergeEdges': any; + 'elk.direction': string; + 'spacing.baseValue': number; + }; + children?: never[]; + edges: any; + }, + svg: SVG + ) { + log.info('abc78 DAGA edges = ', dataForLayout); + const edges = dataForLayout.edges; + const labelsEl = svg.insert('g').attr('class', 'edgeLabels'); + const linkIdCnt: any = {}; + const dir = dataForLayout.direction || 'DOWN'; + let defaultStyle: string | undefined; + let defaultLabelStyle: string | undefined; + + await Promise.all( + edges.map(async function (edge: { + id: string; + start: string; + end: string; + length: number; + text: undefined; + label: any; + type: string; + stroke: any; + interpolate: undefined; + style: undefined; + labelType: any; + }) { + // Identify Link + const linkIdBase = edge.id; // 'L-' + edge.start + '-' + edge.end; + // count the links from+to the same node to give unique id + if (linkIdCnt[linkIdBase] === undefined) { + linkIdCnt[linkIdBase] = 0; + log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]); + } else { + linkIdCnt[linkIdBase]++; + log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]); + } + const linkId = linkIdBase + '_' + linkIdCnt[linkIdBase]; + edge.id = linkId; + log.info('abc78 new link id to be used is', linkIdBase, linkId, linkIdCnt[linkIdBase]); + const linkNameStart = 'LS_' + edge.start; + const linkNameEnd = 'LE_' + edge.end; + + const edgeData: any = { style: '', labelStyle: '' }; + edgeData.minlen = edge.length || 1; + edge.text = edge.label; + // Set link type for rendering + if (edge.type === 'arrow_open') { + edgeData.arrowhead = 'none'; + } else { + edgeData.arrowhead = 'normal'; + } + + // Check of arrow types, placed here in order not to break old rendering + edgeData.arrowTypeStart = 'arrow_open'; + edgeData.arrowTypeEnd = 'arrow_open'; + + /* eslint-disable no-fallthrough */ + switch (edge.type) { + case 'double_arrow_cross': + edgeData.arrowTypeStart = 'arrow_cross'; + case 'arrow_cross': + edgeData.arrowTypeEnd = 'arrow_cross'; + break; + case 'double_arrow_point': + edgeData.arrowTypeStart = 'arrow_point'; + case 'arrow_point': + edgeData.arrowTypeEnd = 'arrow_point'; + break; + case 'double_arrow_circle': + edgeData.arrowTypeStart = 'arrow_circle'; + case 'arrow_circle': + edgeData.arrowTypeEnd = 'arrow_circle'; + break; + } + + let style = ''; + let labelStyle = ''; + + switch (edge.stroke) { + case 'normal': + style = 'fill:none;'; + if (defaultStyle !== undefined) { + style = defaultStyle; + } + if (defaultLabelStyle !== undefined) { + labelStyle = defaultLabelStyle; + } + edgeData.thickness = 'normal'; + edgeData.pattern = 'solid'; + break; + case 'dotted': + edgeData.thickness = 'normal'; + edgeData.pattern = 'dotted'; + edgeData.style = 'fill:none;stroke-width:2px;stroke-dasharray:3;'; + break; + case 'thick': + edgeData.thickness = 'thick'; + edgeData.pattern = 'solid'; + edgeData.style = 'stroke-width: 3.5px;fill:none;'; + break; + } + + edgeData.style = edgeData.style += style; + edgeData.labelStyle = edgeData.labelStyle += labelStyle; + + const conf = getConfig(); + if (edge.interpolate !== undefined) { + edgeData.curve = interpolateToCurve(edge.interpolate, curveLinear); + } else if (edges.defaultInterpolate !== undefined) { + edgeData.curve = interpolateToCurve(edges.defaultInterpolate, curveLinear); + } else { + // @ts-ignore TODO: fix this + edgeData.curve = interpolateToCurve(conf.curve, curveLinear); + } + + if (edge.text === undefined) { + if (edge.style !== undefined) { + edgeData.arrowheadStyle = 'fill: #333'; + } + } else { + edgeData.arrowheadStyle = 'fill: #333'; + edgeData.labelpos = 'c'; + } + + edgeData.labelType = edge.labelType; + edgeData.label = (edge?.text || '').replace(common.lineBreakRegex, '\n'); + + if (edge.style === undefined) { + edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none;'; + } + + edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:'); + + edgeData.id = linkId; + edgeData.classes = 'flowchart-link ' + linkNameStart + ' ' + linkNameEnd; + + const labelEl = await insertEdgeLabel(labelsEl, edgeData); + + // calculate start and end points of the edge, note that the source and target + // can be modified for shapes that have ports + // @ts-ignore TODO: fix this + const { source, target, sourceId, targetId } = getEdgeStartEndPoint(edge, dir); + log.debug('abc78 source and target', source, target); + // Add the edge to the graph + graph.edges.push({ + // @ts-ignore TODO: fix this + id: 'e' + edge.start + edge.end, + ...edge, + sources: [source], + targets: [target], + sourceId, + targetId, + labelEl: labelEl, + labels: [ + { + width: edgeData.width, + height: edgeData.height, + orgWidth: edgeData.width, + orgHeight: edgeData.height, + text: edgeData.label, + layoutOptions: { + 'edgeLabels.inline': 'true', + 'edgeLabels.placement': 'CENTER', + }, + }, + ], + edgeData, + }); + }) + ); + return graph; + }; + + function dir2ElkDirection(dir: any) { + switch (dir) { + case 'LR': + return 'RIGHT'; + case 'RL': + return 'LEFT'; + case 'TB': + return 'DOWN'; + case 'BT': + return 'UP'; + default: + return 'DOWN'; + } + } + + function setIncludeChildrenPolicy(nodeId: string, ancestorId: string) { + const node = nodeDb[nodeId]; + + if (!node) { + return; + } + if (node?.layoutOptions === undefined) { + node.layoutOptions = {}; + } + node.layoutOptions['elk.hierarchyHandling'] = 'INCLUDE_CHILDREN'; + if (node.id !== ancestorId) { + setIncludeChildrenPolicy(node.parentId, ancestorId); + } + } + + function intersectLine( + p1: { y: number; x: number }, + p2: { y: number; x: number }, + q1: { x: any; y: any }, + q2: { x: any; y: any } + ) { + log.debug('UIO intersectLine', p1, p2, q1, q2); + // Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994, + // p7 and p473. + + // let a1, a2, b1, b2, c1, c2; + // let r1, r2, r3, r4; + // let denom, offset, num; + // let x, y; + + // Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x + + // b1 y + c1 = 0. + const a1 = p2.y - p1.y; + const b1 = p1.x - p2.x; + const c1 = p2.x * p1.y - p1.x * p2.y; + + // Compute r3 and r4. + const r3 = a1 * q1.x + b1 * q1.y + c1; + const r4 = a1 * q2.x + b1 * q2.y + c1; + + // Check signs of r3 and r4. If both point 3 and point 4 lie on + // same side of line 1, the line segments do not intersect. + if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) { + return /*DON'T_INTERSECT*/; + } + + // Compute a2, b2, c2 where line joining points 3 and 4 is G(x,y) = a2 x + b2 y + c2 = 0 + const a2 = q2.y - q1.y; + const b2 = q1.x - q2.x; + const c2 = q2.x * q1.y - q1.x * q2.y; + + // Compute r1 and r2 + const r1 = a2 * p1.x + b2 * p1.y + c2; + const r2 = a2 * p2.x + b2 * p2.y + c2; + + // Check signs of r1 and r2. If both point 1 and point 2 lie + // on same side of second line segment, the line segments do + // not intersect. + if (r1 !== 0 && r2 !== 0 && sameSign(r1, r2)) { + return /*DON'T_INTERSECT*/; + } + + // Line segments intersect: compute intersection point. + const denom = a1 * b2 - a2 * b1; + if (denom === 0) { + return /*COLLINEAR*/; + } + + const offset = Math.abs(denom / 2); + + // The denom/2 is to get rounding instead of truncating. It + // is added or subtracted to the numerator, depending upon the + // sign of the numerator. + let num = b1 * c2 - b2 * c1; + const x = num < 0 ? (num - offset) / denom : (num + offset) / denom; + + num = a2 * c1 - a1 * c2; + const y = num < 0 ? (num - offset) / denom : (num + offset) / denom; + + return { x: x, y: y }; + } + + function sameSign(r1: number, r2: number) { + return r1 * r2 > 0; + } + const diamondIntersection = ( + bounds: { x: any; y: any; width: any; height: any }, + outsidePoint: { x: number; y: number }, + insidePoint: any + ) => { + const x1 = bounds.x; + const y1 = bounds.y; + + const w = bounds.width; //+ bounds.padding; + const h = bounds.height; // + bounds.padding; + + const polyPoints = [ + { x: x1, y: y1 - h / 2 }, + { x: x1 + w / 2, y: y1 }, + { x: x1, y: y1 + h / 2 }, + { x: x1 - w / 2, y: y1 }, + ]; + log.debug( + `UIO diamondIntersection calc abc89: + outsidePoint: ${JSON.stringify(outsidePoint)} + insidePoint : ${JSON.stringify(insidePoint)} + node : x:${bounds.x} y:${bounds.y} w:${bounds.width} h:${bounds.height}`, + polyPoints + ); + + const intersections = []; + + let minX = Number.POSITIVE_INFINITY; + let minY = Number.POSITIVE_INFINITY; + + polyPoints.forEach(function (entry) { + minX = Math.min(minX, entry.x); + minY = Math.min(minY, entry.y); + }); + + // const left = x1 - w / 2; + // const top = y1 + h / 2; + + for (let i = 0; i < polyPoints.length; i++) { + const p1 = polyPoints[i]; + const p2 = polyPoints[i < polyPoints.length - 1 ? i + 1 : 0]; + const intersect = intersectLine( + bounds, + outsidePoint, + { x: p1.x, y: p1.y }, + { x: p2.x, y: p2.y } + ); + + if (intersect) { + intersections.push(intersect); + } + } + + if (!intersections.length) { + return bounds; + } + + log.debug('UIO intersections', intersections); + + if (intersections.length > 1) { + // More intersections, find the one nearest to edge end point + intersections.sort(function (p, q) { + const pdx = p.x - outsidePoint.x; + const pdy = p.y - outsidePoint.y; + const distp = Math.sqrt(pdx * pdx + pdy * pdy); + + const qdx = q.x - outsidePoint.x; + const qdy = q.y - outsidePoint.y; + const distq = Math.sqrt(qdx * qdx + qdy * qdy); + + return distp < distq ? -1 : distp === distq ? 0 : 1; + }); + } + + return intersections[0]; + }; + + const intersection = ( + node: { x: any; y: any; width: number; height: number }, + outsidePoint: { x: number; y: number }, + insidePoint: { x: number; y: number } + ) => { + log.debug(`intersection calc abc89: + outsidePoint: ${JSON.stringify(outsidePoint)} + insidePoint : ${JSON.stringify(insidePoint)} + node : x:${node.x} y:${node.y} w:${node.width} h:${node.height}`); + const x = node.x; + const y = node.y; + + const dx = Math.abs(x - insidePoint.x); + // const dy = Math.abs(y - insidePoint.y); + const w = node.width / 2; + let r = insidePoint.x < outsidePoint.x ? w - dx : w + dx; + const h = node.height / 2; + + const Q = Math.abs(outsidePoint.y - insidePoint.y); + const R = Math.abs(outsidePoint.x - insidePoint.x); + + if (Math.abs(y - outsidePoint.y) * w > Math.abs(x - outsidePoint.x) * h) { + // Intersection is top or bottom of rect. + const q = insidePoint.y < outsidePoint.y ? outsidePoint.y - h - y : y - h - outsidePoint.y; + r = (R * q) / Q; + const res = { + x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - R + r, + y: insidePoint.y < outsidePoint.y ? insidePoint.y + Q - q : insidePoint.y - Q + q, + }; + + if (r === 0) { + res.x = outsidePoint.x; + res.y = outsidePoint.y; + } + if (R === 0) { + res.x = outsidePoint.x; + } + if (Q === 0) { + res.y = outsidePoint.y; + } + + log.debug(`abc89 topp/bott calc, Q ${Q}, q ${q}, R ${R}, r ${r}`, res); // cspell: disable-line + + return res; + } else { + // Intersection onn sides of rect + if (insidePoint.x < outsidePoint.x) { + r = outsidePoint.x - w - x; + } else { + // r = outsidePoint.x - w - x; + r = x - w - outsidePoint.x; + } + const q = (Q * r) / R; + // OK let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x + dx - w; + // OK let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : outsidePoint.x + r; + let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x - R + r; + // let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : outsidePoint.x + r; + let _y = insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q; + log.debug(`sides calc abc89, Q ${Q}, q ${q}, R ${R}, r ${r}`, { _x, _y }); + if (r === 0) { + _x = outsidePoint.x; + _y = outsidePoint.y; + } + if (R === 0) { + _x = outsidePoint.x; + } + if (Q === 0) { + _y = outsidePoint.y; + } + + return { x: _x, y: _y }; + } + }; + const outsideNode = ( + node: { x: any; y: any; width: number; height: number }, + point: { x: number; y: number } + ) => { + const x = node.x; + const y = node.y; + const dx = Math.abs(point.x - x); + const dy = Math.abs(point.y - y); + const w = node.width / 2; + const h = node.height / 2; + if (dx >= w || dy >= h) { + return true; + } + return false; + }; + /** + * This function will page a path and node where the last point(s) in the path is inside the node + * and return an update path ending by the border of the node. + */ + const cutPathAtIntersect = ( + _points: any[], + bounds: { x: any; y: any; width: any; height: any; padding: any }, + isDiamond: boolean + ) => { + log.debug('UIO cutPathAtIntersect Points:', _points, 'node:', bounds, 'isDiamond', isDiamond); + const points: any[] = []; + let lastPointOutside = _points[0]; + let isInside = false; + _points.forEach((point: any) => { + // const node = clusterDb[edge.toCluster].node; + log.debug(' checking point', point, bounds); + + // check if point is inside the boundary rect + if (!outsideNode(bounds, point) && !isInside) { + // First point inside the rect found + // Calc the intersection coord between the point anf the last point outside the rect + let inter; + + if (isDiamond) { + const inter2 = diamondIntersection(bounds, lastPointOutside, point); + const distance = Math.sqrt( + (lastPointOutside.x - inter2.x) ** 2 + (lastPointOutside.y - inter2.y) ** 2 + ); + if (distance > 1) { + inter = inter2; + } + } + if (!inter) { + inter = intersection(bounds, lastPointOutside, point); + } + + // Check case where the intersection is the same as the last point + let pointPresent = false; + points.forEach((p) => { + pointPresent = pointPresent || (p.x === inter.x && p.y === inter.y); + }); + // if (!pointPresent) { + if (!points.some((e) => e.x === inter.x && e.y === inter.y)) { + points.push(inter); + } else { + log.debug('abc88 no intersect', inter, points); + } + // points.push(inter); + isInside = true; + } else { + // Outside + log.debug('abc88 outside', point, lastPointOutside, points); + lastPointOutside = point; + // points.push(point); + if (!isInside) { + points.push(point); + } + } + }); + log.debug('returning points', points); + return points; + }; + + // @ts-ignore - ELK is not typed const elk = new ELK(); - + const element = svg.select('g'); // Add the arrowheads to the svg insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId); // Setup the graph with the layout options and the data for the layout - let elkGraph = { + let elkGraph: any = { id: 'root', layoutOptions: { 'elk.hierarchyHandling': 'INCLUDE_CHILDREN', @@ -489,7 +773,7 @@ export const render = async (data4Layout: LayoutData, svg, element, algorithm) = // Create the lookup db for the subgraphs and their children to used when creating // the tree structured graph - const parentLookupDb = addSubGraphs(data4Layout.nodes); + const parentLookupDb: any = addSubGraphs(data4Layout.nodes); // Add elements in the svg to be used to hold the subgraphs container // elements and the nodes @@ -510,7 +794,7 @@ export const render = async (data4Layout: LayoutData, svg, element, algorithm) = // Iterate through all nodes and add the top level nodes to the graph const nodes = data4Layout.nodes; - nodes.forEach((n) => { + nodes.forEach((n: { id: string | number }) => { const node = nodeDb[n.id]; // Subgraph @@ -544,7 +828,7 @@ export const render = async (data4Layout: LayoutData, svg, element, algorithm) = delete node.height; } }); - elkGraph.edges.forEach((edge) => { + elkGraph.edges.forEach((edge: any) => { const source = edge.sources[0]; const target = edge.targets[0]; @@ -560,438 +844,149 @@ export const render = async (data4Layout: LayoutData, svg, element, algorithm) = // debugger; await drawNodes(0, 0, g.children, svg, subGraphsEl, 0); - g.edges?.map((edge) => { - // (elem, edge, clusterDb, diagramType, graph, id) - const startNode = nodeDb[edge.sources[0]]; - const startCluster = parentLookupDb[edge.sources[0]]; - const endNode = nodeDb[edge.targets[0]]; - const sourceId = edge.start; - const targetId = edge.end; + g.edges?.map( + (edge: { + sources: (string | number)[]; + targets: (string | number)[]; + start: any; + end: any; + sections: { startPoint: any; endPoint: any; bendPoints: any }[]; + points: any[]; + x: any; + labels: { height: number; width: number; x: number; y: number }[]; + y: any; + }) => { + // (elem, edge, clusterDb, diagramType, graph, id) + const startNode = nodeDb[edge.sources[0]]; + const startCluster = parentLookupDb[edge.sources[0]]; + const endNode = nodeDb[edge.targets[0]]; + const sourceId = edge.start; + const targetId = edge.end; - const offset = calcOffset(sourceId, targetId, parentLookupDb); - log.debug( - 'offset', - offset, - sourceId, - ' ==> ', - targetId, - 'edge:', - edge, - 'cluster:', - startCluster, - startNode - ); - if (edge.sections) { - const src = edge.sections[0].startPoint; - const dest = edge.sections[0].endPoint; - const segments = edge.sections[0].bendPoints ? edge.sections[0].bendPoints : []; - - const segPoints = segments.map((segment) => { - return { x: segment.x + offset.x, y: segment.y + offset.y }; - }); - edge.points = [ - { x: src.x + offset.x, y: src.y + offset.y }, - ...segPoints, - { x: dest.x + offset.x, y: dest.y + offset.y }, - ]; - - let sw = startNode.width; - let ew = endNode.width; - if (startNode.isGroup) { - const bbox = startNode.domId.node().getBBox(); - // sw = Math.max(bbox.width, startNode.width, startNode.labels[0].width); - sw = Math.max(startNode.width, startNode.labels[0].width + startNode.padding); - // sw = startNode.width; - log.debug( - 'UIO width', - startNode.id, - startNode.with, - 'bbox.width=', - bbox.width, - 'lw=', - startNode.labels[0].width, - 'node:', - startNode.width, - 'SW = ', - sw - // 'HTML:', - // startNode.domId.node().innerHTML - ); - } - if (endNode.isGroup) { - const bbox = endNode.domId.node().getBBox(); - ew = Math.max(endNode.width, endNode.labels[0].width + endNode.padding); - - log.debug( - 'UIO width', - startNode.id, - startNode.with, - bbox.width, - 'EW = ', - ew, - 'HTML:', - startNode.innerHTML - ); - } - if (startNode.shape === 'diamond') { - edge.points.unshift({ - x: startNode.x + startNode.width / 2 + offset.x, - y: startNode.y + startNode.height / 2 + offset.y, - }); - } - if (endNode.shape === 'diamond') { - edge.points.push({ - x: endNode.x + endNode.width / 2 + offset.x, - y: endNode.y + endNode.height / 2 + offset.y, - }); - } - - edge.points = cutPathAtIntersect( - edge.points.reverse(), - { - x: startNode.x + startNode.width / 2 + offset.x, - y: startNode.y + startNode.height / 2 + offset.y, - width: sw, - height: startNode.height, - padding: startNode.padding, - }, - startNode.shape === 'diamond' - ).reverse(); - - edge.points = cutPathAtIntersect( - edge.points, - { - x: endNode.x + ew / 2 + endNode.offset.x, - y: endNode.y + endNode.height / 2 + endNode.offset.y, - width: ew, - height: endNode.height, - padding: endNode.padding, - }, - endNode.shape === 'diamond' - ); - - const paths = insertEdge( - edgesEl, + const offset = calcOffset(sourceId, targetId, parentLookupDb); + log.debug( + 'offset', + offset, + sourceId, + ' ==> ', + targetId, + 'edge:', edge, - clusterDb, - data4Layout.type, - startNode, - endNode, - data4Layout.diagramId + 'cluster:', + startCluster, + startNode ); - log.info('APA12 edge points after insert', JSON.stringify(edge.points)); + if (edge.sections) { + const src = edge.sections[0].startPoint; + const dest = edge.sections[0].endPoint; + const segments = edge.sections[0].bendPoints ? edge.sections[0].bendPoints : []; - edge.x = edge.labels[0].x + offset.x + edge.labels[0].width / 2; - edge.y = edge.labels[0].y + offset.y + edge.labels[0].height / 2; - positionEdgeLabel(edge, paths); - } - // const src = edge.sections[0].startPoint; - // const dest = edge.sections[0].endPoint; - // const segments = edge.sections[0].bendPoints ? edge.sections[0].bendPoints : []; + const segPoints = segments.map((segment: { x: any; y: any }) => { + return { x: segment.x + offset.x, y: segment.y + offset.y }; + }); + edge.points = [ + { x: src.x + offset.x, y: src.y + offset.y }, + ...segPoints, + { x: dest.x + offset.x, y: dest.y + offset.y }, + ]; - // const segPoints = segments.map((segment) => { - // return { x: segment.x + offset.x, y: segment.y + offset.y }; - // }); - // edge.points = [ - // { x: src.x + offset.x, y: src.y + offset.y }, - // ...segPoints, - // { x: dest.x + offset.x, y: dest.y + offset.y }, - // ]; - // const paths = insertEdge( - // edgesEl, - // edge, - // clusterDb, - // data4Layout.type, - // startNode, - // endNode, - // data4Layout.diagramId - // ); - // log.info('APA12 edge points after insert', JSON.stringify(edge.points)); - - // edge.x = edge.labels[0].x + offset.x + edge.labels[0].width / 2; - // edge.y = edge.labels[0].y + offset.y + edge.labels[0].height / 2; - // positionEdgeLabel(edge, paths); - }); -}; - -function intersectLine(p1, p2, q1, q2) { - log.debug('UIO intersectLine', p1, p2, q1, q2); - // Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994, - // p7 and p473. - - // let a1, a2, b1, b2, c1, c2; - // let r1, r2, r3, r4; - // let denom, offset, num; - // let x, y; - - // Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x + - // b1 y + c1 = 0. - const a1 = p2.y - p1.y; - const b1 = p1.x - p2.x; - const c1 = p2.x * p1.y - p1.x * p2.y; - - // Compute r3 and r4. - const r3 = a1 * q1.x + b1 * q1.y + c1; - const r4 = a1 * q2.x + b1 * q2.y + c1; - - // Check signs of r3 and r4. If both point 3 and point 4 lie on - // same side of line 1, the line segments do not intersect. - if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) { - return /*DON'T_INTERSECT*/; - } - - // Compute a2, b2, c2 where line joining points 3 and 4 is G(x,y) = a2 x + b2 y + c2 = 0 - const a2 = q2.y - q1.y; - const b2 = q1.x - q2.x; - const c2 = q2.x * q1.y - q1.x * q2.y; - - // Compute r1 and r2 - const r1 = a2 * p1.x + b2 * p1.y + c2; - const r2 = a2 * p2.x + b2 * p2.y + c2; - - // Check signs of r1 and r2. If both point 1 and point 2 lie - // on same side of second line segment, the line segments do - // not intersect. - if (r1 !== 0 && r2 !== 0 && sameSign(r1, r2)) { - return /*DON'T_INTERSECT*/; - } - - // Line segments intersect: compute intersection point. - const denom = a1 * b2 - a2 * b1; - if (denom === 0) { - return /*COLLINEAR*/; - } - - const offset = Math.abs(denom / 2); - - // The denom/2 is to get rounding instead of truncating. It - // is added or subtracted to the numerator, depending upon the - // sign of the numerator. - let num = b1 * c2 - b2 * c1; - const x = num < 0 ? (num - offset) / denom : (num + offset) / denom; - - num = a2 * c1 - a1 * c2; - const y = num < 0 ? (num - offset) / denom : (num + offset) / denom; - - return { x: x, y: y }; -} - -function sameSign(r1, r2) { - return r1 * r2 > 0; -} -const diamondIntersection = (bounds, outsidePoint, insidePoint) => { - const x1 = bounds.x; - const y1 = bounds.y; - - const w = bounds.width; //+ bounds.padding; - const h = bounds.height; // + bounds.padding; - - const polyPoints = [ - { x: x1, y: y1 - h / 2 }, - { x: x1 + w / 2, y: y1 }, - { x: x1, y: y1 + h / 2 }, - { x: x1 - w / 2, y: y1 }, - ]; - log.debug( - `UIO diamondIntersection calc abc89: - outsidePoint: ${JSON.stringify(outsidePoint)} - insidePoint : ${JSON.stringify(insidePoint)} - node : x:${bounds.x} y:${bounds.y} w:${bounds.width} h:${bounds.height}`, - polyPoints - ); - - const intersections = []; - - let minX = Number.POSITIVE_INFINITY; - let minY = Number.POSITIVE_INFINITY; - if (typeof polyPoints.forEach === 'function') { - polyPoints.forEach(function (entry) { - minX = Math.min(minX, entry.x); - minY = Math.min(minY, entry.y); - }); - } else { - minX = Math.min(minX, polyPoints.x); - minY = Math.min(minY, polyPoints.y); - } - - // const left = x1 - w / 2; - // const top = y1 + h / 2; - - for (let i = 0; i < polyPoints.length; i++) { - const p1 = polyPoints[i]; - const p2 = polyPoints[i < polyPoints.length - 1 ? i + 1 : 0]; - const intersect = intersectLine( - bounds, - outsidePoint, - { x: p1.x, y: p1.y }, - { x: p2.x, y: p2.y } - ); - - if (intersect) { - intersections.push(intersect); - } - } - - if (!intersections.length) { - return bounds; - } - - log.debug('UIO intersections', intersections); - - if (intersections.length > 1) { - // More intersections, find the one nearest to edge end point - intersections.sort(function (p, q) { - const pdx = p.x - outsidePoint.x; - const pdy = p.y - outsidePoint.y; - const distp = Math.sqrt(pdx * pdx + pdy * pdy); - - const qdx = q.x - outsidePoint.x; - const qdy = q.y - outsidePoint.y; - const distq = Math.sqrt(qdx * qdx + qdy * qdy); - - return distp < distq ? -1 : distp === distq ? 0 : 1; - }); - } - - return intersections[0]; -}; - -export const intersection = (node, outsidePoint, insidePoint) => { - log.debug(`intersection calc abc89: - outsidePoint: ${JSON.stringify(outsidePoint)} - insidePoint : ${JSON.stringify(insidePoint)} - node : x:${node.x} y:${node.y} w:${node.width} h:${node.height}`); - const x = node.x; - const y = node.y; - - const dx = Math.abs(x - insidePoint.x); - // const dy = Math.abs(y - insidePoint.y); - const w = node.width / 2; - let r = insidePoint.x < outsidePoint.x ? w - dx : w + dx; - const h = node.height / 2; - - const Q = Math.abs(outsidePoint.y - insidePoint.y); - const R = Math.abs(outsidePoint.x - insidePoint.x); - - if (Math.abs(y - outsidePoint.y) * w > Math.abs(x - outsidePoint.x) * h) { - // Intersection is top or bottom of rect. - const q = insidePoint.y < outsidePoint.y ? outsidePoint.y - h - y : y - h - outsidePoint.y; - r = (R * q) / Q; - const res = { - x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - R + r, - y: insidePoint.y < outsidePoint.y ? insidePoint.y + Q - q : insidePoint.y - Q + q, - }; - - if (r === 0) { - res.x = outsidePoint.x; - res.y = outsidePoint.y; - } - if (R === 0) { - res.x = outsidePoint.x; - } - if (Q === 0) { - res.y = outsidePoint.y; - } - - log.debug(`abc89 topp/bott calc, Q ${Q}, q ${q}, R ${R}, r ${r}`, res); // cspell: disable-line - - return res; - } else { - // Intersection onn sides of rect - if (insidePoint.x < outsidePoint.x) { - r = outsidePoint.x - w - x; - } else { - // r = outsidePoint.x - w - x; - r = x - w - outsidePoint.x; - } - const q = (Q * r) / R; - // OK let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x + dx - w; - // OK let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : outsidePoint.x + r; - let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x - R + r; - // let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : outsidePoint.x + r; - let _y = insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q; - log.debug(`sides calc abc89, Q ${Q}, q ${q}, R ${R}, r ${r}`, { _x, _y }); - if (r === 0) { - _x = outsidePoint.x; - _y = outsidePoint.y; - } - if (R === 0) { - _x = outsidePoint.x; - } - if (Q === 0) { - _y = outsidePoint.y; - } - - return { x: _x, y: _y }; - } -}; -const outsideNode = (node, point) => { - const x = node.x; - const y = node.y; - const dx = Math.abs(point.x - x); - const dy = Math.abs(point.y - y); - const w = node.width / 2; - const h = node.height / 2; - if (dx >= w || dy >= h) { - return true; - } - return false; -}; -/** - * This function will page a path and node where the last point(s) in the path is inside the node - * and return an update path ending by the border of the node. - */ -const cutPathAtIntersect = (_points, bounds, isDiamond: boolean) => { - log.debug('UIO cutPathAtIntersect Points:', _points, 'node:', bounds, 'isDiamond', isDiamond); - const points = []; - let lastPointOutside = _points[0]; - let isInside = false; - _points.forEach((point) => { - // const node = clusterDb[edge.toCluster].node; - log.debug(' checking point', point, bounds); - - // check if point is inside the boundary rect - if (!outsideNode(bounds, point) && !isInside) { - // First point inside the rect found - // Calc the intersection coord between the point anf the last point outside the rect - let inter; - - if (isDiamond) { - const inter2 = diamondIntersection(bounds, lastPointOutside, point); - const distance = Math.sqrt( - (lastPointOutside.x - inter2.x) ** 2 + (lastPointOutside.y - inter2.y) ** 2 - ); - if (distance > 1) { - inter = inter2; + let sw = startNode.width; + let ew = endNode.width; + if (startNode.isGroup) { + const bbox = startNode.domId.node().getBBox(); + // sw = Math.max(bbox.width, startNode.width, startNode.labels[0].width); + sw = Math.max(startNode.width, startNode.labels[0].width + startNode.padding); + // sw = startNode.width; + log.debug( + 'UIO width', + startNode.id, + startNode.with, + 'bbox.width=', + bbox.width, + 'lw=', + startNode.labels[0].width, + 'node:', + startNode.width, + 'SW = ', + sw + // 'HTML:', + // startNode.domId.node().innerHTML + ); } - } - if (!inter) { - inter = intersection(bounds, lastPointOutside, point); - } + if (endNode.isGroup) { + const bbox = endNode.domId.node().getBBox(); + ew = Math.max(endNode.width, endNode.labels[0].width + endNode.padding); - // Check case where the intersection is the same as the last point - let pointPresent = false; - points.forEach((p) => { - pointPresent = pointPresent || (p.x === inter.x && p.y === inter.y); - }); - // if (!pointPresent) { - if (!points.some((e) => e.x === inter.x && e.y === inter.y)) { - points.push(inter); - } else { - log.debug('abc88 no intersect', inter, points); - } - // points.push(inter); - isInside = true; - } else { - // Outside - log.debug('abc88 outside', point, lastPointOutside, points); - lastPointOutside = point; - // points.push(point); - if (!isInside) { - points.push(point); + log.debug( + 'UIO width', + startNode.id, + startNode.with, + bbox.width, + 'EW = ', + ew, + 'HTML:', + startNode.innerHTML + ); + } + if (startNode.shape === 'diamond') { + edge.points.unshift({ + x: startNode.x + startNode.width / 2 + offset.x, + y: startNode.y + startNode.height / 2 + offset.y, + }); + } + if (endNode.shape === 'diamond') { + const x = endNode.x + endNode.width / 2 + offset.x; + // Add a point at the center of the diamond + if ( + Math.abs(edge.points[edge.points.length - 1].y - endNode.y - offset.y) > 0.001 || + Math.abs(edge.points[edge.points.length - 1].x - x) > 0.001 + ) { + edge.points.push({ + x: endNode.x + endNode.width / 2 + offset.x, + y: endNode.y + endNode.height / 2 + offset.y, + }); + } + } + + edge.points = cutPathAtIntersect( + edge.points.reverse(), + { + x: startNode.x + startNode.width / 2 + offset.x, + y: startNode.y + startNode.height / 2 + offset.y, + width: sw, + height: startNode.height, + padding: startNode.padding, + }, + startNode.shape === 'diamond' + ).reverse(); + + edge.points = cutPathAtIntersect( + edge.points, + { + x: endNode.x + ew / 2 + endNode.offset.x, + y: endNode.y + endNode.height / 2 + endNode.offset.y, + width: ew, + height: endNode.height, + padding: endNode.padding, + }, + endNode.shape === 'diamond' + ); + + const paths = insertEdge( + edgesEl, + edge, + clusterDb, + data4Layout.type, + startNode, + endNode, + data4Layout.diagramId + ); + log.info('APA12 edge points after insert', JSON.stringify(edge.points)); + + edge.x = edge.labels[0].x + offset.x + edge.labels[0].width / 2; + edge.y = edge.labels[0].y + offset.y + edge.labels[0].height / 2; + positionEdgeLabel(edge, paths); } } - }); - log.debug('returning points', points); - return points; + ); }; diff --git a/packages/mermaid-zenuml/package.json b/packages/mermaid-zenuml/package.json index 1a47a99d8..192f4e0c4 100644 --- a/packages/mermaid-zenuml/package.json +++ b/packages/mermaid-zenuml/package.json @@ -19,8 +19,7 @@ "mermaid" ], "scripts": { - "clean": "rimraf dist", - "prepublishOnly": "pnpm -w run build" + "clean": "rimraf dist" }, "repository": { "type": "git", @@ -40,7 +39,7 @@ "mermaid": "workspace:^" }, "peerDependencies": { - "mermaid": "workspace:>=10.0.0" + "mermaid": "^10 || ^11" }, "files": [ "dist" diff --git a/packages/mermaid/CHANGELOG.md b/packages/mermaid/CHANGELOG.md new file mode 100644 index 000000000..5a34667fa --- /dev/null +++ b/packages/mermaid/CHANGELOG.md @@ -0,0 +1,32 @@ +# mermaid + +## 11.0.2 + +### Patch Changes + +- [#5664](https://github.com/mermaid-js/mermaid/pull/5664) [`5deaef4`](https://github.com/mermaid-js/mermaid/commit/5deaef456e74d796866431c26f69360e4e74dbff) Thanks [@Austin-Fulbright](https://github.com/Austin-Fulbright)! - chore: Migrate git graph to langium, use typescript for internals + +- Updated dependencies [[`5deaef4`](https://github.com/mermaid-js/mermaid/commit/5deaef456e74d796866431c26f69360e4e74dbff)]: + - @mermaid-js/parser@0.2.0 + +## 11.0.1 + +### Patch Changes + +- [#2](https://github.com/calvinvette/mermaid/pull/2) [`bf05d87`](https://github.com/mermaid-js/mermaid/commit/bf05d8781edacb580fdb053da167e968b7570117) Thanks [@calvinvette](https://github.com/calvinvette)! - test changeset + +## 11.0.2 + +### Patch Changes + +- Updated dependencies [[`83926c9`](https://github.com/mermaid-js/mermaid/commit/83926c9707b09c34e300888186250191ee8ae30a)]: + - @mermaid-js/parser@0.1.1 + +## 11.0.1 + +### Patch Changes + +- [#5744](https://github.com/mermaid-js/mermaid/pull/5744) [`5013484`](https://github.com/mermaid-js/mermaid/commit/50134849246141ec400e33e08c12c10539b84de9) Thanks [@sidharthv96](https://github.com/sidharthv96)! - Release parser, test changesets + +- Updated dependencies [[`5013484`](https://github.com/mermaid-js/mermaid/commit/50134849246141ec400e33e08c12c10539b84de9)]: + - @mermaid-js/parser@0.1.0 diff --git a/packages/mermaid/package.json b/packages/mermaid/package.json index f3a2d8299..8956eb1e5 100644 --- a/packages/mermaid/package.json +++ b/packages/mermaid/package.json @@ -1,6 +1,6 @@ { "name": "mermaid", - "version": "11.0.0-alpha.7", + "version": "11.0.2", "description": "Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.", "type": "module", "module": "./dist/mermaid.core.mjs", @@ -48,8 +48,7 @@ "types:build-config": "tsx scripts/create-types-from-json-schema.mts", "types:verify-config": "tsx scripts/create-types-from-json-schema.mts --verify", "checkCircle": "npx madge --circular ./src", - "release": "pnpm build", - "prepublishOnly": "cpy '../../README.*' ./ --cwd=. && pnpm docs:release-version && pnpm -w run build" + "prepublishOnly": "pnpm docs:verify-version" }, "repository": { "type": "git", @@ -106,7 +105,6 @@ "ajv": "^8.12.0", "chokidar": "^3.6.0", "concurrently": "^8.2.2", - "cpy-cli": "^5.0.0", "csstree-validator": "^3.0.0", "globby": "^14.0.1", "jison": "^0.4.18", diff --git a/packages/mermaid/src/defaultConfig.ts b/packages/mermaid/src/defaultConfig.ts index 0fa897d11..97f3e0bb1 100644 --- a/packages/mermaid/src/defaultConfig.ts +++ b/packages/mermaid/src/defaultConfig.ts @@ -248,19 +248,6 @@ const config: RequiredDeep = { ...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, - }, packet: { ...defaultConfigJson.packet, }, diff --git a/packages/mermaid/src/diagram-api/types.ts b/packages/mermaid/src/diagram-api/types.ts index 4556c1d6e..fdb175e52 100644 --- a/packages/mermaid/src/diagram-api/types.ts +++ b/packages/mermaid/src/diagram-api/types.ts @@ -129,6 +129,6 @@ export type HTML = d3.Selection; -export type Group = d3.Selection; +export type SVGGroup = d3.Selection; export type DiagramStylesProvider = (options?: any) => string; diff --git a/packages/mermaid/src/diagrams/common/svgDrawCommon.ts b/packages/mermaid/src/diagrams/common/svgDrawCommon.ts index 8d5ba3b7c..59c6d43cf 100644 --- a/packages/mermaid/src/diagrams/common/svgDrawCommon.ts +++ b/packages/mermaid/src/diagrams/common/svgDrawCommon.ts @@ -1,5 +1,6 @@ import { sanitizeUrl } from '@braintree/sanitize-url'; -import type { Group, SVG } from '../../diagram-api/types.js'; +import type { SVG, SVGGroup } from '../../diagram-api/types.js'; +import { lineBreakRegex } from './common.js'; import type { Bound, D3ImageElement, @@ -11,9 +12,8 @@ import type { TextData, TextObject, } from './commonTypes.js'; -import { lineBreakRegex } from './common.js'; -export const drawRect = (element: SVG | Group, rectData: RectData): D3RectElement => { +export const drawRect = (element: SVG | SVGGroup, rectData: RectData): D3RectElement => { const rectElement: D3RectElement = element.append('rect'); rectElement.attr('x', rectData.x); rectElement.attr('y', rectData.y); @@ -50,7 +50,7 @@ export const drawRect = (element: SVG | Group, rectData: RectData): D3RectElemen * @param element - Diagram (reference for bounds) * @param bounds - Shape of the rectangle */ -export const drawBackgroundRect = (element: SVG | Group, bounds: Bound): void => { +export const drawBackgroundRect = (element: SVG | SVGGroup, bounds: Bound): void => { const rectData: RectData = { x: bounds.startx, y: bounds.starty, @@ -64,7 +64,7 @@ export const drawBackgroundRect = (element: SVG | Group, bounds: Bound): void => rectElement.lower(); }; -export const drawText = (element: SVG | Group, textData: TextData): D3TextElement => { +export const drawText = (element: SVG | SVGGroup, textData: TextData): D3TextElement => { const nText: string = textData.text.replace(lineBreakRegex, ' '); const textElem: D3TextElement = element.append('text'); @@ -84,7 +84,7 @@ export const drawText = (element: SVG | Group, textData: TextData): D3TextElemen return textElem; }; -export const drawImage = (elem: SVG | Group, x: number, y: number, link: string): void => { +export const drawImage = (elem: SVG | SVGGroup, x: number, y: number, link: string): void => { const imageElement: D3ImageElement = elem.append('image'); imageElement.attr('x', x); imageElement.attr('y', y); @@ -93,7 +93,7 @@ export const drawImage = (elem: SVG | Group, x: number, y: number, link: string) }; export const drawEmbeddedImage = ( - element: SVG | Group, + element: SVG | SVGGroup, x: number, y: number, link: string diff --git a/packages/mermaid/src/diagrams/error/errorRenderer.ts b/packages/mermaid/src/diagrams/error/errorRenderer.ts index 1b3622c6d..a5f10acef 100644 --- a/packages/mermaid/src/diagrams/error/errorRenderer.ts +++ b/packages/mermaid/src/diagrams/error/errorRenderer.ts @@ -1,5 +1,5 @@ +import type { SVG, SVGGroup } from '../../diagram-api/types.js'; import { log } from '../../logger.js'; -import type { Group, SVG } from '../../diagram-api/types.js'; import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; import { configureSvgSize } from '../../setupGraphViewbox.js'; @@ -13,7 +13,7 @@ import { configureSvgSize } from '../../setupGraphViewbox.js'; export const draw = (_text: string, id: string, version: string) => { log.debug('rendering svg for syntax error\n'); const svg: SVG = selectSvgElement(id); - const g: Group = svg.append('g'); + const g: SVGGroup = svg.append('g'); svg.attr('viewBox', '0 0 2412 512'); configureSvgSize(svg, 100, 512, true); diff --git a/packages/mermaid/src/diagrams/flowchart/elk/detector.ts b/packages/mermaid/src/diagrams/flowchart/elk/detector.ts index b476ff11b..6688ffd8c 100644 --- a/packages/mermaid/src/diagrams/flowchart/elk/detector.ts +++ b/packages/mermaid/src/diagrams/flowchart/elk/detector.ts @@ -1,34 +1,26 @@ import type { - ExternalDiagramDefinition, DiagramDetector, DiagramLoader, + ExternalDiagramDefinition, } from '../../../diagram-api/types.js'; -import { log } from '../../../logger.js'; const id = 'flowchart-elk'; -const detector: DiagramDetector = (txt, config): boolean => { +const detector: DiagramDetector = (txt, config = {}): boolean => { if ( // If diagram explicitly states flowchart-elk /^\s*flowchart-elk/.test(txt) || // If a flowchart/graph diagram has their default renderer set to elk (/^\s*flowchart|graph/.test(txt) && config?.flowchart?.defaultRenderer === 'elk') ) { - // This will log at the end, hopefully. - setTimeout( - () => - log.warn( - 'flowchart-elk was moved to an external package in Mermaid v11. Please refer [release notes](link) for more details. This diagram will be rendered using `dagre` layout as a fallback.' - ), - 500 - ); + config.layout = 'elk'; return true; } return false; }; const loader: DiagramLoader = async () => { - const { diagram } = await import('../flowDiagram-v2.js'); + const { diagram } = await import('../flowDiagram.js'); return { id, diagram }; }; diff --git a/packages/mermaid/src/diagrams/flowchart/flowDetector-v2.ts b/packages/mermaid/src/diagrams/flowchart/flowDetector-v2.ts index dda5a67f0..b66afe4bf 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDetector-v2.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowDetector-v2.ts @@ -1,5 +1,8 @@ -import type { DiagramDetector, DiagramLoader } from '../../diagram-api/types.js'; -import type { ExternalDiagramDefinition } from '../../diagram-api/types.js'; +import type { + DiagramDetector, + DiagramLoader, + ExternalDiagramDefinition, +} from '../../diagram-api/types.js'; const id = 'flowchart-v2'; @@ -19,7 +22,7 @@ const detector: DiagramDetector = (txt, config) => { }; const loader: DiagramLoader = async () => { - const { diagram } = await import('./flowDiagram-v2.js'); + const { diagram } = await import('./flowDiagram.js'); return { id, diagram }; }; diff --git a/packages/mermaid/src/diagrams/flowchart/flowDiagram-v2.ts b/packages/mermaid/src/diagrams/flowchart/flowDiagram-v2.ts deleted file mode 100644 index 5b8012ede..000000000 --- a/packages/mermaid/src/diagrams/flowchart/flowDiagram-v2.ts +++ /dev/null @@ -1,23 +0,0 @@ -// @ts-ignore: JISON doesn't support types -import flowParser from './parser/flow.jison'; -import flowDb from './flowDb.js'; -import renderer from './flowRenderer-v3-unified.js'; -import flowStyles from './styles.js'; -import type { MermaidConfig } from '../../config.type.js'; -import { setConfig } from '../../diagram-api/diagramAPI.js'; - -export const diagram = { - parser: flowParser, - db: flowDb, - renderer, - styles: flowStyles, - init: (cnf: MermaidConfig) => { - if (!cnf.flowchart) { - cnf.flowchart = {}; - } - cnf.flowchart.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute; - setConfig({ flowchart: { arrowMarkerAbsolute: cnf.arrowMarkerAbsolute } }); - flowDb.clear(); - flowDb.setGen('gen-2'); - }, -}; diff --git a/packages/mermaid/src/diagrams/flowchart/flowDiagram.ts b/packages/mermaid/src/diagrams/flowchart/flowDiagram.ts index 5b8012ede..67cdf918f 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDiagram.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowDiagram.ts @@ -1,10 +1,10 @@ -// @ts-ignore: JISON doesn't support types -import flowParser from './parser/flow.jison'; -import flowDb from './flowDb.js'; -import renderer from './flowRenderer-v3-unified.js'; -import flowStyles from './styles.js'; import type { MermaidConfig } from '../../config.type.js'; import { setConfig } from '../../diagram-api/diagramAPI.js'; +import flowDb from './flowDb.js'; +import renderer from './flowRenderer-v3-unified.js'; +// @ts-ignore: JISON doesn't support types +import flowParser from './parser/flow.jison'; +import flowStyles from './styles.js'; export const diagram = { parser: flowParser, @@ -15,6 +15,9 @@ export const diagram = { if (!cnf.flowchart) { cnf.flowchart = {}; } + if (cnf.layout) { + setConfig({ layout: cnf.layout }); + } cnf.flowchart.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute; setConfig({ flowchart: { arrowMarkerAbsolute: cnf.arrowMarkerAbsolute } }); flowDb.clear(); diff --git a/packages/mermaid/src/diagrams/flowchart/flowRenderer-v3-unified.ts b/packages/mermaid/src/diagrams/flowchart/flowRenderer-v3-unified.ts index 102662ee6..6cc15258d 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowRenderer-v3-unified.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowRenderer-v3-unified.ts @@ -2,8 +2,8 @@ import { select } from 'd3'; import { getConfig } from '../../diagram-api/diagramAPI.js'; import type { DiagramStyleClassDef } from '../../diagram-api/types.js'; import { log } from '../../logger.js'; -import { getDiagramElements } from '../../rendering-util/insertElementsForSize.js'; -import { render } from '../../rendering-util/render.js'; +import { getDiagramElement } from '../../rendering-util/insertElementsForSize.js'; +import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js'; import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js'; import type { LayoutData } from '../../rendering-util/types.js'; import utils from '../../utils.js'; @@ -35,12 +35,17 @@ export const draw = async function (text: string, id: string, _version: string, log.debug('Before getData: '); const data4Layout = diag.db.getData() as LayoutData; log.debug('Data: ', data4Layout); - // Create the root SVG - the element is the div containing the SVG element - const { element, svg } = getDiagramElements(id, securityLevel); + // Create the root SVG + const svg = getDiagramElement(id, securityLevel); const direction = getDirection(); data4Layout.type = diag.type; - data4Layout.layoutAlgorithm = layout; + data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layout); + if (data4Layout.layoutAlgorithm === 'dagre' && layout === 'elk') { + log.warn( + 'flowchart-elk was moved to an external package in Mermaid v11. Please refer [release notes](https://github.com/mermaid-js/mermaid/releases/tag/v11.0.0) for more details. This diagram will be rendered using `dagre` layout as a fallback.' + ); + } data4Layout.direction = direction; data4Layout.nodeSpacing = conf?.nodeSpacing || 50; data4Layout.rankSpacing = conf?.rankSpacing || 50; @@ -48,8 +53,8 @@ export const draw = async function (text: string, id: string, _version: string, data4Layout.diagramId = id; log.debug('REF1:', data4Layout); - await render(data4Layout, svg, element); - const padding = data4Layout.config.flowchart?.padding ?? 8; + await render(data4Layout, svg); + const padding = data4Layout.config.flowchart?.diagramPadding ?? 8; utils.insertTitle( svg, 'flowchartTitleText', diff --git a/packages/mermaid/src/diagrams/git/gitGraph.spec.ts b/packages/mermaid/src/diagrams/git/gitGraph.spec.ts new file mode 100644 index 000000000..9b3236f90 --- /dev/null +++ b/packages/mermaid/src/diagrams/git/gitGraph.spec.ts @@ -0,0 +1,1322 @@ +import { rejects } from 'assert'; +import { db } from './gitGraphAst.js'; +import { parser } from './gitGraphParser.js'; + +describe('when parsing a gitGraph', function () { + beforeEach(function () { + db.clear(); + }); + describe('when parsing basic gitGraph', function () { + it('should handle a gitGraph definition', async () => { + const str = `gitGraph:\n commit\n`; + + await parser.parse(str); + const commits = db.getCommits(); + + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + }); + + it('should handle set direction top to bottom', async () => { + const str = 'gitGraph TB:\n' + 'commit\n'; + + await parser.parse(str); + const commits = db.getCommits(); + + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('TB'); + expect(db.getBranches().size).toBe(1); + }); + + it('should handle set direction bottom to top', async () => { + const str = 'gitGraph BT:\n' + 'commit\n'; + + await parser.parse(str); + const commits = db.getCommits(); + + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('BT'); + expect(db.getBranches().size).toBe(1); + }); + + it('should checkout a branch', async () => { + const str = 'gitGraph:\n' + 'branch new\n' + 'checkout new\n'; + + await parser.parse(str); + const commits = db.getCommits(); + + expect(commits.size).toBe(0); + expect(db.getCurrentBranch()).toBe('new'); + }); + + it('should switch a branch', async () => { + const str = 'gitGraph:\n' + 'branch new\n' + 'switch new\n'; + + await parser.parse(str); + const commits = db.getCommits(); + + expect(commits.size).toBe(0); + expect(db.getCurrentBranch()).toBe('new'); + }); + + it('should add commits to checked out branch', async () => { + const str = 'gitGraph:\n' + 'branch new\n' + 'checkout new\n' + 'commit\n' + 'commit\n'; + + await parser.parse(str); + const commits = db.getCommits(); + + expect(commits.size).toBe(2); + expect(db.getCurrentBranch()).toBe('new'); + const branchCommit = db.getBranches().get('new'); + expect(branchCommit).not.toBeNull(); + if (branchCommit) { + expect(commits.get(branchCommit)?.parents).not.toBeNull(); + } + }); + it('should handle commit with args', async () => { + const str = 'gitGraph:\n' + 'commit "a commit"\n'; + + await parser.parse(str); + const commits = db.getCommits(); + + expect(commits.size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe('a commit'); + expect(db.getCurrentBranch()).toBe('main'); + }); + + it.skip('should reset a branch', async () => { + const str = + 'gitGraph:\n' + + 'commit\n' + + 'commit\n' + + 'branch newbranch\n' + + 'checkout newbranch\n' + + 'commit\n' + + 'reset main\n'; + + await parser.parse(str); + + const commits = db.getCommits(); + expect(commits.size).toBe(3); + expect(db.getCurrentBranch()).toBe('newbranch'); + expect(db.getBranches().get('newbranch')).toEqual(db.getBranches().get('main')); + expect(db.getHead()?.id).toEqual(db.getBranches().get('newbranch')); + }); + + it.skip('reset can take an argument', async () => { + const str = + 'gitGraph:\n' + + 'commit\n' + + 'commit\n' + + 'branch newbranch\n' + + 'checkout newbranch\n' + + 'commit\n' + + 'reset main^\n'; + + await parser.parse(str); + + const commits = db.getCommits(); + expect(commits.size).toBe(3); + expect(db.getCurrentBranch()).toBe('newbranch'); + const branch = db.getBranches().get('main'); + const main = commits.get(branch ?? ''); + expect(db.getHead()?.id).toEqual(main?.parents); + }); + + it.skip('should handle fast forwardable merges', async () => { + const str = + 'gitGraph:\n' + + 'commit\n' + + 'branch newbranch\n' + + 'checkout newbranch\n' + + 'commit\n' + + 'commit\n' + + 'checkout main\n' + + 'merge newbranch\n'; + + await parser.parse(str); + + const commits = db.getCommits(); + expect(commits.size).toBe(4); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getBranches().get('newbranch')).toEqual(db.getBranches().get('main')); + expect(db.getHead()?.id).toEqual(db.getBranches().get('newbranch')); + }); + + it('should handle cases when merge is a noop', async () => { + const str = + 'gitGraph:\n' + + 'commit\n' + + 'branch newbranch\n' + + 'checkout newbranch\n' + + 'commit\n' + + 'commit\n' + + 'merge main\n'; + + await parser.parse(str); + + const commits = db.getCommits(); + expect(commits.size).toBe(4); + expect(db.getCurrentBranch()).toBe('newbranch'); + expect(db.getBranches().get('newbranch')).not.toEqual(db.getBranches().get('main')); + expect(db.getHead()?.id).toEqual(db.getBranches().get('newbranch')); + }); + + it('should handle merge with 2 parents', async () => { + const str = + 'gitGraph:\n' + + 'commit\n' + + 'branch newbranch\n' + + 'checkout newbranch\n' + + 'commit\n' + + 'commit\n' + + 'checkout main\n' + + 'commit\n' + + 'merge newbranch\n'; + + await parser.parse(str); + + const commits = db.getCommits(); + expect(commits.size).toBe(5); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getBranches().get('newbranch')).not.toEqual(db.getBranches().get('main')); + expect(db.getHead()?.id).toEqual(db.getBranches().get('main')); + }); + + it.skip('should handle ff merge when history walk has two parents (merge commit)', async () => { + const str = + 'gitGraph:\n' + + 'commit\n' + + 'branch newbranch\n' + + 'checkout newbranch\n' + + 'commit\n' + + 'commit\n' + + 'checkout main\n' + + 'commit\n' + + 'merge newbranch\n' + + 'commit\n' + + 'checkout newbranch\n' + + 'merge main\n'; + + await parser.parse(str); + + const commits = db.getCommits(); + expect(commits.size).toBe(7); + expect(db.getCurrentBranch()).toBe('newbranch'); + expect(db.getBranches().get('newbranch')).toEqual(db.getBranches().get('main')); + expect(db.getHead()?.id).toEqual(db.getBranches().get('main')); + + db.prettyPrint(); + }); + + it('should generate an array of known branches', async () => { + const str = + 'gitGraph:\n' + + 'commit\n' + + 'branch b1\n' + + 'checkout b1\n' + + 'commit\n' + + 'commit\n' + + 'branch b2\n'; + + await parser.parse(str); + const branches = db.getBranchesAsObjArray(); + + expect(branches).toHaveLength(3); + expect(branches[0]).toHaveProperty('name', 'main'); + expect(branches[1]).toHaveProperty('name', 'b1'); + expect(branches[2]).toHaveProperty('name', 'b2'); + }); + }); + + describe('when parsing more advanced gitGraphs', () => { + it('should handle a gitGraph commit with NO params, get auto-generated read-only ID', async () => { + const str = `gitGraph: + commit + `; + await parser.parse(str); + const commits = db.getCommits(); + //console.info(commits); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe(''); + expect(commits.get(key)?.id).not.toBeNull(); + expect(commits.get(key)?.tags).toStrictEqual([]); + expect(commits.get(key)?.type).toBe(0); + }); + + it('should handle a gitGraph commit with custom commit id only', async () => { + const str = `gitGraph: + commit id:"1111" + `; + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe(''); + expect(commits.get(key)?.id).toBe('1111'); + expect(commits.get(key)?.tags).toStrictEqual([]); + expect(commits.get(key)?.type).toBe(0); + }); + + it('should handle a gitGraph commit with custom commit tag only', async () => { + const str = `gitGraph: + commit tag:"test" + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe(''); + expect(commits.get(key)?.id).not.toBeNull(); + expect(commits.get(key)?.tags).toStrictEqual(['test']); + expect(commits.get(key)?.type).toBe(0); + }); + + it('should handle a gitGraph commit with custom commit type HIGHLIGHT only', async () => { + const str = `gitGraph: + commit type: HIGHLIGHT + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe(''); + expect(commits.get(key)?.id).not.toBeNull(); + expect(commits.get(key)?.tags).toStrictEqual([]); + expect(commits.get(key)?.type).toBe(2); + }); + + it('should handle a gitGraph commit with custom commit type REVERSE only', async () => { + const str = `gitGraph: + commit type: REVERSE + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe(''); + expect(commits.get(key)?.id).not.toBeNull(); + expect(commits.get(key)?.tags).toStrictEqual([]); + expect(commits.get(key)?.type).toBe(1); + }); + + it('should handle a gitGraph commit with custom commit type NORMAL only', async () => { + const str = `gitGraph: + commit type: NORMAL + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe(''); + expect(commits.get(key)?.id).not.toBeNull(); + expect(commits.get(key)?.tags).toStrictEqual([]); + expect(commits.get(key)?.type).toBe(0); + }); + + it('should handle a gitGraph commit with custom commit msg only', async () => { + const str = `gitGraph: + commit "test commit" + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe('test commit'); + expect(commits.get(key)?.id).not.toBeNull(); + expect(commits.get(key)?.tags).toStrictEqual([]); + expect(commits.get(key)?.type).toBe(0); + }); + + it('should handle a gitGraph commit with custom commit "msg:" key only', async () => { + const str = `gitGraph: + commit msg: "test commit" + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe('test commit'); + expect(commits.get(key)?.id).not.toBeNull(); + expect(commits.get(key)?.tags).toStrictEqual([]); + expect(commits.get(key)?.type).toBe(0); + }); + + it('should handle a gitGraph commit with custom commit id, tag only', async () => { + const str = `gitGraph: + commit id:"1111" tag: "test tag" + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe(''); + expect(commits.get(key)?.id).toBe('1111'); + expect(commits.get(key)?.tags).toStrictEqual(['test tag']); + expect(commits.get(key)?.type).toBe(0); + }); + + it('should handle a gitGraph commit with custom commit type, tag only', async () => { + const str = `gitGraph: + commit type:HIGHLIGHT tag: "test tag" + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe(''); + expect(commits.get(key)?.id).not.toBeNull(); + expect(commits.get(key)?.tags).toStrictEqual(['test tag']); + expect(commits.get(key)?.type).toBe(2); + }); + + it('should handle a gitGraph commit with custom commit tag and type only', async () => { + const str = `gitGraph: + commit tag: "test tag" type:HIGHLIGHT + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe(''); + expect(commits.get(key)?.id).not.toBeNull(); + expect(commits.get(key)?.tags).toStrictEqual(['test tag']); + expect(commits.get(key)?.type).toBe(2); + }); + + it('should handle a gitGraph commit with custom commit id, type and tag only', async () => { + const str = `gitGraph: + commit id:"1111" type:REVERSE tag: "test tag" + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe(''); + expect(commits.get(key)?.id).toBe('1111'); + expect(commits.get(key)?.tags).toStrictEqual(['test tag']); + expect(commits.get(key)?.type).toBe(1); + }); + + it('should handle a gitGraph commit with custom commit id, type, tag and msg', async () => { + const str = `gitGraph: + commit id:"1111" type:REVERSE tag: "test tag" msg:"test msg" + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe('test msg'); + expect(commits.get(key)?.id).toBe('1111'); + expect(commits.get(key)?.tags).toStrictEqual(['test tag']); + expect(commits.get(key)?.type).toBe(1); + }); + + it('should handle a gitGraph commit with custom type,tag, msg, commit id,', async () => { + const str = `gitGraph: + commit type:REVERSE tag: "test tag" msg: "test msg" id: "1111" + + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe('test msg'); + expect(commits.get(key)?.id).toBe('1111'); + expect(commits.get(key)?.tags).toStrictEqual(['test tag']); + expect(commits.get(key)?.type).toBe(1); + }); + + it('should handle a gitGraph commit with custom tag, msg, commit id, type,', async () => { + const str = `gitGraph: + commit tag: "test tag" msg:"test msg" id:"1111" type:REVERSE + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe('test msg'); + expect(commits.get(key)?.id).toBe('1111'); + expect(commits.get(key)?.tags).toStrictEqual(['test tag']); + expect(commits.get(key)?.type).toBe(1); + }); + + it('should handle a gitGraph commit with custom msg, commit id, type,tag', async () => { + const str = `gitGraph: + commit msg:"test msg" id:"1111" type:REVERSE tag: "test tag" + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe('test msg'); + expect(commits.get(key)?.id).toBe('1111'); + expect(commits.get(key)?.tags).toStrictEqual(['test tag']); + expect(commits.get(key)?.type).toBe(1); + }); + + it('should handle 3 straight commits', async () => { + const str = `gitGraph: + commit + commit + commit + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(3); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + }); + + it('should handle new branch creation', async () => { + const str = `gitGraph: + commit + branch testBranch + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('testBranch'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); + }); + + it('should allow quoted branch names', async () => { + const str = `gitGraph: + commit + branch "branch" + checkout "branch" + commit + checkout main + merge "branch" + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(3); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); + const [commit1, commit2, commit3] = commits.keys(); + expect(commits.get(commit1)?.branch).toBe('main'); + expect(commits.get(commit2)?.branch).toBe('branch'); + expect(commits.get(commit3)?.branch).toBe('main'); + expect(db.getBranchesAsObjArray()).toStrictEqual([{ name: 'main' }, { name: 'branch' }]); + }); + + it('should allow _-./ characters in branch names', async () => { + const str = `gitGraph: + commit + branch azAZ_-./test + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('azAZ_-./test'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); + }); + + it('should allow branch names starting with numbers', async () => { + const str = `gitGraph: + commit + %% branch names starting with numbers are not recommended, but are supported by git + branch 1.0.1 + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('1.0.1'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); + }); + + it('should allow branch names starting with unusual prefixes', async () => { + const str = `gitGraph: + commit + %% branch names starting with numbers are not recommended, but are supported by git + branch branch01 + branch checkout02 + branch cherry-pick03 + branch branch/example-branch + branch merge/test_merge + %% single character branch name + branch A + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('A'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(7); + expect([...db.getBranches().keys()]).toEqual( + expect.arrayContaining([ + 'branch01', + 'checkout02', + 'cherry-pick03', + 'branch/example-branch', + 'merge/test_merge', + 'A', + ]) + ); + }); + + it('should handle new branch checkout', async () => { + const str = `gitGraph: + commit + branch testBranch + checkout testBranch + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('testBranch'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); + }); + it('should handle new branch checkout with order', async () => { + const str = `gitGraph: + commit + branch test1 order: 3 + branch test2 order: 2 + branch test3 order: 1 + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('test3'); + expect(db.getBranches().size).toBe(4); + expect(db.getBranchesAsObjArray()).toStrictEqual([ + { name: 'main' }, + { name: 'test3' }, + { name: 'test2' }, + { name: 'test1' }, + ]); + }); + it('should handle new branch checkout with and without order', async () => { + const str = `gitGraph: + commit + branch test1 order: 1 + branch test2 + branch test3 + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('test3'); + expect(db.getBranches().size).toBe(4); + expect(db.getBranchesAsObjArray()).toStrictEqual([ + { name: 'main' }, + { name: 'test2' }, + { name: 'test3' }, + { name: 'test1' }, + ]); + }); + + it('should handle new branch checkout & commit', async () => { + const str = `gitGraph: + commit + branch testBranch + checkout testBranch + commit + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(2); + expect(db.getCurrentBranch()).toBe('testBranch'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); + const [commit1, commit2] = commits.keys(); + expect(commits.get(commit1)?.branch).toBe('main'); + expect(commits.get(commit1)?.parents).toStrictEqual([]); + expect(commits.get(commit2)?.branch).toBe('testBranch'); + expect(commits.get(commit2)?.parents).toStrictEqual([commit1]); + }); + + it('should handle new branch checkout & commit and merge', async () => { + const str = `gitGraph: + commit + branch testBranch + checkout testBranch + commit + commit + checkout main + merge testBranch + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(4); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); + const [commit1, commit2, commit3, commit4] = commits.keys(); + expect(commits.get(commit1)?.branch).toBe('main'); + expect(commits.get(commit1)?.parents).toStrictEqual([]); + expect(commits.get(commit2)?.branch).toBe('testBranch'); + expect(commits.get(commit2)?.parents).toStrictEqual([commits.get(commit1)?.id]); + expect(commits.get(commit3)?.branch).toBe('testBranch'); + expect(commits.get(commit3)?.parents).toStrictEqual([commits.get(commit2)?.id]); + expect(commits.get(commit4)?.branch).toBe('main'); + expect(commits.get(commit4)?.parents).toStrictEqual([ + commits.get(commit1)?.id, + commits.get(commit3)?.id, + ]); + expect(db.getBranchesAsObjArray()).toStrictEqual([{ name: 'main' }, { name: 'testBranch' }]); + }); + + it('should handle new branch switch', async () => { + const str = `gitGraph: + commit + branch testBranch + switch testBranch + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('testBranch'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); + }); + + it('should handle new branch switch & commit', async () => { + const str = `gitGraph: + commit + branch testBranch + switch testBranch + commit + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(2); + expect(db.getCurrentBranch()).toBe('testBranch'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); + const [commit1, commit2] = commits.keys(); + expect(commits.get(commit1)?.branch).toBe('main'); + expect(commits.get(commit1)?.parents).toStrictEqual([]); + expect(commits.get(commit2)?.branch).toBe('testBranch'); + expect(commits.get(commit2)?.parents).toStrictEqual([commit1]); + }); + + it('should handle new branch switch & commit and merge', async () => { + const str = `gitGraph: + commit + branch testBranch + switch testBranch + commit + commit + switch main + merge testBranch + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(4); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); + const [commit1, commit2, commit3, commit4] = commits.keys(); + expect(commits.get(commit1)?.branch).toBe('main'); + expect(commits.get(commit1)?.parents).toStrictEqual([]); + expect(commits.get(commit2)?.branch).toBe('testBranch'); + expect(commits.get(commit2)?.parents).toStrictEqual([commits.get(commit1)?.id]); + expect(commits.get(commit3)?.branch).toBe('testBranch'); + expect(commits.get(commit3)?.parents).toStrictEqual([commits.get(commit2)?.id]); + expect(commits.get(commit4)?.branch).toBe('main'); + expect(commits.get(commit4)?.parents).toStrictEqual([ + commits.get(commit1)?.id, + commits.get(commit3)?.id, + ]); + expect(db.getBranchesAsObjArray()).toStrictEqual([{ name: 'main' }, { name: 'testBranch' }]); + }); + + it('should handle merge tags', async () => { + const str = `gitGraph: + commit + branch testBranch + checkout testBranch + commit + checkout main + merge testBranch tag: "merge-tag" + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(3); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); + const [commit1, commit2, commit3] = commits.keys(); + expect(commits.get(commit1)?.branch).toBe('main'); + expect(commits.get(commit1)?.parents).toStrictEqual([]); + + expect(commits.get(commit2)?.branch).toBe('testBranch'); + expect(commits.get(commit2)?.parents).toStrictEqual([commits.get(commit1)?.id]); + + expect(commits.get(commit3)?.branch).toBe('main'); + expect(commits.get(commit3)?.parents).toStrictEqual([ + commits.get(commit1)?.id, + commits.get(commit2)?.id, + ]); + expect(commits.get(commit3)?.tags).toStrictEqual(['merge-tag']); + expect(db.getBranchesAsObjArray()).toStrictEqual([{ name: 'main' }, { name: 'testBranch' }]); + }); + + it('should handle merge with custom ids, tags and type', async () => { + const str = `gitGraph: + commit + branch testBranch + checkout testBranch + commit + checkout main + %% Merge Tag and ID + merge testBranch tag: "merge-tag" id: "2-222" + branch testBranch2 + checkout testBranch2 + commit + checkout main + %% Merge ID and Tag (reverse order) + merge testBranch2 id: "4-444" tag: "merge-tag2" type:HIGHLIGHT + branch testBranch3 + checkout testBranch3 + commit + checkout main + %% just Merge ID + merge testBranch3 id: "6-666" + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(7); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + + // The order of these commits is in alphabetical order of IDs + const [ + mainCommit, + testBranchCommit, + testBranchMerge, + testBranch2Commit, + testBranch2Merge, + testBranch3Commit, + testBranch3Merge, + ] = [...commits.values()]; + + expect(mainCommit.branch).toBe('main'); + expect(mainCommit.parents).toStrictEqual([]); + + expect(testBranchCommit.branch).toBe('testBranch'); + expect(testBranchCommit.parents).toStrictEqual([mainCommit.id]); + + expect(testBranchMerge.branch).toBe('main'); + expect(testBranchMerge.parents).toStrictEqual([mainCommit.id, testBranchCommit.id]); + expect(testBranchMerge.tags).toStrictEqual(['merge-tag']); + expect(testBranchMerge.id).toBe('2-222'); + + expect(testBranch2Merge.branch).toBe('main'); + expect(testBranch2Merge.parents).toStrictEqual([testBranchMerge.id, testBranch2Commit.id]); + expect(testBranch2Merge.tags).toStrictEqual(['merge-tag2']); + expect(testBranch2Merge.id).toBe('4-444'); + expect(testBranch2Merge.customType).toBe(2); + expect(testBranch2Merge.customId).toBe(true); + + expect(testBranch3Merge.branch).toBe('main'); + expect(testBranch3Merge.parents).toStrictEqual([testBranch2Merge.id, testBranch3Commit.id]); + expect(testBranch3Merge.id).toBe('6-666'); + + expect(db.getBranchesAsObjArray()).toStrictEqual([ + { name: 'main' }, + { name: 'testBranch' }, + { name: 'testBranch2' }, + { name: 'testBranch3' }, + ]); + }); + + it('should support cherry-picking commits', async () => { + const str = `gitGraph + commit id: "ZERO" + branch develop + commit id:"A" + checkout main + cherry-pick id:"A" + `; + + await parser.parse(str); + const commits = db.getCommits(); + const cherryPickCommitID = [...commits.keys()][2]; + expect(commits.get(cherryPickCommitID)?.tags).toStrictEqual(['cherry-pick:A']); + expect(commits.get(cherryPickCommitID)?.branch).toBe('main'); + }); + + it('should support cherry-picking commits with custom tag', async () => { + const str = `gitGraph + commit id: "ZERO" + branch develop + commit id:"A" + checkout main + cherry-pick id:"A" tag:"MyTag" + `; + + await parser.parse(str); + const commits = db.getCommits(); + const cherryPickCommitID = [...commits.keys()][2]; + expect(commits.get(cherryPickCommitID)?.tags).toStrictEqual(['MyTag']); + expect(commits.get(cherryPickCommitID)?.branch).toBe('main'); + }); + + it('should support cherry-picking commits with no tag', async () => { + const str = `gitGraph + commit id: "ZERO" + branch develop + commit id:"A" + checkout main + cherry-pick id:"A" tag:"" + `; + + await parser.parse(str); + const commits = db.getCommits(); + const cherryPickCommitID = [...commits.keys()][2]; + expect(commits.get(cherryPickCommitID)?.tags).toStrictEqual([]); + expect(commits.get(cherryPickCommitID)?.branch).toBe('main'); + }); + + it('should support cherry-picking of merge commits', async () => { + const str = `gitGraph + commit id: "ZERO" + branch feature + branch release + checkout feature + commit id: "A" + commit id: "B" + checkout main + merge feature id: "M" + checkout release + cherry-pick id: "M" parent:"B" + `; + + await parser.parse(str); + const commits = db.getCommits(); + const cherryPickCommitID = [...commits.keys()][4]; + expect(commits.get(cherryPickCommitID)?.tags).toStrictEqual(['cherry-pick:M|parent:B']); + expect(commits.get(cherryPickCommitID)?.branch).toBe('release'); + }); + + it('should support cherry-picking of merge commits with tag', async () => { + const str = `gitGraph + commit id: "ZERO" + branch feature + branch release + checkout feature + commit id: "A" + commit id: "B" + checkout main + merge feature id: "M" + checkout release + cherry-pick id: "M" parent:"ZERO" tag: "v1.0" + `; + + await parser.parse(str); + const commits = db.getCommits(); + const cherryPickCommitID = [...commits.keys()][4]; + expect(commits.get(cherryPickCommitID)?.tags).toStrictEqual(['v1.0']); + expect(commits.get(cherryPickCommitID)?.branch).toBe('release'); + }); + + it('should support cherry-picking of merge commits with additional commit', async () => { + const str = `gitGraph + commit id: "ZERO" + branch feature + branch release + checkout feature + commit id: "A" + commit id: "B" + checkout main + merge feature id: "M" + checkout release + commit id: "C" + cherry-pick id: "M" tag: "v2.1:ZERO" parent:"ZERO" + commit id: "D" + `; + + await parser.parse(str); + const commits = db.getCommits(); + const cherryPickCommitID = [...commits.keys()][5]; + expect(commits.get(cherryPickCommitID)?.tags).toStrictEqual(['v2.1:ZERO']); + expect(commits.get(cherryPickCommitID)?.branch).toBe('release'); + }); + + it('should support cherry-picking of merge commits with empty tag', async () => { + const str = `gitGraph + commit id: "ZERO" + branch feature + branch release + checkout feature + commit id: "A" + commit id: "B" + checkout main + merge feature id: "M" + checkout release + commit id: "C" + cherry-pick id:"M" parent: "ZERO" tag:"" + commit id: "D" + cherry-pick id:"M" tag:"" parent: "B" + `; + + await parser.parse(str); + const commits = db.getCommits(); + const cherryPickCommitID = [...commits.keys()][5]; + const cherryPickCommitID2 = [...commits.keys()][7]; + expect(commits.get(cherryPickCommitID)?.tags).toStrictEqual([]); + expect(commits.get(cherryPickCommitID2)?.tags).toStrictEqual([]); + expect(commits.get(cherryPickCommitID)?.branch).toBe('release'); + }); + + it('should fail cherry-picking of merge commits if the parent of merge commits is not specified', async () => { + await expect( + parser.parse( + `gitGraph + commit id: "ZERO" + branch feature + branch release + checkout feature + commit id: "A" + commit id: "B" + checkout main + merge feature id: "M" + checkout release + commit id: "C" + cherry-pick id:"M" + ` + ) + ).rejects.toThrow( + 'Incorrect usage of cherry-pick: If the source commit is a merge commit, an immediate parent commit must be specified.' + ); + }); + + it('should fail cherry-picking of merge commits when the parent provided is not an immediate parent of cherry picked commit', async () => { + await expect( + parser.parse( + `gitGraph + commit id: "ZERO" + branch feature + branch release + checkout feature + commit id: "A" + commit id: "B" + checkout main + merge feature id: "M" + checkout release + commit id: "C" + cherry-pick id:"M" parent: "A" + ` + ) + ).rejects.toThrow( + 'Invalid operation: The specified parent commit is not an immediate parent of the cherry-picked commit.' + ); + }); + + it('should throw error when try to branch existing branch: main', async () => { + const str = `gitGraph + commit + branch testBranch + commit + branch main + commit + checkout main + merge testBranch + `; + + try { + await parser.parse(str); + expect(true).toBe(false); + } catch (e: any) { + expect(e.message).toBe( + 'Trying to create an existing branch. (Help: Either use a new name if you want create a new branch or try using "checkout main")' + ); + } + }); + it('should throw error when try to branch existing branch: testBranch', async () => { + const str = `gitGraph + commit + branch testBranch + commit + branch testBranch + commit + checkout main + merge testBranch + `; + + try { + await parser.parse(str); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e: any) { + expect(e.message).toBe( + 'Trying to create an existing branch. (Help: Either use a new name if you want create a new branch or try using "checkout testBranch")' + ); + } + }); + it('should throw error when try to checkout unknown branch: testBranch', async () => { + const str = `gitGraph + commit + checkout testBranch + commit + branch testBranch + commit + checkout main + merge testBranch + `; + + try { + await parser.parse(str); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e: any) { + expect(e.message).toBe( + 'Trying to checkout branch which is not yet created. (Help try using "branch testBranch")' + ); + } + }); + it('should throw error when trying to merge, when current branch has no commits', async () => { + const str = `gitGraph + merge testBranch + commit + checkout testBranch + commit + branch testBranch + commit + checkout main + merge testBranch + `; + + try { + await parser.parse(str); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e: any) { + expect(e.message).toBe('Incorrect usage of "merge". Current branch (main)has no commits'); + } + }); + it('should throw error when trying to merge unknown branch', async () => { + const str = `gitGraph + commit + merge testBranch + commit + checkout testBranch + commit + branch testBranch + commit + checkout main + merge testBranch + `; + + try { + await parser.parse(str); + expect(true).toBe(false); + } catch (e: any) { + expect(e.message).toBe( + 'Incorrect usage of "merge". Branch to be merged (testBranch) does not exist' + ); + } + }); + it('should throw error when trying to merge branch to itself', async () => { + const str = `gitGraph + commit + branch testBranch + merge testBranch + `; + + try { + await parser.parse(str); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e: any) { + expect(e.message).toBe('Incorrect usage of "merge". Cannot merge a branch to itself'); + } + }); + + it('should throw error when using existing id as merge ID', async () => { + const str = `gitGraph + commit id: "1-111" + branch testBranch + commit id: "2-222" + commit id: "3-333" + checkout main + merge testBranch id: "1-111" + `; + + try { + await parser.parse(str); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e: any) { + expect(e.message).toBe( + 'Incorrect usage of "merge". Commit with id:1-111 already exists, use different custom Id' + ); + } + }); + it('should throw error when trying to merge branches having same heads', async () => { + const str = `gitGraph + commit + branch testBranch + checkout main + merge testBranch + `; + + try { + await parser.parse(str); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e: any) { + expect(e.message).toBe('Incorrect usage of "merge". Both branches have same head'); + } + }); + it('should throw error when trying to merge branch which has no commits', async () => { + const str = `gitGraph + branch test1 + + checkout main + commit + merge test1 + `; + + try { + await parser.parse(str); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e: any) { + expect(e.message).toBe( + 'Incorrect usage of "merge". Branch to be merged (test1) has no commits' + ); + } + }); + describe('accessibility', () => { + it('should handle a title and a description (accDescr)', async () => { + const str = `gitGraph: + accTitle: This is a title + accDescr: This is a description + commit + `; + await parser.parse(str); + expect(db.getAccTitle()).toBe('This is a title'); + expect(db.getAccDescription()).toBe('This is a description'); + }); + it('should handle a title and a multiline description (accDescr)', async () => { + const str = `gitGraph: + accTitle: This is a title + accDescr { + This is a description + using multiple lines + } + commit + `; + await parser.parse(str); + expect(db.getAccTitle()).toBe('This is a title'); + expect(db.getAccDescription()).toBe('This is a description\nusing multiple lines'); + }); + }); + + describe('unsafe properties', () => { + for (const prop of ['__proto__', 'constructor']) { + it(`should work with custom commit id or branch name ${prop}`, async () => { + const str = `gitGraph + commit id:"${prop}" + branch ${prop} + checkout ${prop} + commit + checkout main + merge ${prop} + `; + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(3); + expect(commits.keys().next().value).toBe(prop); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getBranches().size).toBe(2); + expect(db.getBranchesAsObjArray()[1].name).toBe(prop); + }); + } + }); + }); +}); diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.js b/packages/mermaid/src/diagrams/git/gitGraphAst.js deleted file mode 100644 index cebc4fc3e..000000000 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.js +++ /dev/null @@ -1,535 +0,0 @@ -import { log } from '../../logger.js'; -import { random } from '../../utils.js'; -import { getConfig } from '../../diagram-api/diagramAPI.js'; -import common from '../common/common.js'; -import { - setAccTitle, - getAccTitle, - getAccDescription, - setAccDescription, - clear as commonClear, - setDiagramTitle, - getDiagramTitle, -} from '../common/commonDb.js'; - -let { mainBranchName, mainBranchOrder } = getConfig().gitGraph; -let commits = new Map(); -let head = null; -let branchesConfig = new Map(); -branchesConfig.set(mainBranchName, { name: mainBranchName, order: mainBranchOrder }); -let branches = new Map(); -branches.set(mainBranchName, head); -let curBranch = mainBranchName; -let direction = 'LR'; -let seq = 0; - -/** - * - */ -function getId() { - return random({ length: 7 }); -} - -// /** -// * @param currentCommit -// * @param otherCommit -// */ - -// function isFastForwardable(currentCommit, otherCommit) { -// log.debug('Entering isFastForwardable:', currentCommit.id, otherCommit.id); -// let cnt = 0; -// while (currentCommit.seq <= otherCommit.seq && currentCommit !== otherCommit && cnt < 1000) { -// cnt++; -// // only if other branch has more commits -// if (otherCommit.parent == null) break; -// if (Array.isArray(otherCommit.parent)) { -// log.debug('In merge commit:', otherCommit.parent); -// return ( -// isFastForwardable(currentCommit, commits.get(otherCommit.parent[0])) || -// isFastForwardable(currentCommit, commits.get(otherCommit.parent[1])) -// ); -// } else { -// otherCommit = commits.get(otherCommit.parent); -// } -// } -// log.debug(currentCommit.id, otherCommit.id); -// return currentCommit.id === otherCommit.id; -// } - -/** - * @param currentCommit - * @param otherCommit - */ -// function isReachableFrom(currentCommit, otherCommit) { -// const currentSeq = currentCommit.seq; -// const otherSeq = otherCommit.seq; -// if (currentSeq > otherSeq) return isFastForwardable(otherCommit, currentCommit); -// return false; -// } - -/** - * @param list - * @param fn - */ -function uniqBy(list, fn) { - const recordMap = Object.create(null); - return list.reduce((out, item) => { - const key = fn(item); - if (!recordMap[key]) { - recordMap[key] = true; - out.push(item); - } - return out; - }, []); -} - -export const setDirection = function (dir) { - direction = dir; -}; -let options = {}; -export const setOptions = function (rawOptString) { - log.debug('options str', rawOptString); - rawOptString = rawOptString?.trim(); - rawOptString = rawOptString || '{}'; - try { - options = JSON.parse(rawOptString); - } catch (e) { - log.error('error while parsing gitGraph options', e.message); - } -}; - -export const getOptions = function () { - return options; -}; - -export const commit = function (msg, id, type, tags) { - log.debug('Entering commit:', msg, id, type, tags); - const config = getConfig(); - id = common.sanitizeText(id, config); - msg = common.sanitizeText(msg, config); - tags = tags?.map((tag) => common.sanitizeText(tag, config)); - const commit = { - id: id ? id : seq + '-' + getId(), - message: msg, - seq: seq++, - type: type ? type : commitType.NORMAL, - tags: tags ?? [], - parents: head == null ? [] : [head.id], - branch: curBranch, - }; - head = commit; - commits.set(commit.id, commit); - branches.set(curBranch, commit.id); - log.debug('in pushCommit ' + commit.id); -}; - -export const branch = function (name, order) { - name = common.sanitizeText(name, getConfig()); - if (!branches.has(name)) { - branches.set(name, head != null ? head.id : null); - branchesConfig.set(name, { name, order: order ? parseInt(order, 10) : null }); - checkout(name); - log.debug('in createBranch'); - } else { - let error = new Error( - 'Trying to create an existing branch. (Help: Either use a new name if you want create a new branch or try using "checkout ' + - name + - '")' - ); - error.hash = { - text: 'branch ' + name, - token: 'branch ' + name, - line: '1', - loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 }, - expected: ['"checkout ' + name + '"'], - }; - throw error; - } -}; - -export const merge = function (otherBranch, custom_id, override_type, custom_tags) { - const config = getConfig(); - otherBranch = common.sanitizeText(otherBranch, config); - custom_id = common.sanitizeText(custom_id, config); - - const currentCommit = commits.get(branches.get(curBranch)); - const otherCommit = commits.get(branches.get(otherBranch)); - if (curBranch === otherBranch) { - let error = new Error('Incorrect usage of "merge". Cannot merge a branch to itself'); - error.hash = { - text: 'merge ' + otherBranch, - token: 'merge ' + otherBranch, - line: '1', - loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 }, - expected: ['branch abc'], - }; - throw error; - } else if (currentCommit === undefined || !currentCommit) { - let error = new Error( - 'Incorrect usage of "merge". Current branch (' + curBranch + ')has no commits' - ); - error.hash = { - text: 'merge ' + otherBranch, - token: 'merge ' + otherBranch, - line: '1', - loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 }, - expected: ['commit'], - }; - throw error; - } else if (!branches.has(otherBranch)) { - let error = new Error( - 'Incorrect usage of "merge". Branch to be merged (' + otherBranch + ') does not exist' - ); - error.hash = { - text: 'merge ' + otherBranch, - token: 'merge ' + otherBranch, - line: '1', - loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 }, - expected: ['branch ' + otherBranch], - }; - throw error; - } else if (otherCommit === undefined || !otherCommit) { - let error = new Error( - 'Incorrect usage of "merge". Branch to be merged (' + otherBranch + ') has no commits' - ); - error.hash = { - text: 'merge ' + otherBranch, - token: 'merge ' + otherBranch, - line: '1', - loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 }, - expected: ['"commit"'], - }; - throw error; - } else if (currentCommit === otherCommit) { - let error = new Error('Incorrect usage of "merge". Both branches have same head'); - error.hash = { - text: 'merge ' + otherBranch, - token: 'merge ' + otherBranch, - line: '1', - loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 }, - expected: ['branch abc'], - }; - throw error; - } else if (custom_id && commits.has(custom_id)) { - let error = new Error( - 'Incorrect usage of "merge". Commit with id:' + - custom_id + - ' already exists, use different custom Id' - ); - error.hash = { - text: 'merge ' + otherBranch + custom_id + override_type + custom_tags?.join(','), - token: 'merge ' + otherBranch + custom_id + override_type + custom_tags?.join(','), - line: '1', - loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 }, - expected: [ - `merge ${otherBranch} ${custom_id}_UNIQUE ${override_type} ${custom_tags?.join(',')}`, - ], - }; - - throw error; - } - // if (isReachableFrom(currentCommit, otherCommit)) { - // log.debug('Already merged'); - // return; - // } - // if (isFastForwardable(currentCommit, otherCommit)) { - // branches.set(curBranch, branches.get(otherBranch)); - // head = commits.get(branches.get(curBranch)); - // } else { - // create merge commit - const commit = { - id: custom_id ? custom_id : seq + '-' + getId(), - message: 'merged branch ' + otherBranch + ' into ' + curBranch, - seq: seq++, - parents: [head == null ? null : head.id, branches.get(otherBranch)], - branch: curBranch, - type: commitType.MERGE, - customType: override_type, - customId: custom_id ? true : false, - tags: custom_tags ? custom_tags : [], - }; - head = commit; - commits.set(commit.id, commit); - branches.set(curBranch, commit.id); - // } - log.debug(branches); - log.debug('in mergeBranch'); -}; - -export const cherryPick = function (sourceId, targetId, tags, parentCommitId) { - log.debug('Entering cherryPick:', sourceId, targetId, tags); - const config = getConfig(); - sourceId = common.sanitizeText(sourceId, config); - targetId = common.sanitizeText(targetId, config); - tags = tags?.map((tag) => common.sanitizeText(tag, config)); - parentCommitId = common.sanitizeText(parentCommitId, config); - - if (!sourceId || !commits.has(sourceId)) { - let error = new Error( - 'Incorrect usage of "cherryPick". Source commit id should exist and provided' - ); - error.hash = { - text: 'cherryPick ' + sourceId + ' ' + targetId, - token: 'cherryPick ' + sourceId + ' ' + targetId, - line: '1', - loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 }, - expected: ['cherry-pick abc'], - }; - throw error; - } - let sourceCommit = commits.get(sourceId); - let sourceCommitBranch = sourceCommit.branch; - if ( - parentCommitId && - !(Array.isArray(sourceCommit.parents) && sourceCommit.parents.includes(parentCommitId)) - ) { - let error = new Error( - 'Invalid operation: The specified parent commit is not an immediate parent of the cherry-picked commit.' - ); - throw error; - } - if (sourceCommit.type === commitType.MERGE && !parentCommitId) { - let error = new Error( - 'Incorrect usage of cherry-pick: If the source commit is a merge commit, an immediate parent commit must be specified.' - ); - throw error; - } - if (!targetId || !commits.has(targetId)) { - // cherry-pick source commit to current branch - - if (sourceCommitBranch === curBranch) { - let error = new Error( - 'Incorrect usage of "cherryPick". Source commit is already on current branch' - ); - error.hash = { - text: 'cherryPick ' + sourceId + ' ' + targetId, - token: 'cherryPick ' + sourceId + ' ' + targetId, - line: '1', - loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 }, - expected: ['cherry-pick abc'], - }; - throw error; - } - const currentCommit = commits.get(branches.get(curBranch)); - if (currentCommit === undefined || !currentCommit) { - let error = new Error( - 'Incorrect usage of "cherry-pick". Current branch (' + curBranch + ')has no commits' - ); - error.hash = { - text: 'cherryPick ' + sourceId + ' ' + targetId, - token: 'cherryPick ' + sourceId + ' ' + targetId, - line: '1', - loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 }, - expected: ['cherry-pick abc'], - }; - throw error; - } - const commit = { - id: seq + '-' + getId(), - message: 'cherry-picked ' + sourceCommit + ' into ' + curBranch, - seq: seq++, - parents: [head == null ? null : head.id, sourceCommit.id], - branch: curBranch, - type: commitType.CHERRY_PICK, - tags: tags - ? tags.filter(Boolean) - : [ - `cherry-pick:${sourceCommit.id}${ - sourceCommit.type === commitType.MERGE ? `|parent:${parentCommitId}` : '' - }`, - ], - }; - head = commit; - commits.set(commit.id, commit); - branches.set(curBranch, commit.id); - log.debug(branches); - log.debug('in cherryPick'); - } -}; -export const checkout = function (branch) { - branch = common.sanitizeText(branch, getConfig()); - if (!branches.has(branch)) { - let error = new Error( - 'Trying to checkout branch which is not yet created. (Help try using "branch ' + branch + '")' - ); - error.hash = { - text: 'checkout ' + branch, - token: 'checkout ' + branch, - line: '1', - loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 }, - expected: ['"branch ' + branch + '"'], - }; - throw error; - } else { - curBranch = branch; - const id = branches.get(curBranch); - head = commits.get(id); - } -}; - -// export const reset = function (commitRef) { -// log.debug('in reset', commitRef); -// const ref = commitRef.split(':')[0]; -// let parentCount = parseInt(commitRef.split(':')[1]); -// let commit = ref === 'HEAD' ? head : commits.get(branches.get(ref)); -// log.debug(commit, parentCount); -// while (parentCount > 0) { -// commit = commits.get(commit.parent); -// parentCount--; -// if (!commit) { -// const err = 'Critical error - unique parent commit not found during reset'; -// log.error(err); -// throw err; -// } -// } -// head = commit; -// branches[curBranch] = commit.id; -// }; - -/** - * @param arr - * @param key - * @param newVal - */ -function upsert(arr, key, newVal) { - const index = arr.indexOf(key); - if (index === -1) { - arr.push(newVal); - } else { - arr.splice(index, 1, newVal); - } -} - -/** @param commitArr */ -function prettyPrintCommitHistory(commitArr) { - const commit = commitArr.reduce((out, commit) => { - if (out.seq > commit.seq) { - return out; - } - return commit; - }, commitArr[0]); - let line = ''; - commitArr.forEach(function (c) { - if (c === commit) { - line += '\t*'; - } else { - line += '\t|'; - } - }); - const label = [line, commit.id, commit.seq]; - for (let branch in branches) { - if (branches.get(branch) === commit.id) { - label.push(branch); - } - } - log.debug(label.join(' ')); - if (commit.parents && commit.parents.length == 2) { - const newCommit = commits.get(commit.parents[0]); - upsert(commitArr, commit, newCommit); - commitArr.push(commits.get(commit.parents[1])); - } else if (commit.parents.length == 0) { - return; - } else { - const nextCommit = commits.get(commit.parents); - upsert(commitArr, commit, nextCommit); - } - commitArr = uniqBy(commitArr, (c) => c.id); - prettyPrintCommitHistory(commitArr); -} - -export const prettyPrint = function () { - log.debug(commits); - const node = getCommitsArray()[0]; - prettyPrintCommitHistory([node]); -}; - -export const clear = function () { - commits = new Map(); - head = null; - const { mainBranchName, mainBranchOrder } = getConfig().gitGraph; - branches = new Map(); - branches.set(mainBranchName, null); - branchesConfig = new Map(); - branchesConfig.set(mainBranchName, { name: mainBranchName, order: mainBranchOrder }); - curBranch = mainBranchName; - seq = 0; - commonClear(); -}; - -export const getBranchesAsObjArray = function () { - const branchesArray = [...branchesConfig.values()] - .map((branchConfig, i) => { - if (branchConfig.order !== null) { - return branchConfig; - } - return { - ...branchConfig, - order: parseFloat(`0.${i}`, 10), - }; - }) - .sort((a, b) => a.order - b.order) - .map(({ name }) => ({ name })); - - return branchesArray; -}; - -export const getBranches = function () { - return branches; -}; -export const getCommits = function () { - return commits; -}; -export const getCommitsArray = function () { - const commitArr = [...commits.values()]; - commitArr.forEach(function (o) { - log.debug(o.id); - }); - commitArr.sort((a, b) => a.seq - b.seq); - return commitArr; -}; -export const getCurrentBranch = function () { - return curBranch; -}; -export const getDirection = function () { - return direction; -}; -export const getHead = function () { - return head; -}; - -export const commitType = { - NORMAL: 0, - REVERSE: 1, - HIGHLIGHT: 2, - MERGE: 3, - CHERRY_PICK: 4, -}; - -export default { - getConfig: () => getConfig().gitGraph, - setDirection, - setOptions, - getOptions, - commit, - branch, - merge, - cherryPick, - checkout, - //reset, - prettyPrint, - clear, - getBranchesAsObjArray, - getBranches, - getCommits, - getCommitsArray, - getCurrentBranch, - getDirection, - getHead, - setAccTitle, - getAccTitle, - getAccDescription, - setAccDescription, - setDiagramTitle, - getDiagramTitle, - commitType, -}; diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts new file mode 100644 index 000000000..44597e9d7 --- /dev/null +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -0,0 +1,522 @@ +import { log } from '../../logger.js'; +import { cleanAndMerge, random } from '../../utils.js'; +import { getConfig as commonGetConfig } from '../../config.js'; +import common from '../common/common.js'; +import { + setAccTitle, + getAccTitle, + getAccDescription, + setAccDescription, + clear as commonClear, + setDiagramTitle, + getDiagramTitle, +} from '../common/commonDb.js'; +import type { + DiagramOrientation, + Commit, + GitGraphDB, + CommitDB, + MergeDB, + BranchDB, + CherryPickDB, +} from './gitGraphTypes.js'; +import { commitType } from './gitGraphTypes.js'; +import { ImperativeState } from '../../utils/imperativeState.js'; + +import DEFAULT_CONFIG from '../../defaultConfig.js'; + +import type { GitGraphDiagramConfig } from '../../config.type.js'; +interface GitGraphState { + commits: Map; + head: Commit | null; + branchConfig: Map; + branches: Map; + currBranch: string; + direction: DiagramOrientation; + seq: number; + options: any; +} + +const DEFAULT_GITGRAPH_CONFIG: Required = DEFAULT_CONFIG.gitGraph; +const getConfig = (): Required => { + const config = cleanAndMerge({ + ...DEFAULT_GITGRAPH_CONFIG, + ...commonGetConfig().gitGraph, + }); + return config; +}; + +const state = new ImperativeState(() => { + const config = getConfig(); + const mainBranchName = config.mainBranchName; + const mainBranchOrder = config.mainBranchOrder; + return { + mainBranchName, + commits: new Map(), + head: null, + branchConfig: new Map([[mainBranchName, { name: mainBranchName, order: mainBranchOrder }]]), + branches: new Map([[mainBranchName, null]]), + currBranch: mainBranchName, + direction: 'LR', + seq: 0, + options: {}, + }; +}); + +function getID() { + return random({ length: 7 }); +} + +/** + * @param list - list of items + * @param fn - function to get the key + */ +function uniqBy(list: any[], fn: (item: any) => any) { + const recordMap = Object.create(null); + return list.reduce((out, item) => { + const key = fn(item); + if (!recordMap[key]) { + recordMap[key] = true; + out.push(item); + } + return out; + }, []); +} + +export const setDirection = function (dir: DiagramOrientation) { + state.records.direction = dir; +}; + +export const setOptions = function (rawOptString: string) { + log.debug('options str', rawOptString); + rawOptString = rawOptString?.trim(); + rawOptString = rawOptString || '{}'; + try { + state.records.options = JSON.parse(rawOptString); + } catch (e: any) { + log.error('error while parsing gitGraph options', e.message); + } +}; + +export const getOptions = function () { + return state.records.options; +}; + +export const commit = function (commitDB: CommitDB) { + let msg = commitDB.msg; + let id = commitDB.id; + const type = commitDB.type; + let tags = commitDB.tags; + + log.info('commit', msg, id, type, tags); + log.debug('Entering commit:', msg, id, type, tags); + const config = getConfig(); + id = common.sanitizeText(id, config); + msg = common.sanitizeText(msg, config); + tags = tags?.map((tag) => common.sanitizeText(tag, config)); + const newCommit: Commit = { + id: id ? id : state.records.seq + '-' + getID(), + message: msg, + seq: state.records.seq++, + type: type ?? commitType.NORMAL, + tags: tags ?? [], + parents: state.records.head == null ? [] : [state.records.head.id], + branch: state.records.currBranch, + }; + state.records.head = newCommit; + log.info('main branch', config.mainBranchName); + state.records.commits.set(newCommit.id, newCommit); + state.records.branches.set(state.records.currBranch, newCommit.id); + log.debug('in pushCommit ' + newCommit.id); +}; + +export const branch = function (branchDB: BranchDB) { + let name = branchDB.name; + const order = branchDB.order; + name = common.sanitizeText(name, getConfig()); + if (state.records.branches.has(name)) { + throw new Error( + `Trying to create an existing branch. (Help: Either use a new name if you want create a new branch or try using "checkout ${name}")` + ); + } + + state.records.branches.set(name, state.records.head != null ? state.records.head.id : null); + state.records.branchConfig.set(name, { name, order }); + checkout(name); + log.debug('in createBranch'); +}; + +export const merge = (mergeDB: MergeDB): void => { + let otherBranch = mergeDB.branch; + let customId = mergeDB.id; + const overrideType = mergeDB.type; + const customTags = mergeDB.tags; + const config = getConfig(); + otherBranch = common.sanitizeText(otherBranch, config); + if (customId) { + customId = common.sanitizeText(customId, config); + } + const currentBranchCheck = state.records.branches.get(state.records.currBranch); + const otherBranchCheck = state.records.branches.get(otherBranch); + const currentCommit = currentBranchCheck + ? state.records.commits.get(currentBranchCheck) + : undefined; + const otherCommit: Commit | undefined = otherBranchCheck + ? state.records.commits.get(otherBranchCheck) + : undefined; + if (currentCommit && otherCommit && currentCommit.branch === otherBranch) { + throw new Error(`Cannot merge branch '${otherBranch}' into itself.`); + } + if (state.records.currBranch === otherBranch) { + const error: any = new Error('Incorrect usage of "merge". Cannot merge a branch to itself'); + error.hash = { + text: `merge ${otherBranch}`, + token: `merge ${otherBranch}`, + expected: ['branch abc'], + }; + throw error; + } + if (currentCommit === undefined || !currentCommit) { + const error: any = new Error( + `Incorrect usage of "merge". Current branch (${state.records.currBranch})has no commits` + ); + error.hash = { + text: `merge ${otherBranch}`, + token: `merge ${otherBranch}`, + expected: ['commit'], + }; + throw error; + } + if (!state.records.branches.has(otherBranch)) { + const error: any = new Error( + 'Incorrect usage of "merge". Branch to be merged (' + otherBranch + ') does not exist' + ); + error.hash = { + text: `merge ${otherBranch}`, + token: `merge ${otherBranch}`, + expected: [`branch ${otherBranch}`], + }; + throw error; + } + if (otherCommit === undefined || !otherCommit) { + const error: any = new Error( + 'Incorrect usage of "merge". Branch to be merged (' + otherBranch + ') has no commits' + ); + error.hash = { + text: `merge ${otherBranch}`, + token: `merge ${otherBranch}`, + expected: ['"commit"'], + }; + throw error; + } + if (currentCommit === otherCommit) { + const error: any = new Error('Incorrect usage of "merge". Both branches have same head'); + error.hash = { + text: `merge ${otherBranch}`, + token: `merge ${otherBranch}`, + expected: ['branch abc'], + }; + throw error; + } + if (customId && state.records.commits.has(customId)) { + const error: any = new Error( + 'Incorrect usage of "merge". Commit with id:' + + customId + + ' already exists, use different custom Id' + ); + error.hash = { + text: `merge ${otherBranch} ${customId} ${overrideType} ${customTags?.join(' ')}`, + token: `merge ${otherBranch} ${customId} ${overrideType} ${customTags?.join(' ')}`, + expected: [ + `merge ${otherBranch} ${customId}_UNIQUE ${overrideType} ${customTags?.join(' ')}`, + ], + }; + + throw error; + } + + const verifiedBranch: string = otherBranchCheck ? otherBranchCheck : ''; //figure out a cleaner way to do this + + const commit = { + id: customId || `${state.records.seq}-${getID()}`, + message: `merged branch ${otherBranch} into ${state.records.currBranch}`, + seq: state.records.seq++, + parents: state.records.head == null ? [] : [state.records.head.id, verifiedBranch], + branch: state.records.currBranch, + type: commitType.MERGE, + customType: overrideType, + customId: customId ? true : false, + tags: customTags ?? [], + } satisfies Commit; + state.records.head = commit; + state.records.commits.set(commit.id, commit); + state.records.branches.set(state.records.currBranch, commit.id); + log.debug(state.records.branches); + log.debug('in mergeBranch'); +}; + +export const cherryPick = function (cherryPickDB: CherryPickDB) { + let sourceId = cherryPickDB.id; + let targetId = cherryPickDB.targetId; + let tags = cherryPickDB.tags; + let parentCommitId = cherryPickDB.parent; + log.debug('Entering cherryPick:', sourceId, targetId, tags); + const config = getConfig(); + sourceId = common.sanitizeText(sourceId, config); + targetId = common.sanitizeText(targetId, config); + + tags = tags?.map((tag) => common.sanitizeText(tag, config)); + + parentCommitId = common.sanitizeText(parentCommitId, config); + + if (!sourceId || !state.records.commits.has(sourceId)) { + const error: any = new Error( + 'Incorrect usage of "cherryPick". Source commit id should exist and provided' + ); + error.hash = { + text: `cherryPick ${sourceId} ${targetId}`, + token: `cherryPick ${sourceId} ${targetId}`, + expected: ['cherry-pick abc'], + }; + throw error; + } + + const sourceCommit = state.records.commits.get(sourceId); + if (sourceCommit === undefined || !sourceCommit) { + throw new Error('Incorrect usage of "cherryPick". Source commit id should exist and provided'); + } + if ( + parentCommitId && + !(Array.isArray(sourceCommit.parents) && sourceCommit.parents.includes(parentCommitId)) + ) { + const error = new Error( + 'Invalid operation: The specified parent commit is not an immediate parent of the cherry-picked commit.' + ); + throw error; + } + const sourceCommitBranch = sourceCommit.branch; + if (sourceCommit.type === commitType.MERGE && !parentCommitId) { + const error = new Error( + 'Incorrect usage of cherry-pick: If the source commit is a merge commit, an immediate parent commit must be specified.' + ); + throw error; + } + if (!targetId || !state.records.commits.has(targetId)) { + // cherry-pick source commit to current branch + + if (sourceCommitBranch === state.records.currBranch) { + const error: any = new Error( + 'Incorrect usage of "cherryPick". Source commit is already on current branch' + ); + error.hash = { + text: `cherryPick ${sourceId} ${targetId}`, + token: `cherryPick ${sourceId} ${targetId}`, + expected: ['cherry-pick abc'], + }; + throw error; + } + const currentCommitId = state.records.branches.get(state.records.currBranch); + if (currentCommitId === undefined || !currentCommitId) { + const error: any = new Error( + `Incorrect usage of "cherry-pick". Current branch (${state.records.currBranch})has no commits` + ); + error.hash = { + text: `cherryPick ${sourceId} ${targetId}`, + token: `cherryPick ${sourceId} ${targetId}`, + expected: ['cherry-pick abc'], + }; + throw error; + } + + const currentCommit = state.records.commits.get(currentCommitId); + if (currentCommit === undefined || !currentCommit) { + const error: any = new Error( + `Incorrect usage of "cherry-pick". Current branch (${state.records.currBranch})has no commits` + ); + error.hash = { + text: `cherryPick ${sourceId} ${targetId}`, + token: `cherryPick ${sourceId} ${targetId}`, + expected: ['cherry-pick abc'], + }; + throw error; + } + const commit = { + id: state.records.seq + '-' + getID(), + message: `cherry-picked ${sourceCommit?.message} into ${state.records.currBranch}`, + seq: state.records.seq++, + parents: state.records.head == null ? [] : [state.records.head.id, sourceCommit.id], + branch: state.records.currBranch, + type: commitType.CHERRY_PICK, + tags: tags + ? tags.filter(Boolean) + : [ + `cherry-pick:${sourceCommit.id}${ + sourceCommit.type === commitType.MERGE ? `|parent:${parentCommitId}` : '' + }`, + ], + }; + + state.records.head = commit; + state.records.commits.set(commit.id, commit); + state.records.branches.set(state.records.currBranch, commit.id); + log.debug(state.records.branches); + log.debug('in cherryPick'); + } +}; +export const checkout = function (branch: string) { + branch = common.sanitizeText(branch, getConfig()); + if (!state.records.branches.has(branch)) { + const error: any = new Error( + `Trying to checkout branch which is not yet created. (Help try using "branch ${branch}")` + ); + error.hash = { + text: `checkout ${branch}`, + token: `checkout ${branch}`, + expected: [`branch ${branch}`], + }; + throw error; + } else { + state.records.currBranch = branch; + const id = state.records.branches.get(state.records.currBranch); + if (id === undefined || !id) { + state.records.head = null; + } else { + state.records.head = state.records.commits.get(id) ?? null; + } + } +}; + +/** + * @param arr - array + * @param key - key + * @param newVal - new value + */ +function upsert(arr: any[], key: any, newVal: any) { + const index = arr.indexOf(key); + if (index === -1) { + arr.push(newVal); + } else { + arr.splice(index, 1, newVal); + } +} + +function prettyPrintCommitHistory(commitArr: Commit[]) { + const commit = commitArr.reduce((out, commit) => { + if (out.seq > commit.seq) { + return out; + } + return commit; + }, commitArr[0]); + let line = ''; + commitArr.forEach(function (c) { + if (c === commit) { + line += '\t*'; + } else { + line += '\t|'; + } + }); + const label = [line, commit.id, commit.seq]; + for (const branch in state.records.branches) { + if (state.records.branches.get(branch) === commit.id) { + label.push(branch); + } + } + log.debug(label.join(' ')); + if (commit.parents && commit.parents.length == 2 && commit.parents[0] && commit.parents[1]) { + const newCommit = state.records.commits.get(commit.parents[0]); + upsert(commitArr, commit, newCommit); + if (commit.parents[1]) { + commitArr.push(state.records.commits.get(commit.parents[1])!); + } + } else if (commit.parents.length == 0) { + return; + } else { + if (commit.parents[0]) { + const newCommit = state.records.commits.get(commit.parents[0]); + upsert(commitArr, commit, newCommit); + } + } + commitArr = uniqBy(commitArr, (c) => c.id); + prettyPrintCommitHistory(commitArr); +} + +export const prettyPrint = function () { + log.debug(state.records.commits); + const node = getCommitsArray()[0]; + prettyPrintCommitHistory([node]); +}; + +export const clear = function () { + state.reset(); + commonClear(); +}; + +export const getBranchesAsObjArray = function () { + const branchesArray = [...state.records.branchConfig.values()] + .map((branchConfig, i) => { + if (branchConfig.order !== null && branchConfig.order !== undefined) { + return branchConfig; + } + return { + ...branchConfig, + order: parseFloat(`0.${i}`), + }; + }) + .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) + .map(({ name }) => ({ name })); + + return branchesArray; +}; + +export const getBranches = function () { + return state.records.branches; +}; +export const getCommits = function () { + return state.records.commits; +}; +export const getCommitsArray = function () { + const commitArr = [...state.records.commits.values()]; + commitArr.forEach(function (o) { + log.debug(o.id); + }); + commitArr.sort((a, b) => a.seq - b.seq); + return commitArr; +}; +export const getCurrentBranch = function () { + return state.records.currBranch; +}; +export const getDirection = function () { + return state.records.direction; +}; +export const getHead = function () { + return state.records.head; +}; + +export const db: GitGraphDB = { + commitType, + getConfig, + setDirection, + setOptions, + getOptions, + commit, + branch, + merge, + cherryPick, + checkout, + //reset, + prettyPrint, + clear, + getBranchesAsObjArray, + getBranches, + getCommits, + getCommitsArray, + getCurrentBranch, + getDirection, + getHead, + setAccTitle, + getAccTitle, + getAccDescription, + setAccDescription, + setDiagramTitle, + getDiagramTitle, +}; diff --git a/packages/mermaid/src/diagrams/git/gitGraphDiagram.ts b/packages/mermaid/src/diagrams/git/gitGraphDiagram.ts index 2a9efdb59..d6e8a0613 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphDiagram.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphDiagram.ts @@ -1,13 +1,13 @@ // @ts-ignore: JISON doesn't support types -import gitGraphParser from './parser/gitGraph.jison'; -import gitGraphDb from './gitGraphAst.js'; +import { parser } from './gitGraphParser.js'; +import { db } from './gitGraphAst.js'; import gitGraphRenderer from './gitGraphRenderer.js'; import gitGraphStyles from './styles.js'; import type { DiagramDefinition } from '../../diagram-api/types.js'; export const diagram: DiagramDefinition = { - parser: gitGraphParser, - db: gitGraphDb, + parser, + db, renderer: gitGraphRenderer, styles: gitGraphStyles, }; diff --git a/packages/mermaid/src/diagrams/git/gitGraphParser.spec.js b/packages/mermaid/src/diagrams/git/gitGraphParser.spec.js deleted file mode 100644 index d498577fe..000000000 --- a/packages/mermaid/src/diagrams/git/gitGraphParser.spec.js +++ /dev/null @@ -1,272 +0,0 @@ -import gitGraphAst from './gitGraphAst.js'; -import { parser } from './parser/gitGraph.jison'; - -describe('when parsing a gitGraph', function () { - beforeEach(function () { - parser.yy = gitGraphAst; - parser.yy.clear(); - }); - it('should handle a gitGraph definition', function () { - const str = 'gitGraph:\n' + 'commit\n'; - - parser.parse(str); - const commits = parser.yy.getCommits(); - - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(1); - }); - - it('should handle a gitGraph definition with empty options', function () { - const str = 'gitGraph:\n' + 'options\n' + ' end\n' + 'commit\n'; - - parser.parse(str); - const commits = parser.yy.getCommits(); - - expect(parser.yy.getOptions()).toEqual({}); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(1); - }); - - it('should handle a gitGraph definition with valid options', function () { - const str = 'gitGraph:\n' + 'options\n' + '{"key": "value"}\n' + 'end\n' + 'commit\n'; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(parser.yy.getOptions().key).toBe('value'); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(1); - }); - - it('should not fail on a gitGraph with malformed json', function () { - const str = 'gitGraph:\n' + 'options\n' + '{"key": "value"\n' + 'end\n' + 'commit\n'; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(1); - }); - - it('should handle set direction top to bottom', function () { - const str = 'gitGraph TB:\n' + 'commit\n'; - - parser.parse(str); - const commits = parser.yy.getCommits(); - - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('TB'); - expect(parser.yy.getBranches().size).toBe(1); - }); - - it('should handle set direction bottom to top', function () { - const str = 'gitGraph BT:\n' + 'commit\n'; - - parser.parse(str); - const commits = parser.yy.getCommits(); - - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('BT'); - expect(parser.yy.getBranches().size).toBe(1); - }); - - it('should checkout a branch', function () { - const str = 'gitGraph:\n' + 'branch new\n' + 'checkout new\n'; - - parser.parse(str); - const commits = parser.yy.getCommits(); - - expect(commits.size).toBe(0); - expect(parser.yy.getCurrentBranch()).toBe('new'); - }); - - it('should switch a branch', function () { - const str = 'gitGraph:\n' + 'branch new\n' + 'switch new\n'; - - parser.parse(str); - const commits = parser.yy.getCommits(); - - expect(commits.size).toBe(0); - expect(parser.yy.getCurrentBranch()).toBe('new'); - }); - - it('should add commits to checked out branch', function () { - const str = 'gitGraph:\n' + 'branch new\n' + 'checkout new\n' + 'commit\n' + 'commit\n'; - - parser.parse(str); - const commits = parser.yy.getCommits(); - - expect(commits.size).toBe(2); - expect(parser.yy.getCurrentBranch()).toBe('new'); - const branchCommit = parser.yy.getBranches().get('new'); - expect(branchCommit).not.toBeNull(); - expect(commits.get(branchCommit).parent).not.toBeNull(); - }); - it('should handle commit with args', function () { - const str = 'gitGraph:\n' + 'commit "a commit"\n'; - - parser.parse(str); - const commits = parser.yy.getCommits(); - - expect(commits.size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe('a commit'); - expect(parser.yy.getCurrentBranch()).toBe('main'); - }); - - // Reset has been commented out in JISON - it.skip('should reset a branch', function () { - const str = - 'gitGraph:\n' + - 'commit\n' + - 'commit\n' + - 'branch newbranch\n' + - 'checkout newbranch\n' + - 'commit\n' + - 'reset main\n'; - - parser.parse(str); - - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(3); - expect(parser.yy.getCurrentBranch()).toBe('newbranch'); - expect(parser.yy.getBranches().get('newbranch')).toEqual(parser.yy.getBranches().get('main')); - expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches().get('newbranch')); - }); - - it.skip('reset can take an argument', function () { - const str = - 'gitGraph:\n' + - 'commit\n' + - 'commit\n' + - 'branch newbranch\n' + - 'checkout newbranch\n' + - 'commit\n' + - 'reset main^\n'; - - parser.parse(str); - - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(3); - expect(parser.yy.getCurrentBranch()).toBe('newbranch'); - const main = commits.get(parser.yy.getBranches().get('main')); - expect(parser.yy.getHead().id).toEqual(main.parent); - }); - - it.skip('should handle fast forwardable merges', function () { - const str = - 'gitGraph:\n' + - 'commit\n' + - 'branch newbranch\n' + - 'checkout newbranch\n' + - 'commit\n' + - 'commit\n' + - 'checkout main\n' + - 'merge newbranch\n'; - - parser.parse(str); - - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(4); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getBranches().get('newbranch')).toEqual(parser.yy.getBranches().get('main')); - expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches().get('newbranch')); - }); - - it('should handle cases when merge is a noop', function () { - const str = - 'gitGraph:\n' + - 'commit\n' + - 'branch newbranch\n' + - 'checkout newbranch\n' + - 'commit\n' + - 'commit\n' + - 'merge main\n'; - - parser.parse(str); - - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(4); - expect(parser.yy.getCurrentBranch()).toBe('newbranch'); - expect(parser.yy.getBranches().get('newbranch')).not.toEqual( - parser.yy.getBranches().get('main') - ); - expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches().get('newbranch')); - }); - - it('should handle merge with 2 parents', function () { - const str = - 'gitGraph:\n' + - 'commit\n' + - 'branch newbranch\n' + - 'checkout newbranch\n' + - 'commit\n' + - 'commit\n' + - 'checkout main\n' + - 'commit\n' + - 'merge newbranch\n'; - - parser.parse(str); - - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(5); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getBranches().get('newbranch')).not.toEqual( - parser.yy.getBranches().get('main') - ); - expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches().get('main')); - }); - - it.skip('should handle ff merge when history walk has two parents (merge commit)', function () { - const str = - 'gitGraph:\n' + - 'commit\n' + - 'branch newbranch\n' + - 'checkout newbranch\n' + - 'commit\n' + - 'commit\n' + - 'checkout main\n' + - 'commit\n' + - 'merge newbranch\n' + - 'commit\n' + - 'checkout newbranch\n' + - 'merge main\n'; - - parser.parse(str); - - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(7); - expect(parser.yy.getCurrentBranch()).toBe('newbranch'); - expect(parser.yy.getBranches().get('newbranch')).toEqual(parser.yy.getBranches().get('main')); - expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches().get('main')); - - parser.yy.prettyPrint(); - }); - - it('should generate an array of known branches', function () { - const str = - 'gitGraph:\n' + - 'commit\n' + - 'branch b1\n' + - 'checkout b1\n' + - 'commit\n' + - 'commit\n' + - 'branch b2\n'; - - parser.parse(str); - const branches = gitGraphAst.getBranchesAsObjArray(); - - expect(branches).toHaveLength(3); - expect(branches[0]).toHaveProperty('name', 'main'); - expect(branches[1]).toHaveProperty('name', 'b1'); - expect(branches[2]).toHaveProperty('name', 'b2'); - }); -}); diff --git a/packages/mermaid/src/diagrams/git/gitGraphParser.ts b/packages/mermaid/src/diagrams/git/gitGraphParser.ts new file mode 100644 index 000000000..c56bc6f44 --- /dev/null +++ b/packages/mermaid/src/diagrams/git/gitGraphParser.ts @@ -0,0 +1,243 @@ +import type { GitGraph } from '@mermaid-js/parser'; +import { parse } from '@mermaid-js/parser'; +import type { ParserDefinition } from '../../diagram-api/types.js'; +import { log } from '../../logger.js'; +import { populateCommonDb } from '../common/populateCommonDb.js'; +import { db } from './gitGraphAst.js'; +import { commitType } from './gitGraphTypes.js'; +import type { + CheckoutAst, + CherryPickingAst, + MergeAst, + CommitAst, + BranchAst, + GitGraphDBParseProvider, + CommitDB, + BranchDB, + MergeDB, + CherryPickDB, +} from './gitGraphTypes.js'; + +const populate = (ast: GitGraph, db: GitGraphDBParseProvider) => { + populateCommonDb(ast, db); + // @ts-ignore: this wont exist if the direction is not specified + if (ast.dir) { + // @ts-ignore: this wont exist if the direction is not specified + db.setDirection(ast.dir); + } + for (const statement of ast.statements) { + parseStatement(statement, db); + } +}; + +const parseStatement = (statement: any, db: GitGraphDBParseProvider) => { + const parsers: Record void> = { + Commit: (stmt) => db.commit(parseCommit(stmt)), + Branch: (stmt) => db.branch(parseBranch(stmt)), + Merge: (stmt) => db.merge(parseMerge(stmt)), + Checkout: (stmt) => db.checkout(parseCheckout(stmt)), + CherryPicking: (stmt) => db.cherryPick(parseCherryPicking(stmt)), + }; + + const parser = parsers[statement.$type]; + if (parser) { + parser(statement); + } else { + log.error(`Unknown statement type: ${statement.$type}`); + } +}; + +const parseCommit = (commit: CommitAst): CommitDB => { + const commitDB: CommitDB = { + id: commit.id, + msg: commit.message ?? '', + type: commit.type !== undefined ? commitType[commit.type] : commitType.NORMAL, + tags: commit.tags ?? undefined, + }; + return commitDB; +}; + +const parseBranch = (branch: BranchAst): BranchDB => { + const branchDB: BranchDB = { + name: branch.name, + order: branch.order ?? 0, + }; + return branchDB; +}; + +const parseMerge = (merge: MergeAst): MergeDB => { + const mergeDB: MergeDB = { + branch: merge.branch, + id: merge.id ?? '', + type: merge.type !== undefined ? commitType[merge.type] : undefined, + tags: merge.tags ?? undefined, + }; + return mergeDB; +}; + +const parseCheckout = (checkout: CheckoutAst): string => { + const branch = checkout.branch; + return branch; +}; + +const parseCherryPicking = (cherryPicking: CherryPickingAst): CherryPickDB => { + const cherryPickDB: CherryPickDB = { + id: cherryPicking.id, + targetId: '', + tags: cherryPicking.tags?.length === 0 ? undefined : cherryPicking.tags, + parent: cherryPicking.parent, + }; + return cherryPickDB; +}; + +export const parser: ParserDefinition = { + parse: async (input: string): Promise => { + const ast: GitGraph = await parse('gitGraph', input); + log.debug(ast); + populate(ast, db); + }, +}; + +if (import.meta.vitest) { + const { it, expect, describe } = import.meta.vitest; + + const mockDB: GitGraphDBParseProvider = { + commitType: commitType, + setDirection: vi.fn(), + commit: vi.fn(), + branch: vi.fn(), + merge: vi.fn(), + cherryPick: vi.fn(), + checkout: vi.fn(), + }; + + describe('GitGraph Parser', () => { + it('should parse a commit statement', () => { + const commit = { + $type: 'Commit', + id: '1', + message: 'test', + tags: ['tag1', 'tag2'], + type: 'NORMAL', + }; + parseStatement(commit, mockDB); + expect(mockDB.commit).toHaveBeenCalledWith({ + id: '1', + msg: 'test', + tags: ['tag1', 'tag2'], + type: 0, + }); + }); + it('should parse a branch statement', () => { + const branch = { + $type: 'Branch', + name: 'newBranch', + order: 1, + }; + parseStatement(branch, mockDB); + expect(mockDB.branch).toHaveBeenCalledWith({ name: 'newBranch', order: 1 }); + }); + it('should parse a checkout statement', () => { + const checkout = { + $type: 'Checkout', + branch: 'newBranch', + }; + parseStatement(checkout, mockDB); + expect(mockDB.checkout).toHaveBeenCalledWith('newBranch'); + }); + it('should parse a merge statement', () => { + const merge = { + $type: 'Merge', + branch: 'newBranch', + id: '1', + tags: ['tag1', 'tag2'], + type: 'NORMAL', + }; + parseStatement(merge, mockDB); + expect(mockDB.merge).toHaveBeenCalledWith({ + branch: 'newBranch', + id: '1', + tags: ['tag1', 'tag2'], + type: 0, + }); + }); + it('should parse a cherry picking statement', () => { + const cherryPick = { + $type: 'CherryPicking', + id: '1', + tags: ['tag1', 'tag2'], + parent: '2', + }; + parseStatement(cherryPick, mockDB); + expect(mockDB.cherryPick).toHaveBeenCalledWith({ + id: '1', + targetId: '', + parent: '2', + tags: ['tag1', 'tag2'], + }); + }); + + it('should parse a langium generated gitGraph ast', () => { + const dummy: GitGraph = { + $type: 'GitGraph', + statements: [], + }; + const gitGraphAst: GitGraph = { + $type: 'GitGraph', + statements: [ + { + $container: dummy, + $type: 'Commit', + id: '1', + message: 'test', + tags: ['tag1', 'tag2'], + type: 'NORMAL', + }, + { + $container: dummy, + $type: 'Branch', + name: 'newBranch', + order: 1, + }, + { + $container: dummy, + $type: 'Merge', + branch: 'newBranch', + id: '1', + tags: ['tag1', 'tag2'], + type: 'NORMAL', + }, + { + $container: dummy, + $type: 'Checkout', + branch: 'newBranch', + }, + { + $container: dummy, + $type: 'CherryPicking', + id: '1', + tags: ['tag1', 'tag2'], + parent: '2', + }, + ], + }; + + populate(gitGraphAst, mockDB); + + expect(mockDB.commit).toHaveBeenCalledWith({ + id: '1', + msg: 'test', + tags: ['tag1', 'tag2'], + type: 0, + }); + expect(mockDB.branch).toHaveBeenCalledWith({ name: 'newBranch', order: 1 }); + expect(mockDB.merge).toHaveBeenCalledWith({ + branch: 'newBranch', + id: '1', + tags: ['tag1', 'tag2'], + type: 0, + }); + expect(mockDB.checkout).toHaveBeenCalledWith('newBranch'); + }); + }); +} diff --git a/packages/mermaid/src/diagrams/git/gitGraphParserV2.spec.js b/packages/mermaid/src/diagrams/git/gitGraphParserV2.spec.js deleted file mode 100644 index 1fb64a5c4..000000000 --- a/packages/mermaid/src/diagrams/git/gitGraphParserV2.spec.js +++ /dev/null @@ -1,1107 +0,0 @@ -import gitGraphAst from './gitGraphAst.js'; -import { parser } from './parser/gitGraph.jison'; - -describe('when parsing a gitGraph', function () { - beforeEach(function () { - parser.yy = gitGraphAst; - parser.yy.clear(); - }); - it('should handle a gitGraph commit with NO pararms, get auto-generated reandom ID', function () { - const str = `gitGraph: - commit - `; - parser.parse(str); - const commits = parser.yy.getCommits(); - //console.info(commits); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe(''); - expect(commits.get(key).id).not.toBeNull(); - expect(commits.get(key).tags).toStrictEqual([]); - expect(commits.get(key).type).toBe(0); - }); - - it('should handle a gitGraph commit with custom commit id only', function () { - const str = `gitGraph: - commit id:"1111" - `; - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe(''); - expect(commits.get(key).id).toBe('1111'); - expect(commits.get(key).tags).toStrictEqual([]); - expect(commits.get(key).type).toBe(0); - }); - - it('should handle a gitGraph commit with custom commit tag only', function () { - const str = `gitGraph: - commit tag:"test" - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe(''); - expect(commits.get(key).id).not.toBeNull(); - expect(commits.get(key).tags).toStrictEqual(['test']); - expect(commits.get(key).type).toBe(0); - }); - - it('should handle a gitGraph commit with custom commit type HIGHLIGHT only', function () { - const str = `gitGraph: - commit type: HIGHLIGHT - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe(''); - expect(commits.get(key).id).not.toBeNull(); - expect(commits.get(key).tags).toStrictEqual([]); - expect(commits.get(key).type).toBe(2); - }); - - it('should handle a gitGraph commit with custom commit type REVERSE only', function () { - const str = `gitGraph: - commit type: REVERSE - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe(''); - expect(commits.get(key).id).not.toBeNull(); - expect(commits.get(key).tags).toStrictEqual([]); - expect(commits.get(key).type).toBe(1); - }); - - it('should handle a gitGraph commit with custom commit type NORMAL only', function () { - const str = `gitGraph: - commit type: NORMAL - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe(''); - expect(commits.get(key).id).not.toBeNull(); - expect(commits.get(key).tags).toStrictEqual([]); - expect(commits.get(key).type).toBe(0); - }); - - it('should handle a gitGraph commit with custom commit msg only', function () { - const str = `gitGraph: - commit "test commit" - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe('test commit'); - expect(commits.get(key).id).not.toBeNull(); - expect(commits.get(key).tags).toStrictEqual([]); - expect(commits.get(key).type).toBe(0); - }); - - it('should handle a gitGraph commit with custom commit "msg:" key only', function () { - const str = `gitGraph: - commit msg: "test commit" - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe('test commit'); - expect(commits.get(key).id).not.toBeNull(); - expect(commits.get(key).tags).toStrictEqual([]); - expect(commits.get(key).type).toBe(0); - }); - - it('should handle a gitGraph commit with custom commit id, tag only', function () { - const str = `gitGraph: - commit id:"1111" tag: "test tag" - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe(''); - expect(commits.get(key).id).toBe('1111'); - expect(commits.get(key).tags).toStrictEqual(['test tag']); - expect(commits.get(key).type).toBe(0); - }); - - it('should handle a gitGraph commit with custom commit type, tag only', function () { - const str = `gitGraph: - commit type:HIGHLIGHT tag: "test tag" - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe(''); - expect(commits.get(key).id).not.toBeNull(); - expect(commits.get(key).tags).toStrictEqual(['test tag']); - expect(commits.get(key).type).toBe(2); - }); - - it('should handle a gitGraph commit with custom commit tag and type only', function () { - const str = `gitGraph: - commit tag: "test tag" type:HIGHLIGHT - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe(''); - expect(commits.get(key).id).not.toBeNull(); - expect(commits.get(key).tags).toStrictEqual(['test tag']); - expect(commits.get(key).type).toBe(2); - }); - - it('should handle a gitGraph commit with custom commit id, type and tag only', function () { - const str = `gitGraph: - commit id:"1111" type:REVERSE tag: "test tag" - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe(''); - expect(commits.get(key).id).toBe('1111'); - expect(commits.get(key).tags).toStrictEqual(['test tag']); - expect(commits.get(key).type).toBe(1); - }); - - it('should handle a gitGraph commit with custom commit id, type, tag and msg', function () { - const str = `gitGraph: - commit id:"1111" type:REVERSE tag: "test tag" msg:"test msg" - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe('test msg'); - expect(commits.get(key).id).toBe('1111'); - expect(commits.get(key).tags).toStrictEqual(['test tag']); - expect(commits.get(key).type).toBe(1); - }); - - it('should handle a gitGraph commit with custom type,tag, msg, commit id,', function () { - const str = `gitGraph: - commit type:REVERSE tag: "test tag" msg: "test msg" id: "1111" - - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe('test msg'); - expect(commits.get(key).id).toBe('1111'); - expect(commits.get(key).tags).toStrictEqual(['test tag']); - expect(commits.get(key).type).toBe(1); - }); - - it('should handle a gitGraph commit with custom tag, msg, commit id, type,', function () { - const str = `gitGraph: - commit tag: "test tag" msg:"test msg" id:"1111" type:REVERSE - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe('test msg'); - expect(commits.get(key).id).toBe('1111'); - expect(commits.get(key).tags).toStrictEqual(['test tag']); - expect(commits.get(key).type).toBe(1); - }); - - it('should handle a gitGraph commit with custom msg, commit id, type,tag', function () { - const str = `gitGraph: - commit msg:"test msg" id:"1111" type:REVERSE tag: "test tag" - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe('test msg'); - expect(commits.get(key).id).toBe('1111'); - expect(commits.get(key).tags).toStrictEqual(['test tag']); - expect(commits.get(key).type).toBe(1); - }); - - it('should handle 3 straight commits', function () { - const str = `gitGraph: - commit - commit - commit - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(3); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(1); - }); - - it('should handle new branch creation', function () { - const str = `gitGraph: - commit - branch testBranch - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('testBranch'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(2); - }); - - it('should allow quoted branch names', function () { - const str = `gitGraph: - commit - branch "branch" - checkout "branch" - commit - checkout main - merge "branch" - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(3); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(2); - const [commit1, commit2, commit3] = commits.keys(); - expect(commits.get(commit1).branch).toBe('main'); - expect(commits.get(commit2).branch).toBe('branch'); - expect(commits.get(commit3).branch).toBe('main'); - expect(parser.yy.getBranchesAsObjArray()).toStrictEqual([{ name: 'main' }, { name: 'branch' }]); - }); - - it('should allow _-./ characters in branch names', function () { - const str = `gitGraph: - commit - branch azAZ_-./test - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('azAZ_-./test'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(2); - }); - - it('should allow branch names starting with numbers', function () { - const str = `gitGraph: - commit - %% branch names starting with numbers are not recommended, but are supported by git - branch 1.0.1 - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('1.0.1'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(2); - }); - - it('should allow branch names starting with unusual prefixes', function () { - const str = `gitGraph: - commit - %% branch names starting with numbers are not recommended, but are supported by git - branch branch01 - branch checkout02 - branch cherry-pick03 - branch branch/example-branch - branch merge/test_merge - %% single character branch name - branch A - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('A'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(7); - expect([...parser.yy.getBranches().keys()]).toEqual( - expect.arrayContaining([ - 'branch01', - 'checkout02', - 'cherry-pick03', - 'branch/example-branch', - 'merge/test_merge', - 'A', - ]) - ); - }); - - it('should handle new branch checkout', function () { - const str = `gitGraph: - commit - branch testBranch - checkout testBranch - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('testBranch'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(2); - }); - it('should handle new branch checkout with order', function () { - const str = `gitGraph: - commit - branch test1 order: 3 - branch test2 order: 2 - branch test3 order: 1 - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('test3'); - expect(parser.yy.getBranches().size).toBe(4); - expect(parser.yy.getBranchesAsObjArray()).toStrictEqual([ - { name: 'main' }, - { name: 'test3' }, - { name: 'test2' }, - { name: 'test1' }, - ]); - }); - it('should handle new branch checkout with and without order', function () { - const str = `gitGraph: - commit - branch test1 order: 1 - branch test2 - branch test3 - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('test3'); - expect(parser.yy.getBranches().size).toBe(4); - expect(parser.yy.getBranchesAsObjArray()).toStrictEqual([ - { name: 'main' }, - { name: 'test2' }, - { name: 'test3' }, - { name: 'test1' }, - ]); - }); - - it('should handle new branch checkout & commit', function () { - const str = `gitGraph: - commit - branch testBranch - checkout testBranch - commit - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(2); - expect(parser.yy.getCurrentBranch()).toBe('testBranch'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(2); - const [commit1, commit2] = commits.keys(); - expect(commits.get(commit1).branch).toBe('main'); - expect(commits.get(commit1).parents).toStrictEqual([]); - expect(commits.get(commit2).branch).toBe('testBranch'); - expect(commits.get(commit2).parents).toStrictEqual([commit1]); - }); - - it('should handle new branch checkout & commit and merge', function () { - const str = `gitGraph: - commit - branch testBranch - checkout testBranch - commit - commit - checkout main - merge testBranch - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(4); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(2); - const [commit1, commit2, commit3, commit4] = commits.keys(); - expect(commits.get(commit1).branch).toBe('main'); - expect(commits.get(commit1).parents).toStrictEqual([]); - expect(commits.get(commit2).branch).toBe('testBranch'); - expect(commits.get(commit2).parents).toStrictEqual([commits.get(commit1).id]); - expect(commits.get(commit3).branch).toBe('testBranch'); - expect(commits.get(commit3).parents).toStrictEqual([commits.get(commit2).id]); - expect(commits.get(commit4).branch).toBe('main'); - expect(commits.get(commit4).parents).toStrictEqual([ - commits.get(commit1).id, - commits.get(commit3).id, - ]); - expect(parser.yy.getBranchesAsObjArray()).toStrictEqual([ - { name: 'main' }, - { name: 'testBranch' }, - ]); - }); - - it('should handle new branch switch', function () { - const str = `gitGraph: - commit - branch testBranch - switch testBranch - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('testBranch'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(2); - }); - - it('should handle new branch switch & commit', function () { - const str = `gitGraph: - commit - branch testBranch - switch testBranch - commit - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(2); - expect(parser.yy.getCurrentBranch()).toBe('testBranch'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(2); - const [commit1, commit2] = commits.keys(); - expect(commits.get(commit1).branch).toBe('main'); - expect(commits.get(commit1).parents).toStrictEqual([]); - expect(commits.get(commit2).branch).toBe('testBranch'); - expect(commits.get(commit2).parents).toStrictEqual([commit1]); - }); - - it('should handle new branch switch & commit and merge', function () { - const str = `gitGraph: - commit - branch testBranch - switch testBranch - commit - commit - switch main - merge testBranch - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(4); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(2); - const [commit1, commit2, commit3, commit4] = commits.keys(); - expect(commits.get(commit1).branch).toBe('main'); - expect(commits.get(commit1).parents).toStrictEqual([]); - expect(commits.get(commit2).branch).toBe('testBranch'); - expect(commits.get(commit2).parents).toStrictEqual([commits.get(commit1).id]); - expect(commits.get(commit3).branch).toBe('testBranch'); - expect(commits.get(commit3).parents).toStrictEqual([commits.get(commit2).id]); - expect(commits.get(commit4).branch).toBe('main'); - expect(commits.get(commit4).parents).toStrictEqual([ - commits.get(commit1).id, - commits.get(commit3).id, - ]); - expect(parser.yy.getBranchesAsObjArray()).toStrictEqual([ - { name: 'main' }, - { name: 'testBranch' }, - ]); - }); - - it('should handle merge tags', function () { - const str = `gitGraph: - commit - branch testBranch - checkout testBranch - commit - checkout main - merge testBranch tag: "merge-tag" - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(3); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(2); - const [commit1, commit2, commit3] = commits.keys(); - expect(commits.get(commit1).branch).toBe('main'); - expect(commits.get(commit1).parents).toStrictEqual([]); - - expect(commits.get(commit2).branch).toBe('testBranch'); - expect(commits.get(commit2).parents).toStrictEqual([commits.get(commit1).id]); - - expect(commits.get(commit3).branch).toBe('main'); - expect(commits.get(commit3).parents).toStrictEqual([ - commits.get(commit1).id, - commits.get(commit2).id, - ]); - expect(commits.get(commit3).tags).toStrictEqual(['merge-tag']); - expect(parser.yy.getBranchesAsObjArray()).toStrictEqual([ - { name: 'main' }, - { name: 'testBranch' }, - ]); - }); - - it('should handle merge with custom ids, tags and typr', function () { - const str = `gitGraph: - commit - branch testBranch - checkout testBranch - commit - checkout main - %% Merge Tag and ID - merge testBranch tag: "merge-tag" id: "2-222" - branch testBranch2 - checkout testBranch2 - commit - checkout main - %% Merge ID and Tag (reverse order) - merge testBranch2 id: "4-444" tag: "merge-tag2" type:HIGHLIGHT - branch testBranch3 - checkout testBranch3 - commit - checkout main - %% just Merge ID - merge testBranch3 id: "6-666" - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(7); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - - // The order of these commits is in alphabetical order of IDs - const [ - mainCommit, - testBranchCommit, - testBranchMerge, - testBranch2Commit, - testBranch2Merge, - testBranch3Commit, - testBranch3Merge, - ] = [...commits.values()]; - - expect(mainCommit.branch).toBe('main'); - expect(mainCommit.parents).toStrictEqual([]); - - expect(testBranchCommit.branch).toBe('testBranch'); - expect(testBranchCommit.parents).toStrictEqual([mainCommit.id]); - - expect(testBranchMerge.branch).toBe('main'); - expect(testBranchMerge.parents).toStrictEqual([mainCommit.id, testBranchCommit.id]); - expect(testBranchMerge.tags).toStrictEqual(['merge-tag']); - expect(testBranchMerge.id).toBe('2-222'); - - expect(testBranch2Merge.branch).toBe('main'); - expect(testBranch2Merge.parents).toStrictEqual([testBranchMerge.id, testBranch2Commit.id]); - expect(testBranch2Merge.tags).toStrictEqual(['merge-tag2']); - expect(testBranch2Merge.id).toBe('4-444'); - expect(testBranch2Merge.customType).toBe(2); - expect(testBranch2Merge.customId).toBe(true); - - expect(testBranch3Merge.branch).toBe('main'); - expect(testBranch3Merge.parents).toStrictEqual([testBranch2Merge.id, testBranch3Commit.id]); - expect(testBranch3Merge.id).toBe('6-666'); - - expect(parser.yy.getBranchesAsObjArray()).toStrictEqual([ - { name: 'main' }, - { name: 'testBranch' }, - { name: 'testBranch2' }, - { name: 'testBranch3' }, - ]); - }); - - it('should support cherry-picking commits', function () { - const str = `gitGraph - commit id: "ZERO" - branch develop - commit id:"A" - checkout main - cherry-pick id:"A" - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - const cherryPickCommitID = [...commits.keys()][2]; - expect(commits.get(cherryPickCommitID).tags).toStrictEqual(['cherry-pick:A']); - expect(commits.get(cherryPickCommitID).branch).toBe('main'); - }); - - it('should support cherry-picking commits with custom tag', function () { - const str = `gitGraph - commit id: "ZERO" - branch develop - commit id:"A" - checkout main - cherry-pick id:"A" tag:"MyTag" - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - const cherryPickCommitID = [...commits.keys()][2]; - expect(commits.get(cherryPickCommitID).tags).toStrictEqual(['MyTag']); - expect(commits.get(cherryPickCommitID).branch).toBe('main'); - }); - - it('should support cherry-picking commits with no tag', function () { - const str = `gitGraph - commit id: "ZERO" - branch develop - commit id:"A" - checkout main - cherry-pick id:"A" tag:"" - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - const cherryPickCommitID = [...commits.keys()][2]; - expect(commits.get(cherryPickCommitID).tags).toStrictEqual([]); - expect(commits.get(cherryPickCommitID).branch).toBe('main'); - }); - - it('should support cherry-picking of merge commits', function () { - const str = `gitGraph - commit id: "ZERO" - branch feature - branch release - checkout feature - commit id: "A" - commit id: "B" - checkout main - merge feature id: "M" - checkout release - cherry-pick id: "M" parent:"B" - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - const cherryPickCommitID = [...commits.keys()][4]; - expect(commits.get(cherryPickCommitID).tags).toStrictEqual(['cherry-pick:M|parent:B']); - expect(commits.get(cherryPickCommitID).branch).toBe('release'); - }); - - it('should support cherry-picking of merge commits with tag', function () { - const str = `gitGraph - commit id: "ZERO" - branch feature - branch release - checkout feature - commit id: "A" - commit id: "B" - checkout main - merge feature id: "M" - checkout release - cherry-pick id: "M" parent:"ZERO" tag: "v1.0" - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - const cherryPickCommitID = [...commits.keys()][4]; - expect(commits.get(cherryPickCommitID).tags).toStrictEqual(['v1.0']); - expect(commits.get(cherryPickCommitID).branch).toBe('release'); - }); - - it('should support cherry-picking of merge commits with additional commit', function () { - const str = `gitGraph - commit id: "ZERO" - branch feature - branch release - checkout feature - commit id: "A" - commit id: "B" - checkout main - merge feature id: "M" - checkout release - commit id: "C" - cherry-pick id: "M" tag: "v2.1:ZERO" parent:"ZERO" - commit id: "D" - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - const cherryPickCommitID = [...commits.keys()][5]; - expect(commits.get(cherryPickCommitID).tags).toStrictEqual(['v2.1:ZERO']); - expect(commits.get(cherryPickCommitID).branch).toBe('release'); - }); - - it('should support cherry-picking of merge commits with empty tag', function () { - const str = `gitGraph - commit id: "ZERO" - branch feature - branch release - checkout feature - commit id: "A" - commit id: "B" - checkout main - merge feature id: "M" - checkout release - commit id: "C" - cherry-pick id:"M" parent: "ZERO" tag:"" - commit id: "D" - cherry-pick id:"M" tag:"" parent: "B" - `; - - parser.parse(str); - const commits = parser.yy.getCommits(); - const cherryPickCommitID = [...commits.keys()][5]; - const cherryPickCommitID2 = [...commits.keys()][7]; - expect(commits.get(cherryPickCommitID).tags).toStrictEqual([]); - expect(commits.get(cherryPickCommitID2).tags).toStrictEqual([]); - expect(commits.get(cherryPickCommitID).branch).toBe('release'); - }); - - it('should fail cherry-picking of merge commits if the parent of merge commits is not specified', function () { - expect(() => - parser - .parse( - `gitGraph - commit id: "ZERO" - branch feature - branch release - checkout feature - commit id: "A" - commit id: "B" - checkout main - merge feature id: "M" - checkout release - commit id: "C" - cherry-pick id:"M" - ` - ) - .toThrow( - 'Incorrect usage of cherry-pick: If the source commit is a merge commit, an immediate parent commit must be specified.' - ) - ); - }); - - it('should fail cherry-picking of merge commits when the parent provided is not an immediate parent of cherry picked commit', function () { - expect(() => - parser - .parse( - `gitGraph - commit id: "ZERO" - branch feature - branch release - checkout feature - commit id: "A" - commit id: "B" - checkout main - merge feature id: "M" - checkout release - commit id: "C" - cherry-pick id:"M" parent: "A" - ` - ) - .toThrow( - 'Invalid operation: The specified parent commit is not an immediate parent of the cherry-picked commit.' - ) - ); - }); - - it('should throw error when try to branch existing branch: main', function () { - const str = `gitGraph - commit - branch testBranch - commit - branch main - commit - checkout main - merge testBranch - `; - - try { - parser.parse(str); - // Fail test if above expression doesn't throw anything. - expect(true).toBe(false); - } catch (e) { - expect(e.message).toBe( - 'Trying to create an existing branch. (Help: Either use a new name if you want create a new branch or try using "checkout main")' - ); - } - }); - it('should throw error when try to branch existing branch: testBranch', function () { - const str = `gitGraph - commit - branch testBranch - commit - branch testBranch - commit - checkout main - merge testBranch - `; - - try { - parser.parse(str); - // Fail test if above expression doesn't throw anything. - expect(true).toBe(false); - } catch (e) { - expect(e.message).toBe( - 'Trying to create an existing branch. (Help: Either use a new name if you want create a new branch or try using "checkout testBranch")' - ); - } - }); - it('should throw error when try to checkout unknown branch: testBranch', function () { - const str = `gitGraph - commit - checkout testBranch - commit - branch testBranch - commit - checkout main - merge testBranch - `; - - try { - parser.parse(str); - // Fail test if above expression doesn't throw anything. - expect(true).toBe(false); - } catch (e) { - expect(e.message).toBe( - 'Trying to checkout branch which is not yet created. (Help try using "branch testBranch")' - ); - } - }); - it('should throw error when trying to merge, when current branch has no commits', function () { - const str = `gitGraph - merge testBranch - commit - checkout testBranch - commit - branch testBranch - commit - checkout main - merge testBranch - `; - - try { - parser.parse(str); - // Fail test if above expression doesn't throw anything. - expect(true).toBe(false); - } catch (e) { - expect(e.message).toBe('Incorrect usage of "merge". Current branch (main)has no commits'); - } - }); - it('should throw error when trying to merge unknown branch', function () { - const str = `gitGraph - commit - merge testBranch - commit - checkout testBranch - commit - branch testBranch - commit - checkout main - merge testBranch - `; - - try { - parser.parse(str); - // Fail test if above expression doesn't throw anything. - expect(true).toBe(false); - } catch (e) { - expect(e.message).toBe( - 'Incorrect usage of "merge". Branch to be merged (testBranch) does not exist' - ); - } - }); - it('should throw error when trying to merge branch to itself', function () { - const str = `gitGraph - commit - branch testBranch - merge testBranch - `; - - try { - parser.parse(str); - // Fail test if above expression doesn't throw anything. - expect(true).toBe(false); - } catch (e) { - expect(e.message).toBe('Incorrect usage of "merge". Cannot merge a branch to itself'); - } - }); - - it('should throw error when using existing id as merge ID', function () { - const str = `gitGraph - commit id: "1-111" - branch testBranch - commit id: "2-222" - commit id: "3-333" - checkout main - merge testBranch id: "1-111" - `; - - try { - parser.parse(str); - // Fail test if above expression doesn't throw anything. - expect(true).toBe(false); - } catch (e) { - expect(e.message).toBe( - 'Incorrect usage of "merge". Commit with id:1-111 already exists, use different custom Id' - ); - } - }); - it('should throw error when trying to merge branches having same heads', function () { - const str = `gitGraph - commit - branch testBranch - checkout main - merge testBranch - `; - - try { - parser.parse(str); - // Fail test if above expression doesn't throw anything. - expect(true).toBe(false); - } catch (e) { - expect(e.message).toBe('Incorrect usage of "merge". Both branches have same head'); - } - }); - it('should throw error when trying to merge branch which has no commits', function () { - const str = `gitGraph - branch test1 - - checkout main - commit - merge test1 - `; - - try { - parser.parse(str); - // Fail test if above expression doesn't throw anything. - expect(true).toBe(false); - } catch (e) { - expect(e.message).toBe( - 'Incorrect usage of "merge". Branch to be merged (test1) has no commits' - ); - } - }); - describe('accessibility', () => { - it('should handle a title and a description (accDescr)', () => { - const str = `gitGraph: - accTitle: This is a title - accDescr: This is a description - commit - `; - parser.parse(str); - expect(parser.yy.getAccTitle()).toBe('This is a title'); - expect(parser.yy.getAccDescription()).toBe('This is a description'); - }); - it('should handle a title and a multiline description (accDescr)', () => { - const str = `gitGraph: - accTitle: This is a title - accDescr { - This is a description - using multiple lines - } - commit - `; - parser.parse(str); - expect(parser.yy.getAccTitle()).toBe('This is a title'); - expect(parser.yy.getAccDescription()).toBe('This is a description\nusing multiple lines'); - }); - }); - - describe('unsafe properties', () => { - for (const prop of ['__proto__', 'constructor']) { - it(`should work with custom commit id or branch name ${prop}`, () => { - const str = `gitGraph - commit id:"${prop}" - branch ${prop} - checkout ${prop} - commit - checkout main - merge ${prop} - `; - parser.parse(str); - const commits = parser.yy.getCommits(); - expect(commits.size).toBe(3); - expect(commits.keys().next().value).toBe(prop); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getBranches().size).toBe(2); - expect(parser.yy.getBranchesAsObjArray()[1].name).toBe(prop); - }); - } - }); -}); diff --git a/packages/mermaid/src/diagrams/git/gitGraphRenderer.js b/packages/mermaid/src/diagrams/git/gitGraphRenderer.js deleted file mode 100644 index b8b13e089..000000000 --- a/packages/mermaid/src/diagrams/git/gitGraphRenderer.js +++ /dev/null @@ -1,893 +0,0 @@ -import { select } from 'd3'; -import { getConfig, setupGraphViewbox } from '../../diagram-api/diagramAPI.js'; -import { log } from '../../logger.js'; -import utils from '../../utils.js'; - -/** - * @typedef {Map} CommitMap - */ - -/** @type {CommitMap} */ -let allCommitsDict = new Map(); - -const commitType = { - NORMAL: 0, - REVERSE: 1, - HIGHLIGHT: 2, - MERGE: 3, - CHERRY_PICK: 4, -}; - -const THEME_COLOR_LIMIT = 8; - -let branchPos = {}; -let commitPos = {}; -let lanes = []; -let maxPos = 0; -let dir = 'LR'; -let defaultPos = 30; -const clear = () => { - branchPos = new Map(); - commitPos = new Map(); - allCommitsDict = new Map(); - maxPos = 0; - lanes = []; - dir = 'LR'; -}; - -/** - * Draws a text, used for labels of the branches - * - * @param {string} txt The text - * @returns {SVGElement} - */ -const drawText = (txt) => { - const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); - let rows = []; - - // Handling of new lines in the label - if (typeof txt === 'string') { - rows = txt.split(/\\n|\n|/gi); - } else if (Array.isArray(txt)) { - rows = txt; - } else { - rows = []; - } - - for (const row of rows) { - const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan'); - tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve'); - tspan.setAttribute('dy', '1em'); - tspan.setAttribute('x', '0'); - tspan.setAttribute('class', 'row'); - tspan.textContent = row.trim(); - svgLabel.appendChild(tspan); - } - /** - * @param svg - * @param selector - */ - return svgLabel; -}; - -/** - * Searches for the closest parent from the parents list passed as argument. - * The parents list comes from an individual commit. The closest parent is actually - * the one farther down the graph, since that means it is closer to its child. - * - * @param {string[]} parents - * @returns {string | undefined} - */ -const findClosestParent = (parents) => { - let closestParent = ''; - let maxPosition = 0; - - parents.forEach((parent) => { - const parentPosition = - dir === 'TB' || dir === 'BT' ? commitPos.get(parent).y : commitPos.get(parent).x; - if (parentPosition >= maxPosition) { - closestParent = parent; - maxPosition = parentPosition; - } - }); - - return closestParent || undefined; -}; - -/** - * Searches for the closest parent from the parents list passed as argument for Bottom-to-Top orientation. - * The parents list comes from an individual commit. The closest parent is actually - * the one farther down the graph, since that means it is closer to its child. - * - * @param {string[]} parents - * @returns {string | undefined} - */ -const findClosestParentBT = (parents) => { - let closestParent = ''; - let maxPosition = Infinity; - - parents.forEach((parent) => { - const parentPosition = commitPos.get(parent).y; - if (parentPosition <= maxPosition) { - closestParent = parent; - maxPosition = parentPosition; - } - }); - - return closestParent || undefined; -}; - -/** - * Sets the position of the commit elements when the orientation is set to BT-Parallel. - * This is needed to render the chart in Bottom-to-Top mode while keeping the parallel - * commits in the correct position. First, it finds the correct position of the root commit - * using the findClosestParent method. Then, it uses the findClosestParentBT to set the position - * of the remaining commits. - * - * @param {any} sortedKeys - * @param {CommitMap} commits - * @param {any} defaultPos - * @param {any} commitStep - * @param {any} layoutOffset - */ -const setParallelBTPos = (sortedKeys, commits, defaultPos, commitStep, layoutOffset) => { - let curPos = defaultPos; - let maxPosition = defaultPos; - let roots = []; - sortedKeys.forEach((key) => { - const commit = commits.get(key); - if (commit.parents.length) { - const closestParent = findClosestParent(commit.parents); - curPos = commitPos.get(closestParent).y + commitStep; - if (curPos >= maxPosition) { - maxPosition = curPos; - } - } else { - roots.push(commit); - } - const x = branchPos.get(commit.branch).pos; - const y = curPos + layoutOffset; - commitPos.set(commit.id, { x: x, y: y }); - }); - curPos = maxPosition; - roots.forEach((commit) => { - const posWithOffset = curPos + defaultPos; - const y = posWithOffset; - const x = branchPos.get(commit.branch).pos; - commitPos.set(commit.id, { x: x, y: y }); - }); - sortedKeys.forEach((key) => { - const commit = commits.get(key); - if (commit.parents.length) { - const closestParent = findClosestParentBT(commit.parents); - curPos = commitPos.get(closestParent).y - commitStep; - if (curPos <= maxPosition) { - maxPosition = curPos; - } - const x = branchPos.get(commit.branch).pos; - const y = curPos - layoutOffset; - commitPos.set(commit.id, { x: x, y: y }); - } - }); -}; - -/** - * Draws the commits with its symbol and labels. The function has two modes, one which only - * calculates the positions and one that does the actual drawing. This for a simple way getting the - * vertical layering correct in the graph. - * - * @param {any} svg - * @param {CommitMap} commits - * @param {any} modifyGraph - */ -const drawCommits = (svg, commits, modifyGraph) => { - const gitGraphConfig = getConfig().gitGraph; - const gBullets = svg.append('g').attr('class', 'commit-bullets'); - const gLabels = svg.append('g').attr('class', 'commit-labels'); - let pos = 0; - - if (dir === 'TB' || dir === 'BT') { - pos = defaultPos; - } - const keys = [...commits.keys()]; - const isParallelCommits = gitGraphConfig.parallelCommits; - const layoutOffset = 10; - const commitStep = 40; - let sortedKeys = - dir !== 'BT' || (dir === 'BT' && isParallelCommits) - ? keys.sort((a, b) => { - return commits.get(a).seq - commits.get(b).seq; - }) - : keys - .sort((a, b) => { - return commits.get(a).seq - commits.get(b).seq; - }) - .reverse(); - - if (dir === 'BT' && isParallelCommits) { - setParallelBTPos(sortedKeys, commits, pos, commitStep, layoutOffset); - sortedKeys = sortedKeys.reverse(); - } - sortedKeys.forEach((key) => { - const commit = commits.get(key); - if (isParallelCommits) { - if (commit.parents.length) { - const closestParent = - dir === 'BT' ? findClosestParentBT(commit.parents) : findClosestParent(commit.parents); - if (dir === 'TB') { - pos = commitPos.get(closestParent).y + commitStep; - } else if (dir === 'BT') { - pos = commitPos.get(key).y - commitStep; - } else { - pos = commitPos.get(closestParent).x + commitStep; - } - } else { - if (dir === 'TB') { - pos = defaultPos; - } else if (dir === 'BT') { - pos = commitPos.get(key).y - commitStep; - } else { - pos = 0; - } - } - } - const posWithOffset = dir === 'BT' && isParallelCommits ? pos : pos + layoutOffset; - const y = dir === 'TB' || dir === 'BT' ? posWithOffset : branchPos.get(commit.branch).pos; - const x = dir === 'TB' || dir === 'BT' ? branchPos.get(commit.branch).pos : posWithOffset; - - // Don't draw the commits now but calculate the positioning which is used by the branch lines etc. - if (modifyGraph) { - let typeClass; - let commitSymbolType = - commit.customType !== undefined && commit.customType !== '' - ? commit.customType - : commit.type; - switch (commitSymbolType) { - case commitType.NORMAL: - typeClass = 'commit-normal'; - break; - case commitType.REVERSE: - typeClass = 'commit-reverse'; - break; - case commitType.HIGHLIGHT: - typeClass = 'commit-highlight'; - break; - case commitType.MERGE: - typeClass = 'commit-merge'; - break; - case commitType.CHERRY_PICK: - typeClass = 'commit-cherry-pick'; - break; - default: - typeClass = 'commit-normal'; - } - - if (commitSymbolType === commitType.HIGHLIGHT) { - const circle = gBullets.append('rect'); - circle.attr('x', x - 10); - circle.attr('y', y - 10); - circle.attr('height', 20); - circle.attr('width', 20); - circle.attr( - 'class', - `commit ${commit.id} commit-highlight${ - branchPos.get(commit.branch).index % THEME_COLOR_LIMIT - } ${typeClass}-outer` - ); - gBullets - .append('rect') - .attr('x', x - 6) - .attr('y', y - 6) - .attr('height', 12) - .attr('width', 12) - .attr( - 'class', - `commit ${commit.id} commit${ - branchPos.get(commit.branch).index % THEME_COLOR_LIMIT - } ${typeClass}-inner` - ); - } else if (commitSymbolType === commitType.CHERRY_PICK) { - gBullets - .append('circle') - .attr('cx', x) - .attr('cy', y) - .attr('r', 10) - .attr('class', `commit ${commit.id} ${typeClass}`); - gBullets - .append('circle') - .attr('cx', x - 3) - .attr('cy', y + 2) - .attr('r', 2.75) - .attr('fill', '#fff') - .attr('class', `commit ${commit.id} ${typeClass}`); - gBullets - .append('circle') - .attr('cx', x + 3) - .attr('cy', y + 2) - .attr('r', 2.75) - .attr('fill', '#fff') - .attr('class', `commit ${commit.id} ${typeClass}`); - gBullets - .append('line') - .attr('x1', x + 3) - .attr('y1', y + 1) - .attr('x2', x) - .attr('y2', y - 5) - .attr('stroke', '#fff') - .attr('class', `commit ${commit.id} ${typeClass}`); - gBullets - .append('line') - .attr('x1', x - 3) - .attr('y1', y + 1) - .attr('x2', x) - .attr('y2', y - 5) - .attr('stroke', '#fff') - .attr('class', `commit ${commit.id} ${typeClass}`); - } else { - const circle = gBullets.append('circle'); - circle.attr('cx', x); - circle.attr('cy', y); - circle.attr('r', commit.type === commitType.MERGE ? 9 : 10); - circle.attr( - 'class', - `commit ${commit.id} commit${branchPos.get(commit.branch).index % THEME_COLOR_LIMIT}` - ); - if (commitSymbolType === commitType.MERGE) { - const circle2 = gBullets.append('circle'); - circle2.attr('cx', x); - circle2.attr('cy', y); - circle2.attr('r', 6); - circle2.attr( - 'class', - `commit ${typeClass} ${commit.id} commit${ - branchPos.get(commit.branch).index % THEME_COLOR_LIMIT - }` - ); - } - if (commitSymbolType === commitType.REVERSE) { - const cross = gBullets.append('path'); - cross - .attr('d', `M ${x - 5},${y - 5}L${x + 5},${y + 5}M${x - 5},${y + 5}L${x + 5},${y - 5}`) - .attr( - 'class', - `commit ${typeClass} ${commit.id} commit${ - branchPos.get(commit.branch).index % THEME_COLOR_LIMIT - }` - ); - } - } - } - if (dir === 'TB' || dir === 'BT') { - commitPos.set(commit.id, { x: x, y: posWithOffset }); - } else { - commitPos.set(commit.id, { x: posWithOffset, 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 - // placing them on top of the lines. - if (modifyGraph) { - const px = 4; - const py = 2; - // Draw the commit label - if ( - commit.type !== commitType.CHERRY_PICK && - ((commit.customId && commit.type === commitType.MERGE) || - commit.type !== commitType.MERGE) && - gitGraphConfig.showCommitLabel - ) { - const wrapper = gLabels.append('g'); - const labelBkg = wrapper.insert('rect').attr('class', 'commit-label-bkg'); - - const text = wrapper - .append('text') - .attr('x', pos) - .attr('y', y + 25) - .attr('class', 'commit-label') - .text(commit.id); - let bbox = text.node().getBBox(); - - // Now we have the label, lets position the background - labelBkg - .attr('x', posWithOffset - bbox.width / 2 - py) - .attr('y', y + 13.5) - .attr('width', bbox.width + 2 * py) - .attr('height', bbox.height + 2 * py); - - if (dir === 'TB' || dir === 'BT') { - 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); - } else { - text.attr('x', posWithOffset - bbox.width / 2); - } - if (gitGraphConfig.rotateCommitLabel) { - if (dir === 'TB' || dir === 'BT') { - 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.tags.length > 0) { - let yOffset = 0; - let maxTagBboxWidth = 0; - let maxTagBboxHeight = 0; - const tagElements = []; - - for (const tagValue of commit.tags.reverse()) { - const rect = gLabels.insert('polygon'); - const hole = gLabels.append('circle'); - const tag = gLabels - .append('text') - // Note that we are delaying setting the x position until we know the width of the text - .attr('y', y - 16 - yOffset) - .attr('class', 'tag-label') - .text(tagValue); - let tagBbox = tag.node().getBBox(); - maxTagBboxWidth = Math.max(maxTagBboxWidth, tagBbox.width); - maxTagBboxHeight = Math.max(maxTagBboxHeight, tagBbox.height); - - // We don't use the max over here to center the text within the tags - tag.attr('x', posWithOffset - tagBbox.width / 2); - - tagElements.push({ - tag, - hole, - rect, - yOffset, - }); - - yOffset += 20; - } - - for (const { tag, hole, rect, yOffset } of tagElements) { - const h2 = maxTagBboxHeight / 2; - const ly = y - 19.2 - yOffset; - rect.attr('class', 'tag-label-bkg').attr( - 'points', - ` - ${pos - maxTagBboxWidth / 2 - px / 2},${ly + py} - ${pos - maxTagBboxWidth / 2 - px / 2},${ly - py} - ${posWithOffset - maxTagBboxWidth / 2 - px},${ly - h2 - py} - ${posWithOffset + maxTagBboxWidth / 2 + px},${ly - h2 - py} - ${posWithOffset + maxTagBboxWidth / 2 + px},${ly + h2 + py} - ${posWithOffset - maxTagBboxWidth / 2 - px},${ly + h2 + py}` - ); - - hole - .attr('cy', ly) - .attr('cx', pos - maxTagBboxWidth / 2 + px / 2) - .attr('r', 1.5) - .attr('class', 'tag-hole'); - - if (dir === 'TB' || dir === 'BT') { - const yOrigin = pos + yOffset; - - rect - .attr('class', 'tag-label-bkg') - .attr( - 'points', - ` - ${x},${yOrigin + py} - ${x},${yOrigin - py} - ${x + layoutOffset},${yOrigin - h2 - py} - ${x + layoutOffset + maxTagBboxWidth + px},${yOrigin - h2 - py} - ${x + layoutOffset + maxTagBboxWidth + px},${yOrigin + h2 + py} - ${x + layoutOffset},${yOrigin + h2 + py}` - ) - .attr('transform', 'translate(12,12) rotate(45, ' + x + ',' + pos + ')'); - hole - .attr('cx', x + px / 2) - .attr('cy', yOrigin) - .attr('transform', 'translate(12,12) rotate(45, ' + x + ',' + pos + ')'); - tag - .attr('x', x + 5) - .attr('y', yOrigin + 3) - .attr('transform', 'translate(14,14) rotate(45, ' + x + ',' + pos + ')'); - } - } - } - } - pos = dir === 'BT' && isParallelCommits ? pos + commitStep : pos + commitStep + layoutOffset; - if (pos > maxPos) { - maxPos = pos; - } - }); -}; - -/** - * Detect if there are commits - * between commitA's x-position - * and commitB's x-position on the - * same branch as commitA, where - * commitA isn't main - * - * @param {any} commitA - * @param {any} commitB - * @param p1 - * @param p2 - * @param {CommitMap} allCommits - * @returns {boolean} - * If there are commits between - * commitA's x-position - * and commitB's x-position - * on the source branch, where - * source branch is not main - * return true - */ -const shouldRerouteArrow = (commitA, commitB, p1, p2, allCommits) => { - const commitBIsFurthest = dir === 'TB' || dir === 'BT' ? p1.x < p2.x : p1.y < p2.y; - const branchToGetCurve = commitBIsFurthest ? commitB.branch : commitA.branch; - const isOnBranchToGetCurve = (x) => x.branch === branchToGetCurve; - const isBetweenCommits = (x) => x.seq > commitA.seq && x.seq < commitB.seq; - return [...allCommits.values()].some((commitX) => { - return isBetweenCommits(commitX) && isOnBranchToGetCurve(commitX); - }); -}; - -/** - * This function find a lane in the y-axis that is not overlapping with any other lanes. This is - * used for drawing the lines between commits. - * - * @param {any} y1 - * @param {any} y2 - * @param {any} depth - * @returns {number} Y value between y1 and y2 - */ -const findLane = (y1, y2, depth = 0) => { - const candidate = y1 + Math.abs(y1 - y2) / 2; - if (depth > 5) { - return candidate; - } - - let ok = lanes.every((lane) => Math.abs(lane - candidate) >= 10); - if (ok) { - lanes.push(candidate); - return candidate; - } - const diff = Math.abs(y1 - y2); - return findLane(y1, y2 - diff / 5, depth + 1); -}; - -/** - * Draw the lines between the commits. They were arrows initially. - * - * @param {any} svg - * @param {any} commitA - * @param {any} commitB - * @param {CommitMap} allCommits - */ -const drawArrow = (svg, commitA, commitB, allCommits) => { - const p1 = commitPos.get(commitA.id); // arrowStart - const p2 = commitPos.get(commitB.id); // arrowEnd - const arrowNeedsRerouting = shouldRerouteArrow(commitA, commitB, p1, p2, allCommits); - // log.debug('drawArrow', p1, p2, arrowNeedsRerouting, commitA.id, commitB.id); - - // Lower-right quadrant logic; top-left is 0,0 - - let arc = ''; - let arc2 = ''; - let radius = 0; - let offset = 0; - let colorClassNum = branchPos.get(commitB.branch).index; - if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) { - colorClassNum = branchPos.get(commitA.branch).index; - } - - let lineDef; - if (arrowNeedsRerouting) { - arc = 'A 10 10, 0, 0, 0,'; - arc2 = 'A 10 10, 0, 0, 1,'; - radius = 10; - offset = 10; - - 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 (dir === 'TB') { - if (p1.x < p2.x) { - // Source commit is on branch position left of destination commit - // so render arrow rightward with colour of destination branch - 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 { - // Source commit is on branch position right of destination commit - // so render arrow leftward with colour of source branch - colorClassNum = branchPos.get(commitA.branch).index; - 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 if (dir === 'BT') { - if (p1.x < p2.x) { - // Source commit is on branch position left of destination commit - // so render arrow rightward with colour of destination branch - 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 { - // Source commit is on branch position right of destination commit - // so render arrow leftward with colour of source branch - colorClassNum = branchPos.get(commitA.branch).index; - 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 { - if (p1.y < p2.y) { - // Source commit is on branch positioned above destination commit - // so render arrow downward with colour of destination branch - 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 { - // Source commit is on branch positioned below destination commit - // so render arrow upward with colour of source branch - colorClassNum = branchPos.get(commitA.branch).index; - 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 { - arc = 'A 20 20, 0, 0, 0,'; - arc2 = 'A 20 20, 0, 0, 1,'; - radius = 20; - offset = 20; - - if (dir === 'TB') { - if (p1.x < p2.x) { - if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) { - lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc} ${p1.x + offset} ${ - p2.y - } L ${p2.x} ${p2.y}`; - } else { - 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; - if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) { - lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc2} ${p1.x - offset} ${ - p2.y - } L ${p2.x} ${p2.y}`; - } else { - 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.x === p2.x) { - lineDef = `M ${p1.x} ${p1.y} L ${p2.x} ${p2.y}`; - } - } else if (dir === 'BT') { - if (p1.x < p2.x) { - if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) { - lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y + radius} ${arc2} ${p1.x + offset} ${ - p2.y - } L ${p2.x} ${p2.y}`; - } else { - 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.x > p2.x) { - arc = 'A 20 20, 0, 0, 0,'; - arc2 = 'A 20 20, 0, 0, 1,'; - radius = 20; - offset = 20; - - if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) { - lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y + radius} ${arc} ${p1.x - offset} ${ - p2.y - } L ${p2.x} ${p2.y}`; - } else { - 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.x === p2.x) { - lineDef = `M ${p1.x} ${p1.y} L ${p2.x} ${p2.y}`; - } - } else { - if (p1.y < p2.y) { - if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) { - lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc2} ${p2.x} ${ - p1.y + offset - } L ${p2.x} ${p2.y}`; - } else { - 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) { - if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) { - lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc} ${p2.x} ${ - p1.y - offset - } L ${p2.x} ${p2.y}`; - } else { - lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y + radius} ${arc2} ${p1.x + offset} ${ - p2.y - } L ${p2.x} ${p2.y}`; - } - } - - if (p1.y === p2.y) { - lineDef = `M ${p1.x} ${p1.y} L ${p2.x} ${p2.y}`; - } - } - } - svg - .append('path') - .attr('d', lineDef) - .attr('class', 'arrow arrow' + (colorClassNum % THEME_COLOR_LIMIT)); -}; - -/** - * @param {*} svg - * @param {CommitMap} commits - */ -const drawArrows = (svg, commits) => { - const gArrows = svg.append('g').attr('class', 'commit-arrows'); - [...commits.keys()].forEach((key) => { - const commit = commits.get(key); - if (commit.parents && commit.parents.length > 0) { - commit.parents.forEach((parent) => { - drawArrow(gArrows, commits.get(parent), commit, commits); - }); - } - }); -}; - -/** - * Adds the branches and the branches' labels to the svg. - * - * @param svg - * @param branches - */ -const drawBranches = (svg, branches) => { - const gitGraphConfig = getConfig().gitGraph; - const g = svg.append('g'); - branches.forEach((branch, index) => { - const adjustIndexForTheme = index % THEME_COLOR_LIMIT; - - const pos = branchPos.get(branch.name).pos; - const line = g.append('line'); - line.attr('x1', 0); - line.attr('y1', pos); - line.attr('x2', maxPos); - line.attr('y2', pos); - line.attr('class', 'branch branch' + adjustIndexForTheme); - - if (dir === 'TB') { - line.attr('y1', defaultPos); - line.attr('x1', pos); - line.attr('y2', maxPos); - line.attr('x2', pos); - } else if (dir === 'BT') { - line.attr('y1', maxPos); - line.attr('x1', pos); - line.attr('y2', defaultPos); - line.attr('x2', pos); - } - lanes.push(pos); - - let name = branch.name; - - // Create the actual text element - const labelElement = drawText(name); - // Create outer g, edgeLabel, this will be positioned after graph layout - const bkg = g.insert('rect'); - const branchLabel = g.insert('g').attr('class', 'branchLabel'); - - // Create inner g, label, this will be positioned now for centering the text - const label = branchLabel.insert('g').attr('class', 'label branch-label' + adjustIndexForTheme); - label.node().appendChild(labelElement); - let bbox = labelElement.getBBox(); - bkg - .attr('class', 'branchLabelBkg label' + adjustIndexForTheme) - .attr('rx', 4) - .attr('ry', 4) - .attr('x', -bbox.width - 4 - (gitGraphConfig.rotateCommitLabel === true ? 30 : 0)) - .attr('y', -bbox.height / 2 + 8) - .attr('width', bbox.width + 18) - .attr('height', bbox.height + 4); - label.attr( - 'transform', - 'translate(' + - (-bbox.width - 14 - (gitGraphConfig.rotateCommitLabel === true ? 30 : 0)) + - ', ' + - (pos - bbox.height / 2 - 1) + - ')' - ); - if (dir === 'TB') { - bkg.attr('x', pos - bbox.width / 2 - 10).attr('y', 0); - label.attr('transform', 'translate(' + (pos - bbox.width / 2 - 5) + ', ' + 0 + ')'); - } else if (dir === 'BT') { - bkg.attr('x', pos - bbox.width / 2 - 10).attr('y', maxPos); - label.attr('transform', 'translate(' + (pos - bbox.width / 2 - 5) + ', ' + maxPos + ')'); - } else { - bkg.attr('transform', 'translate(' + -19 + ', ' + (pos - bbox.height / 2) + ')'); - } - }); -}; - -/** - * @param txt - * @param id - * @param ver - * @param diagObj - */ -export const draw = function (txt, id, ver, diagObj) { - clear(); - const conf = getConfig(); - const gitGraphConfig = conf.gitGraph; - // try { - log.debug('in gitgraph renderer', txt + '\n', 'id:', id, ver); - - allCommitsDict = diagObj.db.getCommits(); - const branches = diagObj.db.getBranchesAsObjArray(); - 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.set(branch.name, { pos, index }); - pos += - 50 + - (gitGraphConfig.rotateCommitLabel ? 40 : 0) + - (dir === 'TB' || dir === 'BT' ? bbox.width / 2 : 0); - label.remove(); - branchLabel.remove(); - g.remove(); - }); - - drawCommits(diagram, allCommitsDict, false); - if (gitGraphConfig.showBranches) { - drawBranches(diagram, branches); - } - drawArrows(diagram, allCommitsDict); - drawCommits(diagram, allCommitsDict, true); - utils.insertTitle( - diagram, - 'gitTitleText', - gitGraphConfig.titleTopMargin, - diagObj.db.getDiagramTitle() - ); - - // Setup the view box and size of the svg element - setupGraphViewbox( - undefined, - diagram, - gitGraphConfig.diagramPadding, - gitGraphConfig.useMaxWidth ?? conf.useMaxWidth - ); -}; - -export default { - draw, -}; diff --git a/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts b/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts new file mode 100644 index 000000000..39a64a623 --- /dev/null +++ b/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts @@ -0,0 +1,1350 @@ +import { select } from 'd3'; +import { getConfig, setupGraphViewbox } from '../../diagram-api/diagramAPI.js'; +import { log } from '../../logger.js'; +import utils from '../../utils.js'; +import type { DrawDefinition } from '../../diagram-api/types.js'; +import type d3 from 'd3'; +import type { Commit, GitGraphDBRenderProvider, DiagramOrientation } from './gitGraphTypes.js'; +import { commitType } from './gitGraphTypes.js'; + +interface BranchPosition { + pos: number; + index: number; +} + +interface CommitPosition { + x: number; + y: number; +} + +interface CommitPositionOffset extends CommitPosition { + posWithOffset: number; +} + +const DEFAULT_CONFIG = getConfig(); +const DEFAULT_GITGRAPH_CONFIG = DEFAULT_CONFIG?.gitGraph; +const LAYOUT_OFFSET = 10; +const COMMIT_STEP = 40; +const PX = 4; +const PY = 2; + +const THEME_COLOR_LIMIT = 8; +const branchPos = new Map(); +const commitPos = new Map(); +const defaultPos = 30; + +let allCommitsDict = new Map(); +let lanes: number[] = []; +let maxPos = 0; +let dir: DiagramOrientation = 'LR'; + +const clear = () => { + branchPos.clear(); + commitPos.clear(); + allCommitsDict.clear(); + maxPos = 0; + lanes = []; + dir = 'LR'; +}; + +const drawText = (txt: string | string[]) => { + const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + const rows = typeof txt === 'string' ? txt.split(/\\n|\n|/gi) : txt; + + rows.forEach((row) => { + const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan'); + tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve'); + tspan.setAttribute('dy', '1em'); + tspan.setAttribute('x', '0'); + tspan.setAttribute('class', 'row'); + tspan.textContent = row.trim(); + svgLabel.appendChild(tspan); + }); + + return svgLabel; +}; + +const findClosestParent = (parents: string[]): string | undefined => { + let closestParent: string | undefined; + let comparisonFunc; + let targetPosition: number; + if (dir === 'BT') { + comparisonFunc = (a: number, b: number) => a <= b; + targetPosition = Infinity; + } else { + comparisonFunc = (a: number, b: number) => a >= b; + targetPosition = 0; + } + + parents.forEach((parent) => { + const parentPosition = + dir === 'TB' || dir == 'BT' ? commitPos.get(parent)?.y : commitPos.get(parent)?.x; + + if (parentPosition !== undefined && comparisonFunc(parentPosition, targetPosition)) { + closestParent = parent; + targetPosition = parentPosition; + } + }); + + return closestParent; +}; + +const findClosestParentBT = (parents: string[]) => { + let closestParent = ''; + let maxPosition = Infinity; + + parents.forEach((parent) => { + const parentPosition = commitPos.get(parent)!.y; + if (parentPosition <= maxPosition) { + closestParent = parent; + maxPosition = parentPosition; + } + }); + return closestParent || undefined; +}; + +const setParallelBTPos = ( + sortedKeys: string[], + commits: Map, + defaultPos: number +) => { + let curPos = defaultPos; + let maxPosition = defaultPos; + const roots: Commit[] = []; + + sortedKeys.forEach((key) => { + const commit = commits.get(key); + if (!commit) { + throw new Error(`Commit not found for key ${key}`); + } + + if (commit.parents.length) { + curPos = calculateCommitPosition(commit); + maxPosition = Math.max(curPos, maxPosition); + } else { + roots.push(commit); + } + setCommitPosition(commit, curPos); + }); + + curPos = maxPosition; + roots.forEach((commit) => { + setRootPosition(commit, curPos, defaultPos); + }); + sortedKeys.forEach((key) => { + const commit = commits.get(key); + + if (commit?.parents.length) { + const closestParent = findClosestParentBT(commit.parents)!; + curPos = commitPos.get(closestParent)!.y - COMMIT_STEP; + if (curPos <= maxPosition) { + maxPosition = curPos; + } + const x = branchPos.get(commit.branch)!.pos; + const y = curPos - LAYOUT_OFFSET; + commitPos.set(commit.id, { x: x, y: y }); + } + }); +}; + +const findClosestParentPos = (commit: Commit): number => { + const closestParent = findClosestParent(commit.parents.filter((p) => p !== null)); + if (!closestParent) { + throw new Error(`Closest parent not found for commit ${commit.id}`); + } + + const closestParentPos = commitPos.get(closestParent)?.y; + if (closestParentPos === undefined) { + throw new Error(`Closest parent position not found for commit ${commit.id}`); + } + return closestParentPos; +}; + +const calculateCommitPosition = (commit: Commit): number => { + const closestParentPos = findClosestParentPos(commit); + return closestParentPos + COMMIT_STEP; +}; + +const setCommitPosition = (commit: Commit, curPos: number): CommitPosition => { + const branch = branchPos.get(commit.branch); + + if (!branch) { + throw new Error(`Branch not found for commit ${commit.id}`); + } + + const x = branch.pos; + const y = curPos + LAYOUT_OFFSET; + commitPos.set(commit.id, { x, y }); + return { x, y }; +}; + +const setRootPosition = (commit: Commit, curPos: number, defaultPos: number) => { + const branch = branchPos.get(commit.branch); + if (!branch) { + throw new Error(`Branch not found for commit ${commit.id}`); + } + + const y = curPos + defaultPos; + const x = branch.pos; + commitPos.set(commit.id, { x, y }); +}; + +const drawCommitBullet = ( + gBullets: d3.Selection, + commit: Commit, + commitPosition: CommitPositionOffset, + typeClass: string, + branchIndex: number, + commitSymbolType: number +) => { + if (commitSymbolType === commitType.HIGHLIGHT) { + gBullets + .append('rect') + .attr('x', commitPosition.x - 10) + .attr('y', commitPosition.y - 10) + .attr('width', 20) + .attr('height', 20) + .attr( + 'class', + `commit ${commit.id} commit-highlight${branchIndex % THEME_COLOR_LIMIT} ${typeClass}-outer` + ); + gBullets + .append('rect') + .attr('x', commitPosition.x - 6) + .attr('y', commitPosition.y - 6) + .attr('width', 12) + .attr('height', 12) + .attr( + 'class', + `commit ${commit.id} commit${branchIndex % THEME_COLOR_LIMIT} ${typeClass}-inner` + ); + } else if (commitSymbolType === commitType.CHERRY_PICK) { + gBullets + .append('circle') + .attr('cx', commitPosition.x) + .attr('cy', commitPosition.y) + .attr('r', 10) + .attr('class', `commit ${commit.id} ${typeClass}`); + gBullets + .append('circle') + .attr('cx', commitPosition.x - 3) + .attr('cy', commitPosition.y + 2) + .attr('r', 2.75) + .attr('fill', '#fff') + .attr('class', `commit ${commit.id} ${typeClass}`); + gBullets + .append('circle') + .attr('cx', commitPosition.x + 3) + .attr('cy', commitPosition.y + 2) + .attr('r', 2.75) + .attr('fill', '#fff') + .attr('class', `commit ${commit.id} ${typeClass}`); + gBullets + .append('line') + .attr('x1', commitPosition.x + 3) + .attr('y1', commitPosition.y + 1) + .attr('x2', commitPosition.x) + .attr('y2', commitPosition.y - 5) + .attr('stroke', '#fff') + .attr('class', `commit ${commit.id} ${typeClass}`); + gBullets + .append('line') + .attr('x1', commitPosition.x - 3) + .attr('y1', commitPosition.y + 1) + .attr('x2', commitPosition.x) + .attr('y2', commitPosition.y - 5) + .attr('stroke', '#fff') + .attr('class', `commit ${commit.id} ${typeClass}`); + } else { + const circle = gBullets.append('circle'); + circle.attr('cx', commitPosition.x); + circle.attr('cy', commitPosition.y); + circle.attr('r', commit.type === commitType.MERGE ? 9 : 10); + circle.attr('class', `commit ${commit.id} commit${branchIndex % THEME_COLOR_LIMIT}`); + if (commitSymbolType === commitType.MERGE) { + const circle2 = gBullets.append('circle'); + circle2.attr('cx', commitPosition.x); + circle2.attr('cy', commitPosition.y); + circle2.attr('r', 6); + circle2.attr( + 'class', + `commit ${typeClass} ${commit.id} commit${branchIndex % THEME_COLOR_LIMIT}` + ); + } + if (commitSymbolType === commitType.REVERSE) { + const cross = gBullets.append('path'); + cross + .attr( + 'd', + `M ${commitPosition.x - 5},${commitPosition.y - 5}L${commitPosition.x + 5},${commitPosition.y + 5}M${commitPosition.x - 5},${commitPosition.y + 5}L${commitPosition.x + 5},${commitPosition.y - 5}` + ) + .attr('class', `commit ${typeClass} ${commit.id} commit${branchIndex % THEME_COLOR_LIMIT}`); + } + } +}; + +const drawCommitLabel = ( + gLabels: d3.Selection, + commit: Commit, + commitPosition: CommitPositionOffset, + pos: number +) => { + if ( + commit.type !== commitType.CHERRY_PICK && + ((commit.customId && commit.type === commitType.MERGE) || commit.type !== commitType.MERGE) && + DEFAULT_GITGRAPH_CONFIG?.showCommitLabel + ) { + const wrapper = gLabels.append('g'); + const labelBkg = wrapper.insert('rect').attr('class', 'commit-label-bkg'); + const text = wrapper + .append('text') + .attr('x', pos) + .attr('y', commitPosition.y + 25) + .attr('class', 'commit-label') + .text(commit.id); + const bbox = text.node()?.getBBox(); + + if (bbox) { + labelBkg + .attr('x', commitPosition.posWithOffset - bbox.width / 2 - PY) + .attr('y', commitPosition.y + 13.5) + .attr('width', bbox.width + 2 * PY) + .attr('height', bbox.height + 2 * PY); + + if (dir === 'TB' || dir === 'BT') { + labelBkg + .attr('x', commitPosition.x - (bbox.width + 4 * PX + 5)) + .attr('y', commitPosition.y - 12); + text + .attr('x', commitPosition.x - (bbox.width + 4 * PX)) + .attr('y', commitPosition.y + bbox.height - 12); + } else { + text.attr('x', commitPosition.posWithOffset - bbox.width / 2); + } + + if (DEFAULT_GITGRAPH_CONFIG.rotateCommitLabel) { + if (dir === 'TB' || dir === 'BT') { + text.attr( + 'transform', + 'rotate(' + -45 + ', ' + commitPosition.x + ', ' + commitPosition.y + ')' + ); + labelBkg.attr( + 'transform', + 'rotate(' + -45 + ', ' + commitPosition.x + ', ' + commitPosition.y + ')' + ); + } else { + const r_x = -7.5 - ((bbox.width + 10) / 25) * 9.5; + const r_y = 10 + (bbox.width / 25) * 8.5; + wrapper.attr( + 'transform', + 'translate(' + + r_x + + ', ' + + r_y + + ') rotate(' + + -45 + + ', ' + + pos + + ', ' + + commitPosition.y + + ')' + ); + } + } + } + } +}; + +const drawCommitTags = ( + gLabels: d3.Selection, + commit: Commit, + commitPosition: CommitPositionOffset, + pos: number +) => { + if (commit.tags.length > 0) { + let yOffset = 0; + let maxTagBboxWidth = 0; + let maxTagBboxHeight = 0; + const tagElements = []; + + for (const tagValue of commit.tags.reverse()) { + const rect = gLabels.insert('polygon'); + const hole = gLabels.append('circle'); + const tag = gLabels + .append('text') + .attr('y', commitPosition.y - 16 - yOffset) + .attr('class', 'tag-label') + .text(tagValue); + const tagBbox = tag.node()?.getBBox(); + if (!tagBbox) { + throw new Error('Tag bbox not found'); + } + + maxTagBboxWidth = Math.max(maxTagBboxWidth, tagBbox.width); + maxTagBboxHeight = Math.max(maxTagBboxHeight, tagBbox.height); + + tag.attr('x', commitPosition.posWithOffset - tagBbox.width / 2); + + tagElements.push({ + tag, + hole, + rect, + yOffset, + }); + + yOffset += 20; + } + + for (const { tag, hole, rect, yOffset } of tagElements) { + const h2 = maxTagBboxHeight / 2; + const ly = commitPosition.y - 19.2 - yOffset; + rect.attr('class', 'tag-label-bkg').attr( + 'points', + ` + ${pos - maxTagBboxWidth / 2 - PX / 2},${ly + PY} + ${pos - maxTagBboxWidth / 2 - PX / 2},${ly - PY} + ${commitPosition.posWithOffset - maxTagBboxWidth / 2 - PX},${ly - h2 - PY} + ${commitPosition.posWithOffset + maxTagBboxWidth / 2 + PX},${ly - h2 - PY} + ${commitPosition.posWithOffset + maxTagBboxWidth / 2 + PX},${ly + h2 + PY} + ${commitPosition.posWithOffset - maxTagBboxWidth / 2 - PX},${ly + h2 + PY}` + ); + + hole + .attr('cy', ly) + .attr('cx', pos - maxTagBboxWidth / 2 + PX / 2) + .attr('r', 1.5) + .attr('class', 'tag-hole'); + + if (dir === 'TB' || dir === 'BT') { + const yOrigin = pos + yOffset; + + rect + .attr('class', 'tag-label-bkg') + .attr( + 'points', + ` + ${commitPosition.x},${yOrigin + 2} + ${commitPosition.x},${yOrigin - 2} + ${commitPosition.x + LAYOUT_OFFSET},${yOrigin - h2 - 2} + ${commitPosition.x + LAYOUT_OFFSET + maxTagBboxWidth + 4},${yOrigin - h2 - 2} + ${commitPosition.x + LAYOUT_OFFSET + maxTagBboxWidth + 4},${yOrigin + h2 + 2} + ${commitPosition.x + LAYOUT_OFFSET},${yOrigin + h2 + 2}` + ) + .attr('transform', 'translate(12,12) rotate(45, ' + commitPosition.x + ',' + pos + ')'); + hole + .attr('cx', commitPosition.x + PX / 2) + .attr('cy', yOrigin) + .attr('transform', 'translate(12,12) rotate(45, ' + commitPosition.x + ',' + pos + ')'); + tag + .attr('x', commitPosition.x + 5) + .attr('y', yOrigin + 3) + .attr('transform', 'translate(14,14) rotate(45, ' + commitPosition.x + ',' + pos + ')'); + } + } + } +}; + +const getCommitClassType = (commit: Commit): string => { + const commitSymbolType = commit.customType ?? commit.type; + switch (commitSymbolType) { + case commitType.NORMAL: + return 'commit-normal'; + case commitType.REVERSE: + return 'commit-reverse'; + case commitType.HIGHLIGHT: + return 'commit-highlight'; + case commitType.MERGE: + return 'commit-merge'; + case commitType.CHERRY_PICK: + return 'commit-cherry-pick'; + default: + return 'commit-normal'; + } +}; + +const calculatePosition = ( + commit: Commit, + dir: string, + pos: number, + commitPos: Map +): number => { + const defaultCommitPosition = { x: 0, y: 0 }; // Default position if commit is not found + + if (commit.parents.length > 0) { + const closestParent = findClosestParent(commit.parents); + if (closestParent) { + const parentPosition = commitPos.get(closestParent) ?? defaultCommitPosition; + + if (dir === 'TB') { + return parentPosition.y + COMMIT_STEP; + } else if (dir === 'BT') { + const currentPosition = commitPos.get(commit.id) ?? defaultCommitPosition; + return currentPosition.y - COMMIT_STEP; + } else { + return parentPosition.x + COMMIT_STEP; + } + } + } else { + if (dir === 'TB') { + return defaultPos; + } else if (dir === 'BT') { + const currentPosition = commitPos.get(commit.id) ?? defaultCommitPosition; + return currentPosition.y - COMMIT_STEP; + } else { + return 0; + } + } + return 0; +}; + +const getCommitPosition = ( + commit: Commit, + pos: number, + isParallelCommits: boolean +): CommitPositionOffset => { + const posWithOffset = dir === 'BT' && isParallelCommits ? pos : pos + LAYOUT_OFFSET; + const y = dir === 'TB' || dir === 'BT' ? posWithOffset : branchPos.get(commit.branch)?.pos; + const x = dir === 'TB' || dir === 'BT' ? branchPos.get(commit.branch)?.pos : posWithOffset; + if (x === undefined || y === undefined) { + throw new Error(`Position were undefined for commit ${commit.id}`); + } + return { x, y, posWithOffset }; +}; + +const drawCommits = ( + svg: d3.Selection, + commits: Map, + modifyGraph: boolean +) => { + if (!DEFAULT_GITGRAPH_CONFIG) { + throw new Error('GitGraph config not found'); + } + const gBullets = svg.append('g').attr('class', 'commit-bullets'); + const gLabels = svg.append('g').attr('class', 'commit-labels'); + let pos = dir === 'TB' || dir === 'BT' ? defaultPos : 0; + const keys = [...commits.keys()]; + const isParallelCommits = DEFAULT_GITGRAPH_CONFIG?.parallelCommits ?? false; + + const sortKeys = (a: string, b: string) => { + const seqA = commits.get(a)?.seq; + const seqB = commits.get(b)?.seq; + return seqA !== undefined && seqB !== undefined ? seqA - seqB : 0; + }; + + let sortedKeys = keys.sort(sortKeys); + if (dir === 'BT') { + if (isParallelCommits) { + setParallelBTPos(sortedKeys, commits, pos); + } + sortedKeys = sortedKeys.reverse(); + } + + sortedKeys.forEach((key) => { + const commit = commits.get(key); + if (!commit) { + throw new Error(`Commit not found for key ${key}`); + } + if (isParallelCommits) { + pos = calculatePosition(commit, dir, pos, commitPos); + } + + const commitPosition = getCommitPosition(commit, pos, isParallelCommits); + // Don't draw the commits now but calculate the positioning which is used by the branch lines etc. + if (modifyGraph) { + const typeClass = getCommitClassType(commit); + const commitSymbolType = commit.customType ?? commit.type; + const branchIndex = branchPos.get(commit.branch)?.index ?? 0; + drawCommitBullet(gBullets, commit, commitPosition, typeClass, branchIndex, commitSymbolType); + drawCommitLabel(gLabels, commit, commitPosition, pos); + drawCommitTags(gLabels, commit, commitPosition, pos); + } + if (dir === 'TB' || dir === 'BT') { + commitPos.set(commit.id, { x: commitPosition.x, y: commitPosition.posWithOffset }); + } else { + commitPos.set(commit.id, { x: commitPosition.posWithOffset, y: commitPosition.y }); + } + pos = dir === 'BT' && isParallelCommits ? pos + COMMIT_STEP : pos + COMMIT_STEP + LAYOUT_OFFSET; + if (pos > maxPos) { + maxPos = pos; + } + }); +}; + +const shouldRerouteArrow = ( + commitA: Commit, + commitB: Commit, + p1: CommitPosition, + p2: CommitPosition, + allCommits: Map +) => { + const commitBIsFurthest = dir === 'TB' || dir === 'BT' ? p1.x < p2.x : p1.y < p2.y; + const branchToGetCurve = commitBIsFurthest ? commitB.branch : commitA.branch; + const isOnBranchToGetCurve = (x: Commit) => x.branch === branchToGetCurve; + const isBetweenCommits = (x: Commit) => x.seq > commitA.seq && x.seq < commitB.seq; + return [...allCommits.values()].some((commitX) => { + return isBetweenCommits(commitX) && isOnBranchToGetCurve(commitX); + }); +}; + +const findLane = (y1: number, y2: number, depth = 0): number => { + const candidate = y1 + Math.abs(y1 - y2) / 2; + if (depth > 5) { + return candidate; + } + + const ok = lanes.every((lane) => Math.abs(lane - candidate) >= 10); + if (ok) { + lanes.push(candidate); + return candidate; + } + const diff = Math.abs(y1 - y2); + return findLane(y1, y2 - diff / 5, depth + 1); +}; + +const drawArrow = ( + svg: d3.Selection, + commitA: Commit, + commitB: Commit, + allCommits: Map +) => { + const p1 = commitPos.get(commitA.id); // arrowStart + const p2 = commitPos.get(commitB.id); // arrowEnd + if (p1 === undefined || p2 === undefined) { + throw new Error(`Commit positions not found for commits ${commitA.id} and ${commitB.id}`); + } + const arrowNeedsRerouting = shouldRerouteArrow(commitA, commitB, p1, p2, allCommits); + // log.debug('drawArrow', p1, p2, arrowNeedsRerouting, commitA.id, commitB.id); + + // Lower-right quadrant logic; top-left is 0,0 + + let arc = ''; + let arc2 = ''; + let radius = 0; + let offset = 0; + + let colorClassNum = branchPos.get(commitB.branch)?.index; + if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) { + colorClassNum = branchPos.get(commitA.branch)?.index; + } + + let lineDef; + if (arrowNeedsRerouting) { + arc = 'A 10 10, 0, 0, 0,'; + arc2 = 'A 10 10, 0, 0, 1,'; + radius = 10; + offset = 10; + + 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 (dir === 'TB') { + if (p1.x < p2.x) { + // Source commit is on branch position left of destination commit + // so render arrow rightward with colour of destination branch + + 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 { + // Source commit is on branch position right of destination commit + // so render arrow leftward with colour of source branch + + colorClassNum = branchPos.get(commitA.branch)?.index; + + 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 if (dir === 'BT') { + if (p1.x < p2.x) { + // Source commit is on branch position left of destination commit + // so render arrow rightward with colour of destination branch + + 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 { + // Source commit is on branch position right of destination commit + // so render arrow leftward with colour of source branch + + colorClassNum = branchPos.get(commitA.branch)?.index; + + 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 { + if (p1.y < p2.y) { + // Source commit is on branch positioned above destination commit + // so render arrow downward with colour of destination branch + + 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 { + // Source commit is on branch positioned below destination commit + // so render arrow upward with colour of source branch + + colorClassNum = branchPos.get(commitA.branch)?.index; + + 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 { + arc = 'A 20 20, 0, 0, 0,'; + arc2 = 'A 20 20, 0, 0, 1,'; + radius = 20; + offset = 20; + + if (dir === 'TB') { + if (p1.x < p2.x) { + if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) { + lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc} ${p1.x + offset} ${ + p2.y + } L ${p2.x} ${p2.y}`; + } else { + 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; + if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) { + lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc2} ${p1.x - offset} ${ + p2.y + } L ${p2.x} ${p2.y}`; + } else { + 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.x === p2.x) { + lineDef = `M ${p1.x} ${p1.y} L ${p2.x} ${p2.y}`; + } + } else if (dir === 'BT') { + if (p1.x < p2.x) { + if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) { + lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y + radius} ${arc2} ${p1.x + offset} ${ + p2.y + } L ${p2.x} ${p2.y}`; + } else { + 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.x > p2.x) { + arc = 'A 20 20, 0, 0, 0,'; + arc2 = 'A 20 20, 0, 0, 1,'; + radius = 20; + offset = 20; + + if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) { + lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y + radius} ${arc} ${p1.x - offset} ${ + p2.y + } L ${p2.x} ${p2.y}`; + } else { + 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.x === p2.x) { + lineDef = `M ${p1.x} ${p1.y} L ${p2.x} ${p2.y}`; + } + } else { + if (p1.y < p2.y) { + if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) { + lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc2} ${p2.x} ${ + p1.y + offset + } L ${p2.x} ${p2.y}`; + } else { + 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) { + if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) { + lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc} ${p2.x} ${ + p1.y - offset + } L ${p2.x} ${p2.y}`; + } else { + lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y + radius} ${arc2} ${p1.x + offset} ${ + p2.y + } L ${p2.x} ${p2.y}`; + } + } + + if (p1.y === p2.y) { + lineDef = `M ${p1.x} ${p1.y} L ${p2.x} ${p2.y}`; + } + } + } + if (lineDef === undefined) { + throw new Error('Line definition not found'); + } + svg + .append('path') + .attr('d', lineDef) + .attr('class', 'arrow arrow' + (colorClassNum! % THEME_COLOR_LIMIT)); +}; + +const drawArrows = ( + svg: d3.Selection, + commits: Map +) => { + const gArrows = svg.append('g').attr('class', 'commit-arrows'); + [...commits.keys()].forEach((key) => { + const commit = commits.get(key); + + if (commit!.parents && commit!.parents.length > 0) { + commit!.parents.forEach((parent) => { + drawArrow(gArrows, commits.get(parent)!, commit!, commits); + }); + } + }); +}; + +const drawBranches = ( + svg: d3.Selection, + branches: { name: string }[] +) => { + const g = svg.append('g'); + branches.forEach((branch, index) => { + const adjustIndexForTheme = index % THEME_COLOR_LIMIT; + + const pos = branchPos.get(branch.name)?.pos; + if (pos === undefined) { + throw new Error(`Position not found for branch ${branch.name}`); + } + const line = g.append('line'); + line.attr('x1', 0); + line.attr('y1', pos); + line.attr('x2', maxPos); + line.attr('y2', pos); + line.attr('class', 'branch branch' + adjustIndexForTheme); + + if (dir === 'TB') { + line.attr('y1', defaultPos); + line.attr('x1', pos); + line.attr('y2', maxPos); + line.attr('x2', pos); + } else if (dir === 'BT') { + line.attr('y1', maxPos); + line.attr('x1', pos); + line.attr('y2', defaultPos); + line.attr('x2', pos); + } + lanes.push(pos); + + const name = branch.name; + + // Create the actual text element + const labelElement = drawText(name); + // Create outer g, edgeLabel, this will be positioned after graph layout + const bkg = g.insert('rect'); + const branchLabel = g.insert('g').attr('class', 'branchLabel'); + + // Create inner g, label, this will be positioned now for centering the text + const label = branchLabel.insert('g').attr('class', 'label branch-label' + adjustIndexForTheme); + + label.node()!.appendChild(labelElement); + const bbox = labelElement.getBBox(); + bkg + .attr('class', 'branchLabelBkg label' + adjustIndexForTheme) + .attr('rx', 4) + .attr('ry', 4) + .attr('x', -bbox.width - 4 - (DEFAULT_GITGRAPH_CONFIG?.rotateCommitLabel === true ? 30 : 0)) + .attr('y', -bbox.height / 2 + 8) + .attr('width', bbox.width + 18) + .attr('height', bbox.height + 4); + label.attr( + 'transform', + 'translate(' + + (-bbox.width - 14 - (DEFAULT_GITGRAPH_CONFIG?.rotateCommitLabel === true ? 30 : 0)) + + ', ' + + (pos - bbox.height / 2 - 1) + + ')' + ); + if (dir === 'TB') { + bkg.attr('x', pos - bbox.width / 2 - 10).attr('y', 0); + label.attr('transform', 'translate(' + (pos - bbox.width / 2 - 5) + ', ' + 0 + ')'); + } else if (dir === 'BT') { + bkg.attr('x', pos - bbox.width / 2 - 10).attr('y', maxPos); + label.attr('transform', 'translate(' + (pos - bbox.width / 2 - 5) + ', ' + maxPos + ')'); + } else { + bkg.attr('transform', 'translate(' + -19 + ', ' + (pos - bbox.height / 2) + ')'); + } + }); +}; + +const setBranchPosition = function ( + name: string, + pos: number, + index: number, + bbox: DOMRect, + rotateCommitLabel: boolean +): number { + branchPos.set(name, { pos, index }); + pos += 50 + (rotateCommitLabel ? 40 : 0) + (dir === 'TB' || dir === 'BT' ? bbox.width / 2 : 0); + return pos; +}; + +export const draw: DrawDefinition = function (txt, id, ver, diagObj) { + clear(); + + log.debug('in gitgraph renderer', txt + '\n', 'id:', id, ver); + if (!DEFAULT_GITGRAPH_CONFIG) { + throw new Error('GitGraph config not found'); + } + const rotateCommitLabel = DEFAULT_GITGRAPH_CONFIG.rotateCommitLabel ?? false; + const db = diagObj.db as GitGraphDBRenderProvider; + allCommitsDict = db.getCommits(); + const branches = db.getBranchesAsObjArray(); + dir = db.getDirection(); + const diagram = select(`[id="${id}"]`); + 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); + const bbox = labelElement.getBBox(); + + pos = setBranchPosition(branch.name, pos, index, bbox, rotateCommitLabel); + label.remove(); + branchLabel.remove(); + g.remove(); + }); + + drawCommits(diagram, allCommitsDict, false); + if (DEFAULT_GITGRAPH_CONFIG.showBranches) { + drawBranches(diagram, branches); + } + drawArrows(diagram, allCommitsDict); + drawCommits(diagram, allCommitsDict, true); + + utils.insertTitle( + diagram, + 'gitTitleText', + DEFAULT_GITGRAPH_CONFIG.titleTopMargin ?? 0, + db.getDiagramTitle() + ); + + // Setup the view box and size of the svg element + setupGraphViewbox( + undefined, + diagram, + DEFAULT_GITGRAPH_CONFIG.diagramPadding, + DEFAULT_GITGRAPH_CONFIG.useMaxWidth + ); +}; + +export default { + draw, +}; + +if (import.meta.vitest) { + const { it, expect, describe } = import.meta.vitest; + + describe('drawText', () => { + it('should drawText', () => { + const svgLabel = drawText('main'); + expect(svgLabel).toBeDefined(); + expect(svgLabel.children[0].innerHTML).toBe('main'); + }); + }); + + describe('branchPosition', () => { + const bbox: DOMRect = { + x: 0, + y: 0, + width: 10, + height: 10, + top: 0, + right: 0, + bottom: 0, + left: 0, + toJSON: () => '', + }; + + it('should setBranchPositions LR with two branches', () => { + dir = 'LR'; + + const pos = setBranchPosition('main', 0, 0, bbox, true); + expect(pos).toBe(90); + expect(branchPos.get('main')).toEqual({ pos: 0, index: 0 }); + const posNext = setBranchPosition('develop', pos, 1, bbox, true); + expect(posNext).toBe(180); + expect(branchPos.get('develop')).toEqual({ pos: pos, index: 1 }); + }); + + it('should setBranchPositions TB with two branches', () => { + dir = 'TB'; + bbox.width = 34.9921875; + + const pos = setBranchPosition('main', 0, 0, bbox, true); + expect(pos).toBe(107.49609375); + expect(branchPos.get('main')).toEqual({ pos: 0, index: 0 }); + + bbox.width = 56.421875; + const posNext = setBranchPosition('develop', pos, 1, bbox, true); + expect(posNext).toBe(225.70703125); + expect(branchPos.get('develop')).toEqual({ pos: pos, index: 1 }); + }); + }); + + describe('commitPosition', () => { + const commits = new Map([ + [ + 'commitZero', + { + id: 'ZERO', + message: '', + seq: 0, + type: commitType.NORMAL, + tags: [], + parents: [], + branch: 'main', + }, + ], + [ + 'commitA', + { + id: 'A', + message: '', + seq: 1, + type: commitType.NORMAL, + tags: [], + parents: ['ZERO'], + branch: 'feature', + }, + ], + [ + 'commitB', + { + id: 'B', + message: '', + seq: 2, + type: commitType.NORMAL, + tags: [], + parents: ['A'], + branch: 'feature', + }, + ], + [ + 'commitM', + { + id: 'M', + message: 'merged branch feature into main', + seq: 3, + type: commitType.MERGE, + tags: [], + parents: ['ZERO', 'B'], + branch: 'main', + customId: true, + }, + ], + [ + 'commitC', + { + id: 'C', + message: '', + seq: 4, + type: commitType.NORMAL, + tags: [], + parents: ['ZERO'], + branch: 'release', + }, + ], + [ + 'commit5_8928ea0', + { + id: '5-8928ea0', + message: 'cherry-picked [object Object] into release', + seq: 5, + type: commitType.CHERRY_PICK, + tags: [], + parents: ['C', 'M'], + branch: 'release', + }, + ], + [ + 'commitD', + { + id: 'D', + message: '', + seq: 6, + type: commitType.NORMAL, + tags: [], + parents: ['5-8928ea0'], + branch: 'release', + }, + ], + [ + 'commit7_ed848ba', + { + id: '7-ed848ba', + message: 'cherry-picked [object Object] into release', + seq: 7, + type: commitType.CHERRY_PICK, + tags: [], + parents: ['D', 'M'], + branch: 'release', + }, + ], + ]); + let pos = 0; + branchPos.set('main', { pos: 0, index: 0 }); + branchPos.set('feature', { pos: 107.49609375, index: 1 }); + branchPos.set('release', { pos: 224.03515625, index: 2 }); + + describe('TB', () => { + pos = 30; + dir = 'TB'; + const expectedCommitPositionTB = new Map([ + ['commitZero', { x: 0, y: 40, posWithOffset: 40 }], + ['commitA', { x: 107.49609375, y: 90, posWithOffset: 90 }], + ['commitB', { x: 107.49609375, y: 140, posWithOffset: 140 }], + ['commitM', { x: 0, y: 190, posWithOffset: 190 }], + ['commitC', { x: 224.03515625, y: 240, posWithOffset: 240 }], + ['commit5_8928ea0', { x: 224.03515625, y: 290, posWithOffset: 290 }], + ['commitD', { x: 224.03515625, y: 340, posWithOffset: 340 }], + ['commit7_ed848ba', { x: 224.03515625, y: 390, posWithOffset: 390 }], + ]); + commits.forEach((commit, key) => { + it(`should give the correct position for commit ${key}`, () => { + const position = getCommitPosition(commit, pos, false); + expect(position).toEqual(expectedCommitPositionTB.get(key)); + pos += 50; + }); + }); + }); + describe('LR', () => { + let pos = 30; + dir = 'LR'; + const expectedCommitPositionLR = new Map([ + ['commitZero', { x: 0, y: 40, posWithOffset: 40 }], + ['commitA', { x: 107.49609375, y: 90, posWithOffset: 90 }], + ['commitB', { x: 107.49609375, y: 140, posWithOffset: 140 }], + ['commitM', { x: 0, y: 190, posWithOffset: 190 }], + ['commitC', { x: 224.03515625, y: 240, posWithOffset: 240 }], + ['commit5_8928ea0', { x: 224.03515625, y: 290, posWithOffset: 290 }], + ['commitD', { x: 224.03515625, y: 340, posWithOffset: 340 }], + ['commit7_ed848ba', { x: 224.03515625, y: 390, posWithOffset: 390 }], + ]); + commits.forEach((commit, key) => { + it(`should give the correct position for commit ${key}`, () => { + const position = getCommitPosition(commit, pos, false); + expect(position).toEqual(expectedCommitPositionLR.get(key)); + pos += 50; + }); + }); + }); + describe('getCommitClassType', () => { + const expectedCommitClassType = new Map([ + ['commitZero', 'commit-normal'], + ['commitA', 'commit-normal'], + ['commitB', 'commit-normal'], + ['commitM', 'commit-merge'], + ['commitC', 'commit-normal'], + ['commit5_8928ea0', 'commit-cherry-pick'], + ['commitD', 'commit-normal'], + ['commit7_ed848ba', 'commit-cherry-pick'], + ]); + commits.forEach((commit, key) => { + it(`should give the correct class type for commit ${key}`, () => { + const classType = getCommitClassType(commit); + expect(classType).toBe(expectedCommitClassType.get(key)); + }); + }); + }); + }); + describe('building BT parallel commit diagram', () => { + const commits = new Map([ + [ + '1-abcdefg', + { + id: '1-abcdefg', + message: '', + seq: 0, + type: 0, + tags: [], + parents: [], + branch: 'main', + }, + ], + [ + '2-abcdefg', + { + id: '2-abcdefg', + message: '', + seq: 1, + type: 0, + tags: [], + parents: ['1-abcdefg'], + branch: 'main', + }, + ], + [ + '3-abcdefg', + { + id: '3-abcdefg', + message: '', + seq: 2, + type: 0, + tags: [], + parents: ['2-abcdefg'], + branch: 'develop', + }, + ], + [ + '4-abcdefg', + { + id: '4-abcdefg', + message: '', + seq: 3, + type: 0, + tags: [], + parents: ['3-abcdefg'], + branch: 'develop', + }, + ], + [ + '5-abcdefg', + { + id: '5-abcdefg', + message: '', + seq: 4, + type: 0, + tags: [], + parents: ['2-abcdefg'], + branch: 'feature', + }, + ], + [ + '6-abcdefg', + { + id: '6-abcdefg', + message: '', + seq: 5, + type: 0, + tags: [], + parents: ['5-abcdefg'], + branch: 'feature', + }, + ], + [ + '7-abcdefg', + { + id: '7-abcdefg', + message: '', + seq: 6, + type: 0, + tags: [], + parents: ['2-abcdefg'], + branch: 'main', + }, + ], + [ + '8-abcdefg', + { + id: '8-abcdefg', + message: '', + seq: 7, + type: 0, + tags: [], + parents: ['7-abcdefg'], + branch: 'main', + }, + ], + ]); + const expectedCommitPosition = new Map([ + ['1-abcdefg', { x: 0, y: 40 }], + ['2-abcdefg', { x: 0, y: 90 }], + ['3-abcdefg', { x: 107.49609375, y: 140 }], + ['4-abcdefg', { x: 107.49609375, y: 190 }], + ['5-abcdefg', { x: 225.70703125, y: 140 }], + ['6-abcdefg', { x: 225.70703125, y: 190 }], + ['7-abcdefg', { x: 0, y: 140 }], + ['8-abcdefg', { x: 0, y: 190 }], + ]); + + const expectedCommitPositionAfterParallel = new Map([ + ['1-abcdefg', { x: 0, y: 210 }], + ['2-abcdefg', { x: 0, y: 160 }], + ['3-abcdefg', { x: 107.49609375, y: 110 }], + ['4-abcdefg', { x: 107.49609375, y: 60 }], + ['5-abcdefg', { x: 225.70703125, y: 110 }], + ['6-abcdefg', { x: 225.70703125, y: 60 }], + ['7-abcdefg', { x: 0, y: 110 }], + ['8-abcdefg', { x: 0, y: 60 }], + ]); + + const expectedCommitCurrentPosition = new Map([ + ['1-abcdefg', 30], + ['2-abcdefg', 80], + ['3-abcdefg', 130], + ['4-abcdefg', 180], + ['5-abcdefg', 130], + ['6-abcdefg', 180], + ['7-abcdefg', 130], + ['8-abcdefg', 180], + ]); + const sortedKeys = [...expectedCommitPosition.keys()]; + it('should get the correct commit position and current position', () => { + dir = 'BT'; + let curPos = 30; + commitPos.clear(); + branchPos.clear(); + branchPos.set('main', { pos: 0, index: 0 }); + branchPos.set('develop', { pos: 107.49609375, index: 1 }); + branchPos.set('feature', { pos: 225.70703125, index: 2 }); + DEFAULT_GITGRAPH_CONFIG!.parallelCommits = true; + commits.forEach((commit, key) => { + if (commit.parents.length > 0) { + curPos = calculateCommitPosition(commit); + } + const position = setCommitPosition(commit, curPos); + expect(position).toEqual(expectedCommitPosition.get(key)); + expect(curPos).toEqual(expectedCommitCurrentPosition.get(key)); + }); + }); + + it('should get the correct commit position after parallel commits', () => { + commitPos.clear(); + branchPos.clear(); + dir = 'BT'; + const curPos = 30; + commitPos.clear(); + branchPos.clear(); + branchPos.set('main', { pos: 0, index: 0 }); + branchPos.set('develop', { pos: 107.49609375, index: 1 }); + branchPos.set('feature', { pos: 225.70703125, index: 2 }); + setParallelBTPos(sortedKeys, commits, curPos); + sortedKeys.forEach((commit) => { + const position = commitPos.get(commit); + expect(position).toEqual(expectedCommitPositionAfterParallel.get(commit)); + }); + }); + }); + DEFAULT_GITGRAPH_CONFIG!.parallelCommits = false; + it('add', () => { + commitPos.set('parent1', { x: 1, y: 1 }); + commitPos.set('parent2', { x: 2, y: 2 }); + commitPos.set('parent3', { x: 3, y: 3 }); + dir = 'LR'; + const parents = ['parent1', 'parent2', 'parent3']; + const closestParent = findClosestParent(parents); + + expect(closestParent).toBe('parent3'); + commitPos.clear(); + }); +} diff --git a/packages/mermaid/src/diagrams/git/gitGraphTypes.ts b/packages/mermaid/src/diagrams/git/gitGraphTypes.ts new file mode 100644 index 000000000..32b951bcc --- /dev/null +++ b/packages/mermaid/src/diagrams/git/gitGraphTypes.ts @@ -0,0 +1,134 @@ +import type { GitGraphDiagramConfig } from '../../config.type.js'; +import type { DiagramDBBase } from '../../diagram-api/types.js'; + +export const commitType = { + NORMAL: 0, + REVERSE: 1, + HIGHLIGHT: 2, + MERGE: 3, + CHERRY_PICK: 4, +} as const; + +export interface CommitDB { + msg: string; + id: string; + type: number; + tags?: string[]; +} + +export interface BranchDB { + name: string; + order: number; +} + +export interface MergeDB { + branch: string; + id: string; + type?: number; + tags?: string[]; +} + +export interface CherryPickDB { + id: string; + targetId: string; + parent: string; + tags?: string[]; +} + +export interface Commit { + id: string; + message: string; + seq: number; + type: number; + tags: string[]; + parents: string[]; + branch: string; + customType?: number; + customId?: boolean; +} + +export interface GitGraph { + statements: Statement[]; +} + +export type Statement = CommitAst | BranchAst | MergeAst | CheckoutAst | CherryPickingAst; + +export interface CommitAst { + $type: 'Commit'; + id: string; + message?: string; + tags?: string[]; + type?: 'NORMAL' | 'REVERSE' | 'HIGHLIGHT'; +} + +export interface BranchAst { + $type: 'Branch'; + name: string; + order?: number; +} + +export interface MergeAst { + $type: 'Merge'; + branch: string; + id?: string; + tags?: string[]; + type?: 'NORMAL' | 'REVERSE' | 'HIGHLIGHT'; +} + +export interface CheckoutAst { + $type: 'Checkout'; + branch: string; +} + +export interface CherryPickingAst { + $type: 'CherryPicking'; + id: string; + parent: string; + tags?: string[]; +} + +export interface GitGraphDB extends DiagramDBBase { + commitType: typeof commitType; + setDirection: (dir: DiagramOrientation) => void; + setOptions: (rawOptString: string) => void; + getOptions: () => any; + commit: (commitDB: CommitDB) => void; + branch: (branchDB: BranchDB) => void; + merge: (mergeDB: MergeDB) => void; + cherryPick: (cherryPickDB: CherryPickDB) => void; + checkout: (branch: string) => void; + prettyPrint: () => void; + clear: () => void; + getBranchesAsObjArray: () => { name: string }[]; + getBranches: () => Map; + getCommits: () => Map; + getCommitsArray: () => Commit[]; + getCurrentBranch: () => string; + getDirection: () => DiagramOrientation; + getHead: () => Commit | null; +} + +export interface GitGraphDBParseProvider extends Partial { + commitType: typeof commitType; + setDirection: (dir: DiagramOrientation) => void; + commit: (commitDB: CommitDB) => void; + branch: (branchDB: BranchDB) => void; + merge: (mergeDB: MergeDB) => void; + cherryPick: (cherryPickDB: CherryPickDB) => void; + checkout: (branch: string) => void; +} + +export interface GitGraphDBRenderProvider extends Partial { + prettyPrint: () => void; + clear: () => void; + getBranchesAsObjArray: () => { name: string }[]; + getBranches: () => Map; + getCommits: () => Map; + getCommitsArray: () => Commit[]; + getCurrentBranch: () => string; + getDirection: () => DiagramOrientation; + getHead: () => Commit | null; + getDiagramTitle: () => string; +} + +export type DiagramOrientation = 'LR' | 'TB' | 'BT'; diff --git a/packages/mermaid/src/diagrams/git/parser/gitGraph.jison b/packages/mermaid/src/diagrams/git/parser/gitGraph.jison deleted file mode 100644 index fa2c70586..000000000 --- a/packages/mermaid/src/diagrams/git/parser/gitGraph.jison +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Parse following - * gitGraph: - * commit - * commit - * branch - */ -%lex - -%x string -%x options -%x acc_title -%x acc_descr -%x acc_descr_multiline -%options case-insensitive - - -%% -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"; -(\r?\n)+ /*{console.log('New line');return 'NL';}*/ return 'NL'; -\#[^\n]* /* skip comments */ -\%%[^\n]* /* skip comments */ -"gitGraph" return 'GG'; -commit(?=\s|$) return 'COMMIT'; -"id:" return 'COMMIT_ID'; -"type:" return 'COMMIT_TYPE'; -"msg:" return 'COMMIT_MSG'; -"NORMAL" return 'NORMAL'; -"REVERSE" return 'REVERSE'; -"HIGHLIGHT" return 'HIGHLIGHT'; -"tag:" return 'COMMIT_TAG'; -branch(?=\s|$) return 'BRANCH'; -"order:" return 'ORDER'; -merge(?=\s|$) return 'MERGE'; -cherry\-pick(?=\s|$) return 'CHERRY_PICK'; -"parent:" return 'PARENT_COMMIT' -// "reset" return 'RESET'; -\b(checkout|switch)(?=\s|$) return 'CHECKOUT'; -"LR" return 'DIR'; -"TB" return 'DIR'; -"BT" return 'DIR'; -":" return ':'; -"^" return 'CARET' -"options"\r?\n this.begin("options"); // -[ \r\n\t]+"end" this.popState(); // not used anymore in the renderer, fixed for backward compatibility -[\s\S]+(?=[ \r\n\t]+"end") return 'OPT'; // -["]["] return 'EMPTYSTR'; -["] this.begin("string"); -["] this.popState(); -[^"]* return 'STR'; -[0-9]+(?=\s|$) return 'NUM'; -\w([-\./\w]*[-\w])? return 'ID'; // only a subset of https://git-scm.com/docs/git-check-ref-format -<> return 'EOF'; -\s+ /* skip all whitespace */ // lowest priority so we can use lookaheads in earlier regex - -/lex - -%left '^' - -%start start - -%% /* language grammar */ - -start - : eol start - | GG document EOF{ return $3; } - | GG ':' document EOF{ return $3; } - | GG DIR ':' document EOF {yy.setDirection($2); return $4;} - ; - - -document - : /*empty*/ - | options body { yy.setOptions($1); $$ = $2} - ; - -options - : options OPT {$1 +=$2; $$=$1} - | NL - ; -body - : /*empty*/ {$$ = []} - | body line {$1.push($2); $$=$1;} - ; -line - : statement eol {$$ =$1} - | NL - ; - -statement - : commitStatement - | mergeStatement - | cherryPickStatement - | 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($$); } | section {yy.addSection($1.substr(8));$$=$1.substr(8);} - | branchStatement - | CHECKOUT ref {yy.checkout($2)} - // | RESET reset_arg {yy.reset($2)} - ; -branchStatement - : BRANCH ref {yy.branch($2)} - | BRANCH ref ORDER NUM {yy.branch($2, $4)} - ; - -cherryPickStatement - : CHERRY_PICK COMMIT_ID STR {yy.cherryPick($3, '', undefined)} - | CHERRY_PICK COMMIT_ID STR PARENT_COMMIT STR {yy.cherryPick($3, '', undefined,$5)} - | CHERRY_PICK COMMIT_ID STR commitTags {yy.cherryPick($3, '', $4)} - | CHERRY_PICK COMMIT_ID STR PARENT_COMMIT STR commitTags {yy.cherryPick($3, '', $6,$5)} - | CHERRY_PICK COMMIT_ID STR commitTags PARENT_COMMIT STR {yy.cherryPick($3, '', $4,$6)} - | CHERRY_PICK commitTags COMMIT_ID STR {yy.cherryPick($4, '', $2)} - | CHERRY_PICK commitTags COMMIT_ID STR PARENT_COMMIT STR {yy.cherryPick($4, '', $2,$6)} - ; - -mergeStatement - : MERGE ref {yy.merge($2,'','', undefined)} - | MERGE ref COMMIT_ID STR {yy.merge($2, $4,'', undefined)} - | MERGE ref COMMIT_TYPE commitType {yy.merge($2,'', $4, undefined)} - | MERGE ref commitTags {yy.merge($2, '','',$3)} - | MERGE ref commitTags COMMIT_ID STR {yy.merge($2, $5,'', $3)} - | MERGE ref commitTags COMMIT_TYPE commitType {yy.merge($2, '',$5, $3)} - | MERGE ref COMMIT_TYPE commitType commitTags {yy.merge($2, '',$4, $5)} - | MERGE ref COMMIT_ID STR COMMIT_TYPE commitType {yy.merge($2, $4, $6, undefined)} - | MERGE ref COMMIT_ID STR commitTags {yy.merge($2, $4, '', $5)} - | MERGE ref COMMIT_TYPE commitType COMMIT_ID STR {yy.merge($2, $6,$4, undefined)} - | MERGE ref COMMIT_ID STR COMMIT_TYPE commitType commitTags {yy.merge($2, $4, $6, $7)} - | MERGE ref COMMIT_TYPE commitType commitTags COMMIT_ID STR {yy.merge($2, $7, $4, $5)} - | MERGE ref COMMIT_ID STR commitTags COMMIT_TYPE commitType {yy.merge($2, $4, $7, $5)} - | MERGE ref COMMIT_TYPE commitType COMMIT_ID STR commitTags {yy.merge($2, $6, $4, $7)} - | MERGE ref commitTags COMMIT_TYPE commitType COMMIT_ID STR {yy.merge($2, $7, $5, $3)} - | MERGE ref commitTags COMMIT_ID STR COMMIT_TYPE commitType {yy.merge($2, $5, $7, $3)} - ; - -commitStatement - : COMMIT commit_arg {yy.commit($2)} - | COMMIT commitTags {yy.commit('','',yy.commitType.NORMAL,$2)} - | COMMIT COMMIT_TYPE commitType {yy.commit('','',$3, undefined)} - | COMMIT commitTags COMMIT_TYPE commitType {yy.commit('','',$4,$2)} - | COMMIT COMMIT_TYPE commitType commitTags {yy.commit('','',$3,$4)} - | COMMIT COMMIT_ID STR {yy.commit('',$3,yy.commitType.NORMAL, undefined)} - | COMMIT COMMIT_ID STR commitTags {yy.commit('',$3,yy.commitType.NORMAL,$4)} - | COMMIT commitTags COMMIT_ID STR {yy.commit('',$4,yy.commitType.NORMAL,$2)} - | COMMIT COMMIT_ID STR COMMIT_TYPE commitType {yy.commit('',$3,$5, undefined)} - | COMMIT COMMIT_TYPE commitType COMMIT_ID STR {yy.commit('',$5,$3, undefined)} - | COMMIT COMMIT_ID STR COMMIT_TYPE commitType commitTags {yy.commit('',$3,$5,$6)} - | COMMIT COMMIT_ID STR commitTags COMMIT_TYPE commitType {yy.commit('',$3,$6,$4)} - | COMMIT COMMIT_TYPE commitType COMMIT_ID STR commitTags {yy.commit('',$5,$3,$6)} - | COMMIT COMMIT_TYPE commitType commitTags COMMIT_ID STR {yy.commit('',$6,$3,$4)} - | COMMIT commitTags COMMIT_TYPE commitType COMMIT_ID STR {yy.commit('',$6,$4,$2)} - | COMMIT commitTags COMMIT_ID STR COMMIT_TYPE commitType {yy.commit('',$4,$6,$2)} - | COMMIT COMMIT_MSG STR {yy.commit($3,'',yy.commitType.NORMAL, undefined)} - | COMMIT commitTags COMMIT_MSG STR {yy.commit($4,'',yy.commitType.NORMAL,$2)} - | COMMIT COMMIT_MSG STR commitTags {yy.commit($3,'',yy.commitType.NORMAL,$4)} - | COMMIT COMMIT_MSG STR COMMIT_TYPE commitType {yy.commit($3,'',$5, undefined)} - | COMMIT COMMIT_TYPE commitType COMMIT_MSG STR {yy.commit($5,'',$3, undefined)} - | COMMIT COMMIT_ID STR COMMIT_MSG STR {yy.commit($5,$3,yy.commitType.NORMAL, undefined)} - | COMMIT COMMIT_MSG STR COMMIT_ID STR {yy.commit($3,$5,yy.commitType.NORMAL, undefined)} - - | COMMIT COMMIT_MSG STR COMMIT_TYPE commitType commitTags {yy.commit($3,'',$5,$6)} - | COMMIT COMMIT_MSG STR commitTags COMMIT_TYPE commitType {yy.commit($3,'',$6,$4)} - | COMMIT COMMIT_TYPE commitType COMMIT_MSG STR commitTags {yy.commit($5,'',$3,$6)} - | COMMIT COMMIT_TYPE commitType commitTags COMMIT_MSG STR {yy.commit($6,'',$3,$4)} - | COMMIT commitTags COMMIT_TYPE commitType COMMIT_MSG STR {yy.commit($6,'',$4,$2)} - | COMMIT commitTags COMMIT_MSG STR COMMIT_TYPE commitType {yy.commit($4,'',$6,$2)} - - | COMMIT COMMIT_MSG STR COMMIT_TYPE commitType COMMIT_ID STR {yy.commit($3,$7,$5, undefined)} - | COMMIT COMMIT_MSG STR COMMIT_ID STR COMMIT_TYPE commitType {yy.commit($3,$5,$7, undefined)} - | COMMIT COMMIT_TYPE commitType COMMIT_MSG STR COMMIT_ID STR {yy.commit($5,$7,$3, undefined)} - | COMMIT COMMIT_TYPE commitType COMMIT_ID STR COMMIT_MSG STR {yy.commit($7,$5,$3, undefined)} - | COMMIT COMMIT_ID STR COMMIT_TYPE commitType COMMIT_MSG STR {yy.commit($7,$3,$5, undefined)} - | COMMIT COMMIT_ID STR COMMIT_MSG STR COMMIT_TYPE commitType {yy.commit($5,$3,$7, undefined)} - - | COMMIT COMMIT_MSG STR commitTags COMMIT_ID STR {yy.commit($3,$6,yy.commitType.NORMAL,$4)} - | COMMIT COMMIT_MSG STR COMMIT_ID STR commitTags {yy.commit($3,$5,yy.commitType.NORMAL,$6)} - | COMMIT commitTags COMMIT_MSG STR COMMIT_ID STR {yy.commit($4,$6,yy.commitType.NORMAL,$2)} - | COMMIT commitTags COMMIT_ID STR COMMIT_MSG STR {yy.commit($6,$4,yy.commitType.NORMAL,$2)} - | COMMIT COMMIT_ID STR commitTags COMMIT_MSG STR {yy.commit($6,$3,yy.commitType.NORMAL,$4)} - | COMMIT COMMIT_ID STR COMMIT_MSG STR commitTags {yy.commit($5,$3,yy.commitType.NORMAL,$6)} - - | COMMIT COMMIT_MSG STR COMMIT_ID STR COMMIT_TYPE commitType commitTags {yy.commit($3,$5,$7,$8)} - | COMMIT COMMIT_MSG STR COMMIT_ID STR commitTags COMMIT_TYPE commitType {yy.commit($3,$5,$8,$6)} - | COMMIT COMMIT_MSG STR COMMIT_TYPE commitType COMMIT_ID STR commitTags {yy.commit($3,$7,$5,$8)} - | COMMIT COMMIT_MSG STR COMMIT_TYPE commitType commitTags COMMIT_ID STR {yy.commit($3,$8,$5,$6)} - | COMMIT COMMIT_MSG STR commitTags COMMIT_ID STR COMMIT_TYPE commitType {yy.commit($3,$6,$8,$4)} - | COMMIT COMMIT_MSG STR commitTags COMMIT_TYPE commitType COMMIT_ID STR {yy.commit($3,$8,$6,$4)} - - | COMMIT COMMIT_ID STR COMMIT_MSG STR COMMIT_TYPE commitType commitTags {yy.commit($5,$3,$7,$8)} - | COMMIT COMMIT_ID STR COMMIT_MSG STR commitTags COMMIT_TYPE commitType {yy.commit($5,$3,$8,$6)} - | COMMIT COMMIT_ID STR COMMIT_TYPE commitType COMMIT_MSG STR commitTags {yy.commit($7,$3,$5,$8)} - | COMMIT COMMIT_ID STR COMMIT_TYPE commitType commitTags COMMIT_MSG STR {yy.commit($8,$3,$5,$6)} - | COMMIT COMMIT_ID STR commitTags COMMIT_MSG STR COMMIT_TYPE commitType {yy.commit($6,$3,$8,$4)} - | COMMIT COMMIT_ID STR commitTags COMMIT_TYPE commitType COMMIT_MSG STR {yy.commit($8,$3,$6,$4)} - - | COMMIT commitTags COMMIT_ID STR COMMIT_TYPE commitType COMMIT_MSG STR {yy.commit($8,$4,$6,$2)} - | COMMIT commitTags COMMIT_ID STR COMMIT_MSG STR COMMIT_TYPE commitType {yy.commit($6,$4,$8,$2)} - | COMMIT commitTags COMMIT_TYPE commitType COMMIT_ID STR COMMIT_MSG STR {yy.commit($8,$6,$4,$2)} - | COMMIT commitTags COMMIT_TYPE commitType COMMIT_MSG STR COMMIT_ID STR {yy.commit($6,$8,$4,$2)} - | COMMIT commitTags COMMIT_MSG STR COMMIT_ID STR COMMIT_TYPE commitType {yy.commit($4,$6,$8,$2)} - | COMMIT commitTags COMMIT_MSG STR COMMIT_TYPE commitType COMMIT_ID STR {yy.commit($4,$8,$6,$2)} - - | COMMIT COMMIT_TYPE commitType COMMIT_ID STR COMMIT_MSG STR commitTags {yy.commit($7,$5,$3,$8)} - | COMMIT COMMIT_TYPE commitType COMMIT_ID STR commitTags COMMIT_MSG STR {yy.commit($8,$5,$3,$6)} - | COMMIT COMMIT_TYPE commitType commitTags COMMIT_MSG STR COMMIT_ID STR {yy.commit($6,$8,$3,$4)} - | COMMIT COMMIT_TYPE commitType commitTags COMMIT_ID STR COMMIT_MSG STR {yy.commit($8,$6,$3,$4)} - | COMMIT COMMIT_TYPE commitType COMMIT_MSG STR COMMIT_ID STR commitTags {yy.commit($5,$7,$3,$8)} - | COMMIT COMMIT_TYPE commitType COMMIT_MSG STR commitTags COMMIT_ID STR {yy.commit($5,$8,$3,$6)} - ; -commit_arg - : /* empty */ {$$ = ""} - | STR {$$=$1} - ; -commitType - : NORMAL { $$=yy.commitType.NORMAL;} - | REVERSE { $$=yy.commitType.REVERSE;} - | HIGHLIGHT { $$=yy.commitType.HIGHLIGHT;} - ; -commitTags - : COMMIT_TAG STR {$$=[$2]} - | COMMIT_TAG EMPTYSTR {$$=['']} - | commitTags COMMIT_TAG STR {$commitTags.push($3); $$=$commitTags;} - | commitTags COMMIT_TAG EMPTYSTR {$commitTags.push(''); $$=$commitTags;} - ; - -ref - : ID - | STR - ; - -eol - : NL - | ';' - | EOF - ; -// reset_arg -// : 'HEAD' reset_parents{$$ = $1+ ":" + $2 } -// | ID reset_parents{$$ = $1+ ":" + yy.count; yy.count = 0} -// ; -// reset_parents -// : /* empty */ {yy.count = 0} -// | CARET reset_parents { yy.count += 1 } -// ; diff --git a/packages/mermaid/src/diagrams/info/infoRenderer.ts b/packages/mermaid/src/diagrams/info/infoRenderer.ts index 25ae72fce..a8314eb72 100644 --- a/packages/mermaid/src/diagrams/info/infoRenderer.ts +++ b/packages/mermaid/src/diagrams/info/infoRenderer.ts @@ -1,7 +1,7 @@ +import type { DrawDefinition, SVG, SVGGroup } from '../../diagram-api/types.js'; import { log } from '../../logger.js'; -import { configureSvgSize } from '../../setupGraphViewbox.js'; -import type { DrawDefinition, 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. @@ -16,7 +16,7 @@ const draw: DrawDefinition = (text, id, version) => { const svg: SVG = selectSvgElement(id); configureSvgSize(svg, 100, 400, true); - const group: Group = svg.append('g'); + const group: SVGGroup = svg.append('g'); group .append('text') .attr('x', 100) diff --git a/packages/mermaid/src/diagrams/packet/renderer.ts b/packages/mermaid/src/diagrams/packet/renderer.ts index c89e055cc..25445a228 100644 --- a/packages/mermaid/src/diagrams/packet/renderer.ts +++ b/packages/mermaid/src/diagrams/packet/renderer.ts @@ -1,6 +1,6 @@ import type { Diagram } from '../../Diagram.js'; import type { PacketDiagramConfig } from '../../config.type.js'; -import type { DiagramRenderer, DrawDefinition, Group, SVG } from '../../diagram-api/types.js'; +import type { DiagramRenderer, DrawDefinition, SVG, SVGGroup } from '../../diagram-api/types.js'; import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; import { configureSvgSize } from '../../setupGraphViewbox.js'; import type { PacketDB, PacketWord } from './types.js'; @@ -39,7 +39,7 @@ const drawWord = ( rowNumber: number, { rowHeight, paddingX, paddingY, bitWidth, bitsPerRow, showBits }: Required ) => { - const group: Group = svg.append('g'); + const group: SVGGroup = svg.append('g'); const wordY = rowNumber * (rowHeight + paddingY) + paddingY; for (const block of word) { const blockX = (block.start % bitsPerRow) * bitWidth + 1; diff --git a/packages/mermaid/src/diagrams/pie/pieRenderer.ts b/packages/mermaid/src/diagrams/pie/pieRenderer.ts index 8f3b9cc5b..a0cdce3df 100644 --- a/packages/mermaid/src/diagrams/pie/pieRenderer.ts +++ b/packages/mermaid/src/diagrams/pie/pieRenderer.ts @@ -1,13 +1,13 @@ 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 '../../diagram-api/diagramAPI.js'; -import { cleanAndMerge, parseFontSize } from '../../utils.js'; -import type { DrawDefinition, Group, SVG } from '../../diagram-api/types.js'; -import type { D3Section, PieDB, Sections } from './pieTypes.js'; +import { arc, pie as d3pie, scaleOrdinal } from 'd3'; import type { MermaidConfig, PieDiagramConfig } from '../../config.type.js'; +import { getConfig } from '../../diagram-api/diagramAPI.js'; +import type { DrawDefinition, SVG, SVGGroup } from '../../diagram-api/types.js'; +import { log } from '../../logger.js'; import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; +import { configureSvgSize } from '../../setupGraphViewbox.js'; +import { cleanAndMerge, parseFontSize } from '../../utils.js'; +import type { D3Section, PieDB, Sections } from './pieTypes.js'; const createPieArcs = (sections: Sections): d3.PieArcDatum[] => { // Compute the position of each group on the pie: @@ -46,7 +46,7 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => { const height = 450; const pieWidth: number = height; const svg: SVG = selectSvgElement(id); - const group: Group = svg.append('g'); + const group: SVGGroup = svg.append('g'); group.attr('transform', 'translate(' + pieWidth / 2 + ',' + height / 2 + ')'); const { themeVariables } = globalConfig; diff --git a/packages/mermaid/src/diagrams/requirement/requirementRenderer.js b/packages/mermaid/src/diagrams/requirement/requirementRenderer.js index 6b5143752..778dc36b1 100644 --- a/packages/mermaid/src/diagrams/requirement/requirementRenderer.js +++ b/packages/mermaid/src/diagrams/requirement/requirementRenderer.js @@ -1,11 +1,11 @@ import { line, select } from 'd3'; import { layout as dagreLayout } from 'dagre-d3-es/src/dagre/index.js'; import * as graphlib from 'dagre-d3-es/src/graphlib/index.js'; +import { getConfig } from '../../diagram-api/diagramAPI.js'; import { log } from '../../logger.js'; import { configureSvgSize } from '../../setupGraphViewbox.js'; import common from '../common/common.js'; import markers from './requirementMarkers.js'; -import { getConfig } from '../../diagram-api/diagramAPI.js'; let conf = {}; let relCnt = 0; diff --git a/packages/mermaid/src/diagrams/state/dataFetcher.js b/packages/mermaid/src/diagrams/state/dataFetcher.js index 35cde69ab..921544ff2 100644 --- a/packages/mermaid/src/diagrams/state/dataFetcher.js +++ b/packages/mermaid/src/diagrams/state/dataFetcher.js @@ -166,43 +166,11 @@ function insertOrUpdateNode(nodes, nodeData, classes) { * @returns {string} */ function getClassesFromDbInfo(dbInfoItem) { - if (dbInfoItem === undefined || dbInfoItem === null) { - return ''; - } else { - if (dbInfoItem.classes) { - let classStr = ''; - // for each class in classes, add it to the string as comma separated - for (let i = 0; i < dbInfoItem.classes.length; i++) { - //do not add comma for the last class - if (i === dbInfoItem.classes.length - 1) { - classStr += dbInfoItem.classes[i]; - } - //add comma for all other classes - else { - classStr += dbInfoItem.classes[i] + ' '; - } - } - return classStr; - } else { - return ''; - } - } + return dbInfoItem?.classes?.join(' ') ?? ''; } -/** - * Get classes from the db for the info item. - * If there aren't any or if dbInfoItem isn't defined, return an empty string. - * Else create 1 string from the list of classes found - */ + function getStylesFromDbInfo(dbInfoItem) { - if (dbInfoItem === undefined || dbInfoItem === null) { - return; - } else { - if (dbInfoItem.styles) { - return dbInfoItem.styles; - } else { - return []; - } - } + return dbInfoItem?.styles ?? []; } export const dataFetcher = ( @@ -224,10 +192,10 @@ export const dataFetcher = ( if (itemId !== 'root') { let shape = SHAPE_STATE; + // The if === true / false can be removed if we can guarantee that the parsedItem.start is always a boolean if (parsedItem.start === true) { shape = SHAPE_START; - } - if (parsedItem.start === false) { + } else if (parsedItem.start === false) { shape = SHAPE_END; } if (parsedItem.type !== DEFAULT_STATE_TYPE) { diff --git a/packages/mermaid/src/diagrams/state/stateRenderer-v3-unified.ts b/packages/mermaid/src/diagrams/state/stateRenderer-v3-unified.ts index 1f1da6cf2..109417c03 100644 --- a/packages/mermaid/src/diagrams/state/stateRenderer-v3-unified.ts +++ b/packages/mermaid/src/diagrams/state/stateRenderer-v3-unified.ts @@ -1,7 +1,7 @@ import { getConfig } from '../../diagram-api/diagramAPI.js'; import type { DiagramStyleClassDef } from '../../diagram-api/types.js'; import { log } from '../../logger.js'; -import { getDiagramElements } from '../../rendering-util/insertElementsForSize.js'; +import { getDiagramElement } from '../../rendering-util/insertElementsForSize.js'; import { render } from '../../rendering-util/render.js'; import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js'; import type { LayoutData } from '../../rendering-util/types.js'; @@ -55,7 +55,7 @@ export const draw = async function (text: string, id: string, _version: string, const data4Layout = diag.db.getData() as LayoutData; // Create the root SVG - the element is the div containing the SVG element - const { element, svg } = getDiagramElements(id, securityLevel); + const svg = getDiagramElement(id, securityLevel); data4Layout.type = diag.type; data4Layout.layoutAlgorithm = layout; @@ -67,10 +67,10 @@ export const draw = async function (text: string, id: string, _version: string, data4Layout.markers = ['barb']; data4Layout.diagramId = id; // console.log('REF1:', data4Layout); - await render(data4Layout, svg, element); + await render(data4Layout, svg); const padding = 8; utils.insertTitle( - element, + svg, 'statediagramTitleText', conf?.titleTopMargin ?? 25, diag.db.getDiagramTitle() diff --git a/packages/mermaid/src/diagrams/timeline/parser/timeline.jison b/packages/mermaid/src/diagrams/timeline/parser/timeline.jison index 348c31fad..89bfd06f4 100644 --- a/packages/mermaid/src/diagrams/timeline/parser/timeline.jison +++ b/packages/mermaid/src/diagrams/timeline/parser/timeline.jison @@ -18,7 +18,7 @@ \#[^\n]* /* skip comments */ "timeline" return 'timeline'; -"title"\s[^#\n;]+ return 'title'; +"title"\s[^\n]+ return 'title'; 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'; } @@ -26,11 +26,11 @@ accDescr\s*":"\s* { this.begin("ac accDescr\s*"{"\s* { this.begin("acc_descr_multiline");} [\}] { this.popState(); } [^\}]* return "acc_descr_multiline_value"; -"section"\s[^#:\n;]+ return 'section'; +"section"\s[^:\n]+ return 'section'; // event starting with "==>" keyword -":"\s[^#:\n;]+ return 'event'; -[^#:\n;]+ return 'period'; +":"\s[^:\n]+ return 'event'; +[^#:\n]+ return 'period'; <> return 'EOF'; diff --git a/packages/mermaid/src/diagrams/timeline/timeline.spec.js b/packages/mermaid/src/diagrams/timeline/timeline.spec.js index 69b9df1ba..a7005cada 100644 --- a/packages/mermaid/src/diagrams/timeline/timeline.spec.js +++ b/packages/mermaid/src/diagrams/timeline/timeline.spec.js @@ -1,6 +1,7 @@ +import { setLogLevel } from '../../diagram-api/diagramAPI.js'; +import * as commonDb from '../common/commonDb.js'; import { parser as timeline } from './parser/timeline.jison'; import * as timelineDB from './timelineDb.js'; -import { setLogLevel } from '../../diagram-api/diagramAPI.js'; describe('when parsing a timeline ', function () { beforeEach(function () { @@ -9,7 +10,7 @@ describe('when parsing a timeline ', function () { setLogLevel('trace'); }); describe('Timeline', function () { - it('TL-1 should handle a simple section definition abc-123', function () { + it('should handle a simple section definition abc-123', function () { let str = `timeline section abc-123`; @@ -17,7 +18,7 @@ describe('when parsing a timeline ', function () { expect(timelineDB.getSections()).to.deep.equal(['abc-123']); }); - it('TL-2 should handle a simple section and only two tasks', function () { + it('should handle a simple section and only two tasks', function () { let str = `timeline section abc-123 task1 @@ -29,7 +30,7 @@ describe('when parsing a timeline ', function () { }); }); - it('TL-3 should handle a two section and two coressponding tasks', function () { + it('should handle a two section and two coressponding tasks', function () { let str = `timeline section abc-123 task1 @@ -50,7 +51,7 @@ describe('when parsing a timeline ', function () { }); }); - it('TL-4 should handle a section, and task and its events', function () { + it('should handle a section, and task and its events', function () { let str = `timeline section abc-123 task1: event1 @@ -74,7 +75,7 @@ describe('when parsing a timeline ', function () { }); }); - it('TL-5 should handle a section, and task and its multi line events', function () { + it('should handle a section, and task and its multi line events', function () { let str = `timeline section abc-123 task1: event1 @@ -98,5 +99,42 @@ describe('when parsing a timeline ', function () { } }); }); + + it('should handle a title, section, task, and events with semicolons', function () { + let str = `timeline + title ;my;title; + section ;a;bc-123; + ;ta;sk1;: ;ev;ent1; : ;ev;ent2; : ;ev;ent3; + `; + timeline.parse(str); + expect(commonDb.getDiagramTitle()).equal(';my;title;'); + expect(timelineDB.getSections()).to.deep.equal([';a;bc-123;']); + expect(timelineDB.getTasks()[0].events).toMatchInlineSnapshot(` + [ + ";ev;ent1; ", + ";ev;ent2; ", + ";ev;ent3;", + ] + `); + }); + + it('should handle a title, section, task, and events with hashtags', function () { + let str = `timeline + title #my#title# + section #a#bc-123# + task1: #ev#ent1# : #ev#ent2# : #ev#ent3# + `; + timeline.parse(str); + expect(commonDb.getDiagramTitle()).equal('#my#title#'); + expect(timelineDB.getSections()).to.deep.equal(['#a#bc-123#']); + expect(timelineDB.getTasks()[0].task).equal('task1'); + expect(timelineDB.getTasks()[0].events).toMatchInlineSnapshot(` + [ + "#ev#ent1# ", + "#ev#ent2# ", + "#ev#ent3#", + ] + `); + }); }); }); diff --git a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/index.ts b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/index.ts index cde0d6a93..a1ec492f6 100644 --- a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/index.ts +++ b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/index.ts @@ -1,4 +1,4 @@ -import type { Group } from '../../../../../diagram-api/types.js'; +import type { SVGGroup } from '../../../../../diagram-api/types.js'; import type { AxisDataType, ChartComponent, @@ -25,7 +25,7 @@ export function getAxis( data: AxisDataType, axisConfig: XYChartAxisConfig, axisThemeConfig: XYChartAxisThemeConfig, - tmpSVGGroup: Group + tmpSVGGroup: SVGGroup ): Axis { const textDimensionCalculator = new TextDimensionCalculatorWithFont(tmpSVGGroup); if (isBandAxisData(data)) { diff --git a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/chartTitle.ts b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/chartTitle.ts index bbab56bdc..5512df988 100644 --- a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/chartTitle.ts +++ b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/chartTitle.ts @@ -1,13 +1,13 @@ -import type { Group } from '../../../../diagram-api/types.js'; +import type { SVGGroup } from '../../../../diagram-api/types.js'; import type { BoundingRect, ChartComponent, Dimension, DrawableElem, Point, + XYChartConfig, XYChartData, XYChartThemeConfig, - XYChartConfig, } from '../interfaces.js'; import type { TextDimensionCalculator } from '../textDimensionCalculator.js'; import { TextDimensionCalculatorWithFont } from '../textDimensionCalculator.js'; @@ -84,7 +84,7 @@ export function getChartTitleComponent( chartConfig: XYChartConfig, chartData: XYChartData, chartThemeConfig: XYChartThemeConfig, - tmpSVGGroup: Group + tmpSVGGroup: SVGGroup ): ChartComponent { const textDimensionCalculator = new TextDimensionCalculatorWithFont(tmpSVGGroup); return new ChartTitle(textDimensionCalculator, chartConfig, chartData, chartThemeConfig); diff --git a/packages/mermaid/src/diagrams/xychart/chartBuilder/index.ts b/packages/mermaid/src/diagrams/xychart/chartBuilder/index.ts index 192eb47f6..6a1b6ec3a 100644 --- a/packages/mermaid/src/diagrams/xychart/chartBuilder/index.ts +++ b/packages/mermaid/src/diagrams/xychart/chartBuilder/index.ts @@ -1,4 +1,4 @@ -import type { Group } from '../../../diagram-api/types.js'; +import type { SVGGroup } from '../../../diagram-api/types.js'; import type { DrawableElem, XYChartConfig, XYChartData, XYChartThemeConfig } from './interfaces.js'; import { Orchestrator } from './orchestrator.js'; @@ -7,7 +7,7 @@ export class XYChartBuilder { config: XYChartConfig, chartData: XYChartData, chartThemeConfig: XYChartThemeConfig, - tmpSVGGroup: Group + tmpSVGGroup: SVGGroup ): DrawableElem[] { const orchestrator = new Orchestrator(config, chartData, chartThemeConfig, tmpSVGGroup); return orchestrator.getDrawableElement(); diff --git a/packages/mermaid/src/diagrams/xychart/chartBuilder/orchestrator.ts b/packages/mermaid/src/diagrams/xychart/chartBuilder/orchestrator.ts index 8160d1500..8809efe26 100644 --- a/packages/mermaid/src/diagrams/xychart/chartBuilder/orchestrator.ts +++ b/packages/mermaid/src/diagrams/xychart/chartBuilder/orchestrator.ts @@ -1,3 +1,9 @@ +import type { SVGGroup } from '../../../diagram-api/types.js'; +import type { Axis } from './components/axis/index.js'; +import { getAxis } from './components/axis/index.js'; +import { getChartTitleComponent } from './components/chartTitle.js'; +import type { Plot } from './components/plot/index.js'; +import { getPlotComponent } from './components/plot/index.js'; import type { ChartComponent, DrawableElem, @@ -6,12 +12,6 @@ import type { XYChartThemeConfig, } from './interfaces.js'; import { isBarPlot } from './interfaces.js'; -import type { Axis } from './components/axis/index.js'; -import { getAxis } from './components/axis/index.js'; -import { getChartTitleComponent } from './components/chartTitle.js'; -import type { Plot } from './components/plot/index.js'; -import { getPlotComponent } from './components/plot/index.js'; -import type { Group } from '../../../diagram-api/types.js'; export class Orchestrator { private componentStore: { @@ -24,7 +24,7 @@ export class Orchestrator { private chartConfig: XYChartConfig, private chartData: XYChartData, chartThemeConfig: XYChartThemeConfig, - tmpSVGGroup: Group + tmpSVGGroup: SVGGroup ) { this.componentStore = { title: getChartTitleComponent(chartConfig, chartData, chartThemeConfig, tmpSVGGroup), diff --git a/packages/mermaid/src/diagrams/xychart/chartBuilder/textDimensionCalculator.ts b/packages/mermaid/src/diagrams/xychart/chartBuilder/textDimensionCalculator.ts index 8049bf527..0f118fc92 100644 --- a/packages/mermaid/src/diagrams/xychart/chartBuilder/textDimensionCalculator.ts +++ b/packages/mermaid/src/diagrams/xychart/chartBuilder/textDimensionCalculator.ts @@ -1,13 +1,13 @@ -import type { Dimension } from './interfaces.js'; +import type { SVGGroup } from '../../../diagram-api/types.js'; import { computeDimensionOfText } from '../../../rendering-util/createText.js'; -import type { Group } from '../../../diagram-api/types.js'; +import type { Dimension } from './interfaces.js'; export interface TextDimensionCalculator { getMaxDimension(texts: string[], fontSize: number): Dimension; } export class TextDimensionCalculatorWithFont implements TextDimensionCalculator { - constructor(private parentGroup: Group) {} + constructor(private parentGroup: SVGGroup) {} getMaxDimension(texts: string[], fontSize: number): Dimension { if (!this.parentGroup) { return { diff --git a/packages/mermaid/src/diagrams/xychart/xychartDb.ts b/packages/mermaid/src/diagrams/xychart/xychartDb.ts index 23b90724c..fb2435df2 100644 --- a/packages/mermaid/src/diagrams/xychart/xychartDb.ts +++ b/packages/mermaid/src/diagrams/xychart/xychartDb.ts @@ -1,3 +1,9 @@ +import * as configApi from '../../config.js'; +import defaultConfig from '../../defaultConfig.js'; +import type { SVGGroup } from '../../diagram-api/types.js'; +import { getThemeVariables } from '../../themes/theme-default.js'; +import { cleanAndMerge } from '../../utils.js'; +import { sanitizeText } from '../common/common.js'; import { clear as commonClear, getAccDescription, @@ -7,11 +13,6 @@ import { setAccTitle, setDiagramTitle, } from '../common/commonDb.js'; -import * as configApi from '../../config.js'; -import defaultConfig from '../../defaultConfig.js'; -import { getThemeVariables } from '../../themes/theme-default.js'; -import { cleanAndMerge } from '../../utils.js'; -import { sanitizeText } from '../common/common.js'; import { XYChartBuilder } from './chartBuilder/index.js'; import type { DrawableElem, @@ -21,11 +22,10 @@ import type { XYChartThemeConfig, } from './chartBuilder/interfaces.js'; import { isBandAxisData, isLinearAxisData } from './chartBuilder/interfaces.js'; -import type { Group } from '../../diagram-api/types.js'; let plotIndex = 0; -let tmpSVGGroup: Group; +let tmpSVGGroup: SVGGroup; let xyChartConfig: XYChartConfig = getChartDefaultConfig(); let xyChartThemeConfig: XYChartThemeConfig = getChartDefaultThemeConfig(); @@ -75,7 +75,7 @@ function textSanitizer(text: string) { return sanitizeText(text.trim(), config); } -function setTmpSVGG(SVGG: Group) { +function setTmpSVGG(SVGG: SVGGroup) { tmpSVGGroup = SVGG; } function setOrientation(orientation: string) { diff --git a/packages/mermaid/src/docs/.vitepress/components/ProductHuntBadge.vue b/packages/mermaid/src/docs/.vitepress/components/ProductHuntBadge.vue new file mode 100644 index 000000000..17f0767d7 --- /dev/null +++ b/packages/mermaid/src/docs/.vitepress/components/ProductHuntBadge.vue @@ -0,0 +1,14 @@ + diff --git a/packages/mermaid/src/docs/.vitepress/components/TopBar.vue b/packages/mermaid/src/docs/.vitepress/components/TopBar.vue index 130d6babc..5aa515575 100644 --- a/packages/mermaid/src/docs/.vitepress/components/TopBar.vue +++ b/packages/mermaid/src/docs/.vitepress/components/TopBar.vue @@ -1,26 +1,65 @@ + + + + diff --git a/packages/mermaid/src/docs/.vitepress/theme/index.ts b/packages/mermaid/src/docs/.vitepress/theme/index.ts index 3ce3aea23..3ec200937 100644 --- a/packages/mermaid/src/docs/.vitepress/theme/index.ts +++ b/packages/mermaid/src/docs/.vitepress/theme/index.ts @@ -11,11 +11,11 @@ import HomePage from '../components/HomePage.vue'; import TopBar from '../components/TopBar.vue'; import { getRedirect } from './redirect.js'; // @ts-ignore Type not available -import { h } from 'vue'; -import Theme from 'vitepress/theme'; -import '../style/main.css'; import 'uno.css'; import type { EnhanceAppContext } from 'vitepress'; +import Theme from 'vitepress/theme'; +import { h } from 'vue'; +import '../style/main.css'; export default { ...DefaultTheme, @@ -24,6 +24,7 @@ export default { // Keeping this as comment as it took a lot of time to figure out how to add a component to the top bar. 'home-hero-before': () => h(TopBar), 'home-features-after': () => h(HomePage), + 'doc-before': () => h(TopBar), }); }, enhanceApp({ app, router }: EnhanceAppContext) { diff --git a/packages/mermaid/src/docs/ecosystem/integrations-community.md b/packages/mermaid/src/docs/ecosystem/integrations-community.md index d77a82b44..81b0386b1 100644 --- a/packages/mermaid/src/docs/ecosystem/integrations-community.md +++ b/packages/mermaid/src/docs/ecosystem/integrations-community.md @@ -51,8 +51,10 @@ To add an integration to this list, see the [Integrations - create page](./integ - [SVG diagram generator](https://github.com/SimonKenyonShepard/mermaidjs-github-svg-generator) - [GitLab](https://docs.gitlab.com/ee/user/markdown.html#diagrams-and-flowcharts) ✅ - [Mermaid Plugin for JetBrains IDEs](https://plugins.jetbrains.com/plugin/20146-mermaid) +- [MonsterWriter](https://www.monsterwriter.com/) ✅ - [Joplin](https://joplinapp.org) ✅ - [LiveBook](https://livebook.dev) ✅ +- [Slidev](https://sli.dev) ✅ - [Tuleap](https://docs.tuleap.org/user-guide/writing-in-tuleap.html#graphs) ✅ - [Mermaid Flow Visual Editor](https://www.mermaidflow.app) ✅ - [Mermerd](https://github.com/KarnerTh/mermerd) @@ -128,7 +130,7 @@ Communication tools and platforms ### Wikis - [DokuWiki](https://dokuwiki.org) - - [ComboStrap](https://combostrap.com/mermaid) + - [ComboStrap](https://combostrap.com/utility/create-diagram-with-mermaid-vh3ab9yj) - [Mermaid Plugin](https://www.dokuwiki.org/plugin:mermaid) - [Foswiki](https://foswiki.org) - [Mermaid Plugin](https://foswiki.org/Extensions/MermaidPlugin) diff --git a/packages/mermaid/src/docs/ecosystem/mermaid-chart.md b/packages/mermaid/src/docs/ecosystem/mermaid-chart.md index 732b9b68c..049df836e 100644 --- a/packages/mermaid/src/docs/ecosystem/mermaid-chart.md +++ b/packages/mermaid/src/docs/ecosystem/mermaid-chart.md @@ -6,7 +6,7 @@ Try the Ultimate AI, Mermaid, and Visual Diagramming Suite by creating an accoun
-Mermaid Chart - A smarter way to create diagrams | Product Hunt +Mermaid Chart - A smarter way to create diagrams | Product Hunt ## About diff --git a/packages/mermaid/src/docs/intro/syntax-reference.md b/packages/mermaid/src/docs/intro/syntax-reference.md index d4ee1067f..7d7fd5994 100644 --- a/packages/mermaid/src/docs/intro/syntax-reference.md +++ b/packages/mermaid/src/docs/intro/syntax-reference.md @@ -65,3 +65,110 @@ Allows for the limited reconfiguration of a diagram just before it is rendered. ### [Theme Manipulation](../config/theming.md) An application of using Directives to change [Themes](../config/theming.md). `Theme` is a value within Mermaid's configuration that dictates the color scheme for diagrams. + +### Layout and look + +We've restructured how Mermaid renders diagrams, enabling new features like selecting layout and look. **Currently, this is supported for flowcharts and state diagrams**, with plans to extend support to all diagram types. + +### Selecting Diagram Looks + +Mermaid offers a variety of styles or “looks” for your diagrams, allowing you to tailor the visual appearance to match your specific needs or preferences. Whether you prefer a hand-drawn or classic style, you can easily customize your diagrams. + +**Available Looks:** + + â€ĸ Hand-Drawn Look: For a more personal, creative touch, the hand-drawn look brings a sketch-like quality to your diagrams. This style is perfect for informal settings or when you want to add a bit of personality to your diagrams. + â€ĸ Classic Look: If you prefer the traditional Mermaid style, the classic look maintains the original appearance that many users are familiar with. It’s great for consistency across projects or when you want to keep the familiar aesthetic. + +**How to Select a Look:** + +You can select a look by adding the look parameter in the metadata section of your Mermaid diagram code. Here’s an example: + +```mermaid +--- +config: + look: handDrawn + theme: neutral +--- +flowchart LR + A[Start] --> B{Decision} + B -->|Yes| C[Continue] + B -->|No| D[Stop] +``` + +#### Selecting Layout Algorithms + +In addition to customizing the look of your diagrams, Mermaid Chart now allows you to choose different layout algorithms to better organize and present your diagrams, especially when dealing with more complex structures. The layout algorithm dictates how nodes and edges are arranged on the page. + +#### Supported Layout Algorithms: + + â€ĸ Dagre (default): This is the classic layout algorithm that has been used in Mermaid for a long time. It provides a good balance of simplicity and visual clarity, making it ideal for most diagrams. + â€ĸ ELK: For those who need more sophisticated layout capabilities, especially when working with large or intricate diagrams, the ELK (Eclipse Layout Kernel) layout offers advanced options. It provides a more optimized arrangement, potentially reducing overlapping and improving readability. This is not included out the box but needs to be added when integrating mermaid for sites/applications that want to have elk support. + +#### How to Select a Layout Algorithm: + +You can specify the layout algorithm directly in the metadata section of your Mermaid diagram code. Here’s an example: + +```mermaid +--- +config: + layout: elk + look: handDrawn + theme: dark +--- +flowchart TB + A[Start] --> B{Decision} + B -->|Yes| C[Continue] + B -->|No| D[Stop] +``` + +In this example, the `layout: elk` line configures the diagram to use the ELK layout algorithm, along with the hand drawn look and forest theme. + +#### Customizing ELK Layout: + +When using the ELK layout, you can further refine the diagram’s configuration, such as how nodes are placed and whether parallel edges should be combined: + +- To combine parallel edges, use mergeEdges: true | false. +- To configure node placement, use nodePlacementStrategy with the following options: + - SIMPLE + - NETWORK_SIMPLEX + - LINEAR_SEGMENTS + - BRANDES_KOEPF (default) + +**Example configuration:** + +``` +--- +config: + layout: elk + elk: + mergeEdges: true + nodePlacementStrategy: LINEAR_SEGMENTS +--- +flowchart LR + A[Start] --> B{Choose Path} + B -->|Option 1| C[Path 1] + B -->|Option 2| D[Path 2] + +#### Using Dagre Layout with Classic Look: +``` + +Another example: + +``` +--- +config: + layout: dagre + look: classic + theme: default +--- + +flowchart LR +A[Start] --> B{Choose Path} +B -->|Option 1| C[Path 1] +B -->|Option 2| D[Path 2] + +``` + +These options give you the flexibility to create diagrams that not only look great but are also arranged to best suit your data’s structure and flow. + +When integrating Mermaid, you can include look and layout configuration with the initialize call. This is also where you add the loading of elk. diff --git a/packages/mermaid/src/docs/news/blog.md b/packages/mermaid/src/docs/news/blog.md index 4ada1e05c..f7f28bf4b 100644 --- a/packages/mermaid/src/docs/news/blog.md +++ b/packages/mermaid/src/docs/news/blog.md @@ -1,5 +1,47 @@ # Blog +## [Mermaid v11 is out!](https://www.mermaidchart.com/blog/posts/mermaid-v11/) + +23 August 2024 ¡ 2 mins + +Mermaid v11 introduces advanced layout options, new diagram types, and enhanced customization features, thanks to the incredible contributions from our community. + +## [Mermaid Innovation - Introducing New Looks for Mermaid Diagrams](https://www.mermaidchart.com/blog/posts/mermaid-innovation-introducing-new-looks-for-mermaid-diagrams/) + +6 August 2024 ¡3 mins + +Discover the fresh new and unique Neo and Hand-Drawn looks for Mermaid Diagrams, while still offering the classic look you love. + +## [The Mermaid Chart Plugin for Jira: A How-To User Guide](https://www.mermaidchart.com/blog/posts/the-mermaid-chart-plugin-for-jira-a-how-to-user-guide/) + +31 July 2024 ¡ 5 mins + +The Mermaid Chart plugin for Jira has arrived! + +## [Mermaid AI Is Here to Change the Game For Diagram Creation](https://www.mermaidchart.com/blog/posts/mermaid-ai-is-here-to-change-the-game-for-diagram-creation/) + +22 July 2024 ¡ 5 mins + +The Mermaid AI chat interface + +## [How to Make a Sequence Diagram with Mermaid Chart](https://www.mermaidchart.com/blog/posts/how-to-make-a-sequence-diagram-in-mermaid-chart-step-by-step-guide/) + +8 July 2024 ¡ 6 mins + +Sequence diagrams are important for communicating complex systems in a clear and concise manner. + +## [How to Use the New “Comments” Feature in Mermaid Chart](https://www.mermaidchart.com/blog/posts/how-to-use-the-new-comments-feature-in-mermaid-chart/) + +2 July 2024 ¡ 3 mins + +How to Use the New Comments Feature in Mermaid Chart + +## [How to Use the official Mermaid Chart for Confluence app](https://www.mermaidchart.com/blog/posts/how-to-use-the-official-mermaid-chart-for-confluence-app/) + +21 May 2024 ¡ 4 mins + +It doesn’t matter if you’re a data enthusiast, software engineer, or visual storyteller; our Confluence app can allow you to embed Mermaid Chart diagrams — and dynamically edit them — within your Confluence pages. + ## [How to Choose the Right Documentation Software](https://www.mermaidchart.com/blog/posts/how-to-choose-the-right-documentation-software/) 7 May 2024 ¡ 5 mins diff --git a/packages/mermaid/src/docs/package.json b/packages/mermaid/src/docs/package.json index 146c4d2d8..7cfb8ab6c 100644 --- a/packages/mermaid/src/docs/package.json +++ b/packages/mermaid/src/docs/package.json @@ -1,6 +1,7 @@ { - "name": "docs", + "name": "@mermaid-js/docs", "private": true, + "version": "0.0.1", "type": "module", "scripts": { "dev": "vitepress --port 3333 --open", diff --git a/packages/mermaid/src/docs/syntax/gantt.md b/packages/mermaid/src/docs/syntax/gantt.md index 8497b96a1..01a9f041d 100644 --- a/packages/mermaid/src/docs/syntax/gantt.md +++ b/packages/mermaid/src/docs/syntax/gantt.md @@ -114,7 +114,7 @@ The `title` is an _optional_ string to be displayed at the top of the Gantt char The `excludes` is an _optional_ attribute that accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays". These date will be marked on the graph, and be excluded from the duration calculation of tasks. Meaning that if there are excluded dates during a task interval, the number of 'skipped' days will be added to the end of the task to ensure the duration is as specified in the code. -#### Weekend (v\+) +#### Weekend (v\11.0.0+) When excluding weekends, it is possible to configure the weekends to be either Friday and Saturday or Saturday and Sunday. By default weekends are Saturday and Sunday. To define the weekend start day, there is an _optional_ attribute `weekend` that can be added in a new line followed by either `friday` or `saturday`. diff --git a/packages/mermaid/src/docs/syntax/gitgraph.md b/packages/mermaid/src/docs/syntax/gitgraph.md index d0791718b..2b3f1a88b 100644 --- a/packages/mermaid/src/docs/syntax/gitgraph.md +++ b/packages/mermaid/src/docs/syntax/gitgraph.md @@ -571,7 +571,7 @@ Usage example: commit ``` -### Bottom to Top (`BT:`) (v+) +### Bottom to Top (`BT:`) (v11.0.0+) In `BT` (**Bottom-to-Top**) orientation, the commits run from bottom to top of the graph and branches are arranged side-by-side. diff --git a/packages/mermaid/src/docs/syntax/packet.md b/packages/mermaid/src/docs/syntax/packet.md index 52a0de887..c7b6cb71b 100644 --- a/packages/mermaid/src/docs/syntax/packet.md +++ b/packages/mermaid/src/docs/syntax/packet.md @@ -1,4 +1,4 @@ -# Packet Diagram (v+) +# Packet Diagram (v11.0.0+) ## Introduction diff --git a/packages/mermaid/src/docs/syntax/sequenceDiagram.md b/packages/mermaid/src/docs/syntax/sequenceDiagram.md index 249f7bde0..8826f6275 100644 --- a/packages/mermaid/src/docs/syntax/sequenceDiagram.md +++ b/packages/mermaid/src/docs/syntax/sequenceDiagram.md @@ -143,18 +143,18 @@ Messages can be of two displayed either solid or with a dotted line. There are ten types of arrows currently supported: -| Type | Description | -| -------- | ----------------------------------------------------------------------- | -| `->` | Solid line without arrow | -| `-->` | Dotted line without arrow | -| `->>` | Solid line with arrowhead | -| `-->>` | Dotted line with arrowhead | -| `<<->>` | Solid line with bidirectional arrowheads (v+) | -| `<<-->>` | Dotted line with bidirectional arrowheads (v+) | -| `-x` | Solid line with a cross at the end | -| `--x` | Dotted line with a cross at the end. | -| `-)` | Solid line with an open arrow at the end (async) | -| `--)` | Dotted line with a open arrow at the end (async) | +| Type | Description | +| -------- | ---------------------------------------------------- | +| `->` | Solid line without arrow | +| `-->` | Dotted line without arrow | +| `->>` | Solid line with arrowhead | +| `-->>` | Dotted line with arrowhead | +| `<<->>` | Solid line with bidirectional arrowheads (v11.0.0+) | +| `<<-->>` | Dotted line with bidirectional arrowheads (v11.0.0+) | +| `-x` | Solid line with a cross at the end | +| `--x` | Dotted line with a cross at the end. | +| `-)` | Solid line with an open arrow at the end (async) | +| `--)` | Dotted line with a open arrow at the end (async) | ## Activations diff --git a/packages/mermaid/src/internals.ts b/packages/mermaid/src/internals.ts index cebcd4ace..7cc058cb3 100644 --- a/packages/mermaid/src/internals.ts +++ b/packages/mermaid/src/internals.ts @@ -29,3 +29,5 @@ export const internalHelpers = { log, positionEdgeLabel, }; + +export type InternalHelpers = typeof internalHelpers; diff --git a/packages/mermaid/src/mermaid.ts b/packages/mermaid/src/mermaid.ts index aa0737592..43fc5bd31 100644 --- a/packages/mermaid/src/mermaid.ts +++ b/packages/mermaid/src/mermaid.ts @@ -4,34 +4,37 @@ */ import { dedent } from 'ts-dedent'; import type { MermaidConfig } from './config.type.js'; -import { log } from './logger.js'; -import utils from './utils.js'; -import type { ParseOptions, ParseResult, RenderResult } from './types.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 type { ExternalDiagramDefinition } from './diagram-api/types.js'; -import type { UnknownDiagramError } from './errors.js'; +import { detectType, registerLazyLoadedDiagrams } from './diagram-api/detectType.js'; import { addDiagrams } from './diagram-api/diagram-orchestration.js'; +import { loadRegisteredDiagrams } from './diagram-api/loadDiagram.js'; +import type { ExternalDiagramDefinition, SVG, SVGGroup } from './diagram-api/types.js'; +import type { ParseErrorFunction } from './Diagram.js'; +import type { UnknownDiagramError } from './errors.js'; +import type { InternalHelpers } from './internals.js'; +import { log } from './logger.js'; +import { mermaidAPI } from './mermaidAPI.js'; +import type { LayoutLoaderDefinition, RenderOptions } from './rendering-util/render.js'; import { registerLayoutLoaders } from './rendering-util/render.js'; -import type { LayoutLoaderDefinition } from './rendering-util/render.js'; -import { internalHelpers } from './internals.js'; import type { LayoutData } from './rendering-util/types.js'; +import type { ParseOptions, ParseResult, RenderResult } from './types.js'; +import type { DetailedError } from './utils.js'; +import utils, { isDetailedError } from './utils.js'; export type { - MermaidConfig, DetailedError, ExternalDiagramDefinition, + InternalHelpers, + LayoutData, + LayoutLoaderDefinition, + MermaidConfig, ParseErrorFunction, - RenderResult, ParseOptions, ParseResult, + RenderOptions, + RenderResult, + SVG, + SVGGroup, UnknownDiagramError, - LayoutLoaderDefinition, - LayoutData, }; export interface RunOptions { @@ -432,11 +435,6 @@ export interface Mermaid { contentLoaded: typeof contentLoaded; setParseErrorHandler: typeof setParseErrorHandler; detectType: typeof detectType; - /** - * Internal helpers for mermaid - * @deprecated - This should not be used by external packages, as the definitions will change without notice. - */ - internalHelpers: typeof internalHelpers; } const mermaid: Mermaid = { @@ -453,7 +451,6 @@ const mermaid: Mermaid = { contentLoaded, setParseErrorHandler, detectType, - internalHelpers, }; export default mermaid; diff --git a/packages/mermaid/src/mermaidAPI.ts b/packages/mermaid/src/mermaidAPI.ts index e336f1036..e1c4412b9 100644 --- a/packages/mermaid/src/mermaidAPI.ts +++ b/packages/mermaid/src/mermaidAPI.ts @@ -25,6 +25,7 @@ import { preprocessDiagram } from './preprocess.js'; import { decodeEntities } from './utils.js'; import { toBase64 } from './utils/base64.js'; import type { D3Element, ParseOptions, ParseResult, RenderResult } from './types.js'; +import assignWithDepth from './assignWithDepth.js'; const MAX_TEXTLENGTH = 50_000; const MAX_TEXTLENGTH_EXCEEDED_MSG = @@ -473,9 +474,10 @@ const render = async function ( }; /** - * @param options - Initial Mermaid options + * @param userOptions - Initial Mermaid options */ -function initialize(options: MermaidConfig = {}) { +function initialize(userOptions: MermaidConfig = {}) { + const options = assignWithDepth({}, userOptions); // Handle legacy location of font-family configuration if (options?.fontFamily && !options.themeVariables?.fontFamily) { if (!options.themeVariables) { diff --git a/packages/mermaid/src/rendering-util/createText.ts b/packages/mermaid/src/rendering-util/createText.ts index 12d81715f..a6ad7fa1c 100644 --- a/packages/mermaid/src/rendering-util/createText.ts +++ b/packages/mermaid/src/rendering-util/createText.ts @@ -1,16 +1,16 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ // @ts-nocheck TODO: Fix types -import type { MermaidConfig } from '../config.type.js'; -import type { Group } from '../diagram-api/types.js'; +import { getConfig } from '$root/diagram-api/diagramAPI.js'; +import common, { hasKatex, renderKatex } from '$root/diagrams/common/common.js'; import { select } from 'd3'; +import type { MermaidConfig } from '../config.type.js'; +import type { SVGGroup } from '../diagram-api/types.js'; import type { D3TSpanElement, D3TextElement } from '../diagrams/common/commonTypes.js'; import { log } from '../logger.js'; import { markdownToHTML, markdownToLines } from '../rendering-util/handle-markdown-text.js'; import { decodeEntities } from '../utils.js'; import { splitLineToFitWidth } from './splitText.js'; import type { MarkdownLine, MarkdownWord } from './types.js'; -import common, { hasKatex, renderKatex } from '$root/diagrams/common/common.js'; -import { getConfig } from '$root/diagram-api/diagramAPI.js'; function applyStyle(dom, styleFn) { if (styleFn) { @@ -36,6 +36,7 @@ async function addHtmlSpan(element, node, width, classes, addBackground = false) div.style('white-space', 'nowrap'); div.style('line-height', '1.5'); div.style('max-width', width + 'px'); + div.style('text-align', 'center'); div.attr('xmlns', 'http://www.w3.org/1999/xhtml'); if (addBackground) { div.attr('class', 'labelBkg'); @@ -82,7 +83,7 @@ function computeWidthOfText(parentNode: any, lineHeight: number, line: MarkdownL } export function computeDimensionOfText( - parentNode: Group, + parentNode: SVGGroup, lineHeight: number, text: string ): DOMRect | undefined { diff --git a/packages/mermaid/src/rendering-util/insertElementsForSize.js b/packages/mermaid/src/rendering-util/insertElementsForSize.js index ff0b30ac6..162551058 100644 --- a/packages/mermaid/src/rendering-util/insertElementsForSize.js +++ b/packages/mermaid/src/rendering-util/insertElementsForSize.js @@ -1,7 +1,7 @@ import { select } from 'd3'; import { insertNode } from '../dagre-wrapper/nodes.js'; -export const getDiagramElements = (id, securityLevel) => { +export const getDiagramElement = (id, securityLevel) => { let sandboxElement; if (securityLevel === 'sandbox') { sandboxElement = select('#i' + id); @@ -15,9 +15,7 @@ export const getDiagramElements = (id, securityLevel) => { // Run the renderer. This is what draws the final graph. - // @ts-ignore todo: fix this - const element = root.select('#' + id + ' g'); - return { svg, element }; + return svg; }; export function insertElementsForSize(el, data) { diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js b/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js index 8851a1d95..2717eb717 100644 --- a/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js +++ b/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js @@ -268,7 +268,7 @@ const recursiveRender = async (_elem, graph, diagramType, id, parentCluster, sit return { elem, diff }; }; -export const render = async (data4Layout, svg, element) => { +export const render = async (data4Layout, svg) => { const graph = new graphlib.Graph({ multigraph: true, compound: true, @@ -289,7 +289,7 @@ export const render = async (data4Layout, svg, element) => { .setDefaultEdgeLabel(function () { return {}; }); - + const element = svg.select('g'); insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId); clearNodes(); clearEdges(); diff --git a/packages/mermaid/src/rendering-util/render.ts b/packages/mermaid/src/rendering-util/render.ts index 442780c75..013be7ba4 100644 --- a/packages/mermaid/src/rendering-util/render.ts +++ b/packages/mermaid/src/rendering-util/render.ts @@ -1,5 +1,20 @@ +import type { SVG } from '$root/diagram-api/types.js'; +import type { InternalHelpers } from '$root/internals.js'; +import { internalHelpers } from '$root/internals.js'; +import { log } from '$root/logger.js'; +import type { LayoutData } from './types.js'; + +export interface RenderOptions { + algorithm?: string; +} + export interface LayoutAlgorithm { - render(data4Layout: any, svg: any, element: any, algorithm?: string): any; + render( + layoutData: LayoutData, + svg: SVG, + helpers: InternalHelpers, + options?: RenderOptions + ): Promise; } export type LayoutLoader = () => Promise; @@ -24,21 +39,33 @@ const registerDefaultLayoutLoaders = () => { name: 'dagre', loader: async () => await import('./layout-algorithms/dagre/index.js'), }, - // { - // name: 'elk', - // loader: async () => await import('../../../mermaid-layout-elk/src/render.js'), - // }, ]); }; registerDefaultLayoutLoaders(); -export const render = async (data4Layout: any, svg: any, element: any) => { +export const render = async (data4Layout: LayoutData, svg: SVG) => { if (!(data4Layout.layoutAlgorithm in layoutAlgorithms)) { throw new Error(`Unknown layout algorithm: ${data4Layout.layoutAlgorithm}`); } const layoutDefinition = layoutAlgorithms[data4Layout.layoutAlgorithm]; const layoutRenderer = await layoutDefinition.loader(); - return layoutRenderer.render(data4Layout, svg, element, layoutDefinition.algorithm); + return layoutRenderer.render(data4Layout, svg, internalHelpers, { + algorithm: layoutDefinition.algorithm, + }); +}; + +/** + * Get the registered layout algorithm. If the algorithm is not registered, use the fallback algorithm. + */ +export const getRegisteredLayoutAlgorithm = (algorithm = '', { fallback = 'dagre' } = {}) => { + if (algorithm in layoutAlgorithms) { + return algorithm; + } + if (fallback in layoutAlgorithms) { + log.warn(`Layout algorithm ${algorithm} is not registered. Using ${fallback} as fallback.`); + return fallback; + } + throw new Error(`Both layout algorithms ${algorithm} and ${fallback} are not registered.`); }; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/clusters.js b/packages/mermaid/src/rendering-util/rendering-elements/clusters.js index b85809817..ba87f78f5 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/clusters.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/clusters.js @@ -19,7 +19,7 @@ const rect = async (parent, node) => { const { themeVariables, handDrawnSeed } = siteConfig; const { clusterBkg, clusterBorder } = themeVariables; - const { labelStyles, nodeStyles } = styles2String(node); + const { labelStyles, nodeStyles, borderStyles, backgroundStyles } = styles2String(node); // Add outer g element const shapeSvg = parent @@ -79,6 +79,9 @@ const rect = async (parent, node) => { log.debug('Rough node insert CXC', roughNode); return roughNode; }, ':first-child'); + // Should we affect the options instead of doing this? + rect.select('path:nth-child(2)').attr('style', borderStyles.join(';')); + rect.select('path').attr('style', backgroundStyles.join(';').replace('fill', 'stroke')); } else { // add the rect rect = shapeSvg.insert('rect', ':first-child'); @@ -357,14 +360,15 @@ const shapes = { divider, }; -let clusterElems = {}; +let clusterElems = new Map(); -export const insertCluster = (elem, node) => { +export const insertCluster = async (elem, node) => { const shape = node.shape || 'rect'; - const cluster = shapes[shape](elem, node); - clusterElems[node.id] = cluster; + const cluster = await shapes[shape](elem, node); + clusterElems.set(node.id, cluster); return cluster; }; + export const getClusterTitleWidth = (elem, node) => { const label = createLabel(node.label, node.labelStyle, undefined, true); elem.node().appendChild(label); @@ -374,7 +378,7 @@ export const getClusterTitleWidth = (elem, node) => { }; export const clear = () => { - clusterElems = {}; + clusterElems = new Map(); }; export const positionCluster = (node) => { @@ -390,8 +394,8 @@ export const positionCluster = (node) => { ', ' + node?.height + ')', - clusterElems[node.id] + clusterElems.get(node.id) ); - const el = clusterElems[node.id]; + const el = clusterElems.get(node.id); el.cluster.attr('transform', 'translate(' + node.x + ', ' + node.y + ')'); }; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/edges.js b/packages/mermaid/src/rendering-util/rendering-elements/edges.js index 6d0c157dd..087fcf0be 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/edges.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/edges.js @@ -538,6 +538,27 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod .attr('style', edgeStyles ? edgeStyles.reduce((acc, style) => acc + ';' + style, '') : ''); } + // DEBUG code, DO NOT REMOVE + // adds a red circle at each edge coordinate + // cornerPoints.forEach((point) => { + // elem + // .append('circle') + // .style('stroke', 'blue') + // .style('fill', 'blue') + // .attr('r', 3) + // .attr('cx', point.x) + // .attr('cy', point.y); + // }); + // lineData.forEach((point) => { + // elem + // .append('circle') + // .style('stroke', 'blue') + // .style('fill', 'blue') + // .attr('r', 3) + // .attr('cx', point.x) + // .attr('cy', point.y); + // }); + let url = ''; if (getConfig().flowchart.arrowMarkerAbsolute || getConfig().state.arrowMarkerAbsolute) { url = diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/cylinder.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/cylinder.ts index 9d7e5445b..f85db0f05 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/cylinder.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/cylinder.ts @@ -103,7 +103,7 @@ export const cylinder = async (parent: SVGAElement, node: Node) => { Math.abs(pos.y - (node.y ?? 0)) > (node.height ?? 0) / 2 - ry)) ) { let y = ry * ry * (1 - (x * x) / (rx * rx)); - if (y != 0) { + if (y > 0) { y = Math.sqrt(y); } y = ry - y; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.ts index 11773f543..a5c963e7c 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.ts @@ -37,6 +37,8 @@ export const styles2String = (node: Node) => { const { stylesArray } = compileStyles(node); const labelStyles: string[] = []; const nodeStyles: string[] = []; + const borderStyles: string[] = []; + const backgroundStyles: string[] = []; stylesArray.forEach((style) => { const key = style[0]; @@ -63,10 +65,22 @@ export const styles2String = (node: Node) => { labelStyles.push(style.join(':') + ' !important'); } else { nodeStyles.push(style.join(':') + ' !important'); + if (key.includes('stroke')) { + borderStyles.push(style.join(':') + ' !important'); + } + if (key === 'fill') { + backgroundStyles.push(style.join(':') + ' !important'); + } } }); - return { labelStyles: labelStyles.join(';'), nodeStyles: nodeStyles.join(';') }; + return { + labelStyles: labelStyles.join(';'), + nodeStyles: nodeStyles.join(';'), + stylesArray, + borderStyles, + backgroundStyles, + }; }; // Striped fill like start or fork nodes in state diagrams diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/question.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/question.ts index f6f3f3049..ba770ab4e 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/question.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/question.ts @@ -63,7 +63,14 @@ export const question = async (parent: SVGAElement, node: Node): Promise { ) ); - if (evaluate(getConfig()?.flowchart?.htmlLabels)) { - const div = descr.children[0]; - const dv = select(descr); - bbox = div.getBoundingClientRect(); - dv.attr('width', bbox.width); - dv.attr('height', bbox.height); - } + //if (evaluate(getConfig()?.flowchart?.htmlLabels)) { + const div = descr.children[0]; + const dv = select(descr); + bbox = div.getBoundingClientRect(); + dv.attr('width', bbox.width); + dv.attr('height', bbox.height); + // } const halfPadding = (node.padding || 0) / 2; select(descr).attr( diff --git a/packages/mermaid/src/rendering-util/setupViewPortForSVG.ts b/packages/mermaid/src/rendering-util/setupViewPortForSVG.ts index 1fa2de1fd..e21f3304b 100644 --- a/packages/mermaid/src/rendering-util/setupViewPortForSVG.ts +++ b/packages/mermaid/src/rendering-util/setupViewPortForSVG.ts @@ -22,7 +22,7 @@ export const setupViewPortForSVG = ( svg.attr('viewBox', viewBox); // Log the viewBox configuration for debugging - log.debug(`viewBox configured: ${viewBox}`); + log.debug(`viewBox configured: ${viewBox} with padding: ${padding}`); }; const calculateDimensionsWithPadding = (svg: SVG, padding: number) => { diff --git a/packages/mermaid/src/schemas/config.schema.yaml b/packages/mermaid/src/schemas/config.schema.yaml index 29192d85e..11c294ed9 100644 --- a/packages/mermaid/src/schemas/config.schema.yaml +++ b/packages/mermaid/src/schemas/config.schema.yaml @@ -118,7 +118,7 @@ properties: - NETWORK_SIMPLEX - LINEAR_SEGMENTS - BRANDES_KOEPF - default: SIMPLE + default: BRANDES_KOEPF darkMode: type: boolean default: false diff --git a/packages/mermaid/src/styles.ts b/packages/mermaid/src/styles.ts index d0eead51d..78b514c40 100644 --- a/packages/mermaid/src/styles.ts +++ b/packages/mermaid/src/styles.ts @@ -70,6 +70,9 @@ const getStyles = ( font-family: ${options.fontFamily}; font-size: ${options.fontSize}; } + & p { + margin: 0 + } ${diagramStyles} diff --git a/packages/mermaid/tsconfig.json b/packages/mermaid/tsconfig.json index bb3a8106b..0f06a1731 100644 --- a/packages/mermaid/tsconfig.json +++ b/packages/mermaid/tsconfig.json @@ -9,5 +9,10 @@ "$root/*": ["src/*"] } }, - "include": ["./src/**/*.ts", "./package.json"] + "include": [ + "./src/**/*.ts", + "./package.json", + "src/diagrams/gantt/ganttDb.js", + "src/diagrams/git/gitGraphRenderer.js" + ] } diff --git a/packages/parser/CHANGELOG.md b/packages/parser/CHANGELOG.md new file mode 100644 index 000000000..4b864f523 --- /dev/null +++ b/packages/parser/CHANGELOG.md @@ -0,0 +1,19 @@ +# @mermaid-js/parser + +## 0.2.0 + +### Minor Changes + +- [#5664](https://github.com/mermaid-js/mermaid/pull/5664) [`5deaef4`](https://github.com/mermaid-js/mermaid/commit/5deaef456e74d796866431c26f69360e4e74dbff) Thanks [@Austin-Fulbright](https://github.com/Austin-Fulbright)! - chore: Migrate git graph to langium, use typescript for internals + +## 0.1.1 + +### Patch Changes + +- [#5746](https://github.com/mermaid-js/mermaid/pull/5746) [`83926c9`](https://github.com/mermaid-js/mermaid/commit/83926c9707b09c34e300888186250191ee8ae30a) Thanks [@sidharthv96](https://github.com/sidharthv96)! - test changeset + +## 0.1.0 + +### Minor Changes + +- [#5744](https://github.com/mermaid-js/mermaid/pull/5744) [`5013484`](https://github.com/mermaid-js/mermaid/commit/50134849246141ec400e33e08c12c10539b84de9) Thanks [@sidharthv96](https://github.com/sidharthv96)! - Release parser, test changesets diff --git a/packages/parser/langium-config.json b/packages/parser/langium-config.json index c750f049d..af8a4cfe6 100644 --- a/packages/parser/langium-config.json +++ b/packages/parser/langium-config.json @@ -15,6 +15,11 @@ "id": "pie", "grammar": "src/language/pie/pie.langium", "fileExtensions": [".mmd", ".mermaid"] + }, + { + "id": "gitGraph", + "grammar": "src/language/gitGraph/gitGraph.langium", + "fileExtensions": [".mmd", ".mermaid"] } ], "mode": "production", diff --git a/packages/parser/package.json b/packages/parser/package.json index 17c4afd31..fc70e844b 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -1,6 +1,6 @@ { "name": "@mermaid-js/parser", - "version": "0.1.0-rc.2", + "version": "0.2.0", "description": "MermaidJS parser", "author": "Yokozuna59", "contributors": [ @@ -19,8 +19,7 @@ "scripts": { "clean": "rimraf dist src/language/generated", "langium:generate": "langium generate", - "langium:watch": "langium generate --watch", - "prepublishOnly": "pnpm -w run build" + "langium:watch": "langium generate --watch" }, "repository": { "type": "git", diff --git a/packages/parser/src/language/gitGraph/gitGraph.langium b/packages/parser/src/language/gitGraph/gitGraph.langium new file mode 100644 index 000000000..1571ebba8 --- /dev/null +++ b/packages/parser/src/language/gitGraph/gitGraph.langium @@ -0,0 +1,87 @@ +grammar GitGraph + +interface Common { + accDescr?: string; + accTitle?: string; + title?: string; +} + +fragment TitleAndAccessibilities: + ((accDescr=ACC_DESCR | accTitle=ACC_TITLE | title=TITLE) EOL)+ +; + +fragment EOL returns string: + NEWLINE+ | EOF +; + +terminal NEWLINE: /\r?\n/; +terminal ACC_DESCR: /[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/; +terminal ACC_TITLE: /[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/; +terminal TITLE: /[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/; + +hidden terminal WHITESPACE: /[\t ]+/; +hidden terminal YAML: /---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/; +hidden terminal DIRECTIVE: /[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/; +hidden terminal SINGLE_LINE_COMMENT: /[\t ]*%%[^\n\r]*/; + +entry GitGraph: + NEWLINE* + ('gitGraph' | 'gitGraph' ':' | 'gitGraph:' | ('gitGraph' Direction ':')) + NEWLINE* + ( + NEWLINE* + (TitleAndAccessibilities | + statements+=Statement | + NEWLINE)* + ) +; + +Statement +: Commit +| Branch +| Merge +| Checkout +| CherryPicking +; + +Direction: + dir=('LR' | 'TB' | 'BT'); + +Commit: + 'commit' + ( + 'id:' id=STRING + |'msg:'? message=STRING + |'tag:' tags+=STRING + |'type:' type=('NORMAL' | 'REVERSE' | 'HIGHLIGHT') + )* EOL; +Branch: + 'branch' name=(ID|STRING) + ('order:' order=INT)? + EOL; + +Merge: + 'merge' branch=(ID|STRING) + ( + 'id:' id=STRING + |'tag:' tags+=STRING + |'type:' type=('NORMAL' | 'REVERSE' | 'HIGHLIGHT') + )* EOL; + +Checkout: + ('checkout'|'switch') branch=(ID|STRING) EOL; + +CherryPicking: + 'cherry-pick' + ( + 'id:' id=STRING + |'tag:' tags+=STRING + |'parent:' parent=STRING + )* EOL; + + + +terminal INT returns number: /[0-9]+(?=\s)/; +terminal ID returns string: /\w([-\./\w]*[-\w])?/; +terminal STRING: /"[^"]*"|'[^']*'/; + diff --git a/packages/parser/src/language/gitGraph/index.ts b/packages/parser/src/language/gitGraph/index.ts new file mode 100644 index 000000000..fd3c604b0 --- /dev/null +++ b/packages/parser/src/language/gitGraph/index.ts @@ -0,0 +1 @@ +export * from './module.js'; diff --git a/packages/parser/src/language/gitGraph/module.ts b/packages/parser/src/language/gitGraph/module.ts new file mode 100644 index 000000000..e2d45c8fa --- /dev/null +++ b/packages/parser/src/language/gitGraph/module.ts @@ -0,0 +1,52 @@ +import type { + DefaultSharedCoreModuleContext, + LangiumCoreServices, + LangiumSharedCoreServices, + Module, + PartialLangiumCoreServices, +} from 'langium'; +import { + inject, + createDefaultCoreModule, + createDefaultSharedCoreModule, + EmptyFileSystem, +} from 'langium'; +import { CommonValueConverter } from '../common/valueConverter.js'; +import { MermaidGeneratedSharedModule, GitGraphGeneratedModule } from '../generated/module.js'; +import { GitGraphTokenBuilder } from './tokenBuilder.js'; + +interface GitGraphAddedServices { + parser: { + TokenBuilder: GitGraphTokenBuilder; + ValueConverter: CommonValueConverter; + }; +} + +export type GitGraphServices = LangiumCoreServices & GitGraphAddedServices; + +export const GitGraphModule: Module< + GitGraphServices, + PartialLangiumCoreServices & GitGraphAddedServices +> = { + parser: { + TokenBuilder: () => new GitGraphTokenBuilder(), + ValueConverter: () => new CommonValueConverter(), + }, +}; + +export function createGitGraphServices(context: DefaultSharedCoreModuleContext = EmptyFileSystem): { + shared: LangiumSharedCoreServices; + GitGraph: GitGraphServices; +} { + const shared: LangiumSharedCoreServices = inject( + createDefaultSharedCoreModule(context), + MermaidGeneratedSharedModule + ); + const GitGraph: GitGraphServices = inject( + createDefaultCoreModule({ shared }), + GitGraphGeneratedModule, + GitGraphModule + ); + shared.ServiceRegistry.register(GitGraph); + return { shared, GitGraph }; +} diff --git a/packages/parser/src/language/gitGraph/tokenBuilder.ts b/packages/parser/src/language/gitGraph/tokenBuilder.ts new file mode 100644 index 000000000..ccadf1a1f --- /dev/null +++ b/packages/parser/src/language/gitGraph/tokenBuilder.ts @@ -0,0 +1,7 @@ +import { AbstractMermaidTokenBuilder } from '../common/index.js'; + +export class GitGraphTokenBuilder extends AbstractMermaidTokenBuilder { + public constructor() { + super(['gitGraph']); + } +} diff --git a/packages/parser/src/language/index.ts b/packages/parser/src/language/index.ts index 9f1d92ba8..8e8dbce4f 100644 --- a/packages/parser/src/language/index.ts +++ b/packages/parser/src/language/index.ts @@ -5,20 +5,31 @@ export { PacketBlock, Pie, PieSection, + GitGraph, + Branch, + Commit, + Merge, + Statement, isCommon, isInfo, isPacket, isPacketBlock, isPie, isPieSection, + isGitGraph, + isBranch, + isCommit, + isMerge, } from './generated/ast.js'; export { InfoGeneratedModule, MermaidGeneratedSharedModule, PacketGeneratedModule, PieGeneratedModule, + GitGraphGeneratedModule, } from './generated/module.js'; +export * from './gitGraph/index.js'; export * from './common/index.js'; export * from './info/index.js'; export * from './packet/index.js'; diff --git a/packages/parser/src/parse.ts b/packages/parser/src/parse.ts index 992b96506..233faed00 100644 --- a/packages/parser/src/parse.ts +++ b/packages/parser/src/parse.ts @@ -1,8 +1,8 @@ import type { LangiumParser, ParseResult } from 'langium'; -import type { Info, Packet, Pie } from './index.js'; +import type { Info, Packet, Pie, GitGraph } from './index.js'; -export type DiagramAST = Info | Packet | Pie; +export type DiagramAST = Info | Packet | Pie | GitGraph; const parsers: Record = {}; const initializers = { @@ -21,11 +21,18 @@ const initializers = { const parser = createPieServices().Pie.parser.LangiumParser; parsers.pie = parser; }, + gitGraph: async () => { + const { createGitGraphServices } = await import('./language/gitGraph/index.js'); + const parser = createGitGraphServices().GitGraph.parser.LangiumParser; + parsers.gitGraph = parser; + }, } as const; export async function parse(diagramType: 'info', text: string): Promise; export async function parse(diagramType: 'packet', text: string): Promise; export async function parse(diagramType: 'pie', text: string): Promise; +export async function parse(diagramType: 'gitGraph', text: string): Promise; + export async function parse( diagramType: keyof typeof initializers, text: string diff --git a/packages/parser/tests/gitGraph.test.ts b/packages/parser/tests/gitGraph.test.ts new file mode 100644 index 000000000..2d7c21bbe --- /dev/null +++ b/packages/parser/tests/gitGraph.test.ts @@ -0,0 +1,207 @@ +import { describe, expect, it } from 'vitest'; +import type { Branch, Merge } from '../src/language/index.js'; +import { gitGraphParse as parse } from './test-util.js'; +import type { Commit } from '../src/language/index.js'; +import type { Checkout, CherryPicking } from '../src/language/generated/ast.js'; + +describe('Parsing Commit Statements', () => { + it('should parse a simple commit', () => { + const result = parse(`gitGraph\n commit\n`); + expect(result.value.statements[0].$type).toBe('Commit'); + }); + + it('should parse multiple commits', () => { + const result = parse(`gitGraph\n commit\n commit\n commit\n`); + expect(result.value.statements).toHaveLength(3); + }); + + it('should parse commits with all properties', () => { + const result = parse(`gitGraph\n commit id:"1" msg:"Fix bug" tag:"v1.2" type:NORMAL\n`); + const commit = result.value.statements[0] as Commit; + expect(commit.$type).toBe('Commit'); + expect(commit.id).toBe('1'); + expect(commit.message).toBe('Fix bug'); + expect(commit.tags).toEqual(['v1.2']); + expect(commit.type).toBe('NORMAL'); + }); + + it('should handle commit messages with special characters', () => { + const result = parse(`gitGraph\n commit msg:"Fix issue #123: Handle errors"\n`); + const commit = result.value.statements[0] as Commit; + expect(commit.message).toBe('Fix issue #123: Handle errors'); + }); + + it('should parse commits with only a message and no other properties', () => { + const result = parse(`gitGraph\n commit msg:"Initial release"\n`); + const commit = result.value.statements[0] as Commit; + expect(commit.message).toBe('Initial release'); + expect(commit.id).toBeUndefined(); + expect(commit.type).toBeUndefined(); + }); + + it('should ignore malformed properties and not break parsing', () => { + const result = parse(`gitGraph\n commit id:"2" msg:"Malformed commit" oops:"ignored"\n`); + const commit = result.value.statements[0] as Commit; + expect(commit.id).toBe('2'); + expect(commit.message).toBe('Malformed commit'); + expect(commit.hasOwnProperty('oops')).toBe(false); + }); + + it('should parse multiple commits with different types', () => { + const result = parse(`gitGraph\n commit type:NORMAL\n commit type:REVERSE\n`); + const commit1 = result.value.statements[0] as Commit; + const commit2 = result.value.statements[1] as Commit; + expect(commit1.type).toBe('NORMAL'); + expect(commit2.type).toBe('REVERSE'); + }); +}); + +describe('Parsing Branch Statements', () => { + it('should parse a branch with a simple name', () => { + const result = parse(`gitGraph\n commit\n commit\n branch master\n`); + const branch = result.value.statements[2] as Branch; + expect(branch.name).toBe('master'); + }); + + it('should parse a branch with an order property', () => { + const result = parse(`gitGraph\n commit\n branch feature order:1\n`); + const branch = result.value.statements[1] as Branch; + expect(branch.name).toBe('feature'); + expect(branch.order).toBe(1); + }); + + it('should handle branch names with special characters', () => { + const result = parse(`gitGraph\n branch feature/test-branch\n`); + const branch = result.value.statements[0] as Branch; + expect(branch.name).toBe('feature/test-branch'); + }); + + it('should parse branches with hyphens and underscores', () => { + const result = parse(`gitGraph\n branch my-feature_branch\n`); + const branch = result.value.statements[0] as Branch; + expect(branch.name).toBe('my-feature_branch'); + }); + + it('should correctly handle branch without order property', () => { + const result = parse(`gitGraph\n branch feature\n`); + const branch = result.value.statements[0] as Branch; + expect(branch.name).toBe('feature'); + expect(branch.order).toBeUndefined(); + }); +}); + +describe('Parsing Merge Statements', () => { + it('should parse a merge with a branch name', () => { + const result = parse(`gitGraph\n merge master\n`); + const merge = result.value.statements[0] as Merge; + expect(merge.branch).toBe('master'); + }); + + it('should handle merges with additional properties', () => { + const result = parse(`gitGraph\n merge feature id:"m1" tag:"release" type:HIGHLIGHT\n`); + const merge = result.value.statements[0] as Merge; + expect(merge.branch).toBe('feature'); + expect(merge.id).toBe('m1'); + expect(merge.tags).toEqual(['release']); + expect(merge.type).toBe('HIGHLIGHT'); + }); + + it('should parse merge without any properties', () => { + const result = parse(`gitGraph\n merge feature\n`); + const merge = result.value.statements[0] as Merge; + expect(merge.branch).toBe('feature'); + }); + + it('should ignore malformed properties in merge statements', () => { + const result = parse(`gitGraph\n merge feature random:"ignored"\n`); + const merge = result.value.statements[0] as Merge; + expect(merge.branch).toBe('feature'); + expect(merge.hasOwnProperty('random')).toBe(false); + }); +}); + +describe('Parsing Checkout Statements', () => { + it('should parse a checkout to a named branch', () => { + const result = parse( + `gitGraph\n commit id:"1"\n branch develop\n branch fun\n checkout develop\n` + ); + const checkout = result.value.statements[3] as Checkout; + expect(checkout.branch).toBe('develop'); + }); + + it('should parse checkout to branches with complex names', () => { + const result = parse(`gitGraph\n checkout hotfix-123\n`); + const checkout = result.value.statements[0] as Checkout; + expect(checkout.branch).toBe('hotfix-123'); + }); + + it('should parse checkouts with hyphens and numbers', () => { + const result = parse(`gitGraph\n checkout release-2021\n`); + const checkout = result.value.statements[0] as Checkout; + expect(checkout.branch).toBe('release-2021'); + }); +}); + +describe('Parsing CherryPicking Statements', () => { + it('should parse cherry-picking with a commit id', () => { + const result = parse(`gitGraph\n commit id:"123" commit id:"321" cherry-pick id:"123"\n`); + const cherryPick = result.value.statements[2] as CherryPicking; + expect(cherryPick.id).toBe('123'); + }); + + it('should parse cherry-picking with multiple properties', () => { + const result = parse(`gitGraph\n cherry-pick id:"123" tag:"urgent" parent:"100"\n`); + const cherryPick = result.value.statements[0] as CherryPicking; + expect(cherryPick.id).toBe('123'); + expect(cherryPick.tags).toEqual(['urgent']); + expect(cherryPick.parent).toBe('100'); + }); + + describe('Parsing with Accessibility Titles and Descriptions', () => { + it('should parse accessibility titles', () => { + const result = parse(`gitGraph\n accTitle: Accessible Graph\n commit\n`); + expect(result.value.accTitle).toBe('Accessible Graph'); + }); + + it('should parse multiline accessibility descriptions', () => { + const result = parse( + `gitGraph\n accDescr {\n Detailed description\n across multiple lines\n }\n commit\n` + ); + expect(result.value.accDescr).toBe('Detailed description\nacross multiple lines'); + }); + }); + + describe('Integration Tests', () => { + it('should correctly parse a complex graph with various elements', () => { + const result = parse(` + gitGraph TB: + accTitle: Complex Example + commit id:"init" type:NORMAL + branch feature + commit id:"feat1" msg:"Add feature" + checkout main + merge feature tag:"v1.0" + cherry-pick id:"feat1" tag:"critical fix" + `); + expect(result.value.accTitle).toBe('Complex Example'); + expect(result.value.statements[0].$type).toBe('Commit'); + expect(result.value.statements[1].$type).toBe('Branch'); + expect(result.value.statements[2].$type).toBe('Commit'); + expect(result.value.statements[3].$type).toBe('Checkout'); + expect(result.value.statements[4].$type).toBe('Merge'); + expect(result.value.statements[5].$type).toBe('CherryPicking'); + }); + }); + + describe('Error Handling for Invalid Syntax', () => { + it('should report errors for unknown properties in commit', () => { + const result = parse(`gitGraph\n commit unknown:"oops"\n`); + expect(result.parserErrors).not.toHaveLength(0); + }); + + it('should report errors for invalid branch order', () => { + const result = parse(`gitGraph\n branch feature order:xyz\n`); + expect(result.parserErrors).not.toHaveLength(0); + }); + }); +}); diff --git a/packages/parser/tests/test-util.ts b/packages/parser/tests/test-util.ts index 9bdec348a..5cb487758 100644 --- a/packages/parser/tests/test-util.ts +++ b/packages/parser/tests/test-util.ts @@ -1,7 +1,18 @@ import type { LangiumParser, ParseResult } from 'langium'; import { expect, vi } from 'vitest'; -import type { Info, InfoServices, Pie, PieServices } from '../src/language/index.js'; -import { createInfoServices, createPieServices } from '../src/language/index.js'; +import type { + Info, + InfoServices, + Pie, + PieServices, + GitGraph, + GitGraphServices, +} from '../src/language/index.js'; +import { + createInfoServices, + createPieServices, + createGitGraphServices, +} from '../src/language/index.js'; const consoleMock = vi.spyOn(console, 'log').mockImplementation(() => undefined); @@ -40,3 +51,14 @@ export function createPieTestServices() { return { services: pieServices, parse }; } export const pieParse = createPieTestServices().parse; + +const gitGraphServices: GitGraphServices = createGitGraphServices().GitGraph; +const gitGraphParser: LangiumParser = gitGraphServices.parser.LangiumParser; +export function createGitGraphTestServices() { + const parse = (input: string) => { + return gitGraphParser.parse(input); + }; + + return { services: gitGraphServices, parse }; +} +export const gitGraphParse = createGitGraphTestServices().parse; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f3bc6754..b377a1570 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,12 @@ importers: '@argos-ci/cypress': specifier: ^2.1.0 version: 2.1.1(cypress@13.13.2) + '@changesets/changelog-github': + specifier: ^0.5.0 + version: 0.5.0(encoding@0.1.13) + '@changesets/cli': + specifier: ^2.27.7 + version: 2.27.7 '@cspell/eslint-plugin': specifier: ^8.8.4 version: 8.13.1(eslint@9.8.0) @@ -71,6 +77,9 @@ importers: cors: specifier: ^2.8.5 version: 2.8.5 + cpy-cli: + specifier: ^5.0.0 + version: 5.0.0 cross-env: specifier: ^7.0.3 version: 7.0.3 @@ -309,9 +318,6 @@ importers: concurrently: specifier: ^8.2.2 version: 8.2.2 - cpy-cli: - specifier: ^5.0.0 - version: 5.0.0 csstree-validator: specifier: ^3.0.0 version: 3.0.0 @@ -401,31 +407,6 @@ importers: specifier: ^5.0.5 version: 5.0.10 - packages/mermaid-flowchart-elk: - dependencies: - d3: - specifier: ^7.9.0 - version: 7.9.0 - dagre-d3-es: - specifier: 7.0.10 - version: 7.0.10 - elkjs: - specifier: ^0.9.2 - version: 0.9.3 - khroma: - specifier: ^2.1.0 - version: 2.1.0 - devDependencies: - concurrently: - specifier: ^8.2.2 - version: 8.2.2 - mermaid: - specifier: workspace:^ - version: link:../mermaid - rimraf: - specifier: ^5.0.5 - version: 5.0.10 - packages/mermaid-layout-elk: dependencies: d3: @@ -434,6 +415,10 @@ importers: elkjs: specifier: ^0.9.3 version: 0.9.3 + devDependencies: + '@types/d3': + specifier: ^7.4.3 + version: 7.4.3 mermaid: specifier: workspace:^ version: link:../mermaid @@ -1374,6 +1359,67 @@ packages: '@braintree/sanitize-url@7.1.0': resolution: {integrity: sha512-o+UlMLt49RvtCASlOMW0AkHnabN9wR9rwCCherxO0yG4Npy34GkvrAqdXQvrhNs+jh+gkK8gB8Lf05qL/O7KWg==} + '@changesets/apply-release-plan@7.0.4': + resolution: {integrity: sha512-HLFwhKWayKinWAul0Vj+76jVx1Pc2v55MGPVjZ924Y/ROeSsBMFutv9heHmCUj48lJyRfOTJG5+ar+29FUky/A==} + + '@changesets/assemble-release-plan@6.0.3': + resolution: {integrity: sha512-bLNh9/Lgl1VwkjWZTq8JmRqH+hj7/Yzfz0jsQ/zJJ+FTmVqmqPj3szeKOri8O/hEM8JmHW019vh2gTO9iq5Cuw==} + + '@changesets/changelog-git@0.2.0': + resolution: {integrity: sha512-bHOx97iFI4OClIT35Lok3sJAwM31VbUM++gnMBV16fdbtBhgYu4dxsphBF/0AZZsyAHMrnM0yFcj5gZM1py6uQ==} + + '@changesets/changelog-github@0.5.0': + resolution: {integrity: sha512-zoeq2LJJVcPJcIotHRJEEA2qCqX0AQIeFE+L21L8sRLPVqDhSXY8ZWAt2sohtBpFZkBwu+LUwMSKRr2lMy3LJA==} + + '@changesets/cli@2.27.7': + resolution: {integrity: sha512-6lr8JltiiXPIjDeYg4iM2MeePP6VN/JkmqBsVA5XRiy01hGS3y629LtSDvKcycj/w/5Eur1rEwby/MjcYS+e2A==} + hasBin: true + + '@changesets/config@3.0.2': + resolution: {integrity: sha512-cdEhS4t8woKCX2M8AotcV2BOWnBp09sqICxKapgLHf9m5KdENpWjyrFNMjkLqGJtUys9U+w93OxWT0czorVDfw==} + + '@changesets/errors@0.2.0': + resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + + '@changesets/get-dependents-graph@2.1.1': + resolution: {integrity: sha512-LRFjjvigBSzfnPU2n/AhFsuWR5DK++1x47aq6qZ8dzYsPtS/I5mNhIGAS68IAxh1xjO9BTtz55FwefhANZ+FCA==} + + '@changesets/get-github-info@0.6.0': + resolution: {integrity: sha512-v/TSnFVXI8vzX9/w3DU2Ol+UlTZcu3m0kXTjTT4KlAdwSvwutcByYwyYn9hwerPWfPkT2JfpoX0KgvCEi8Q/SA==} + + '@changesets/get-release-plan@4.0.3': + resolution: {integrity: sha512-6PLgvOIwTSdJPTtpdcr3sLtGatT+Jr22+cQwEBJBy6wP0rjB4yJ9lv583J9fVpn1bfQlBkDa8JxbS2g/n9lIyA==} + + '@changesets/get-version-range-type@0.4.0': + resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} + + '@changesets/git@3.0.0': + resolution: {integrity: sha512-vvhnZDHe2eiBNRFHEgMiGd2CT+164dfYyrJDhwwxTVD/OW0FUD6G7+4DIx1dNwkwjHyzisxGAU96q0sVNBns0w==} + + '@changesets/logger@0.1.0': + resolution: {integrity: sha512-pBrJm4CQm9VqFVwWnSqKEfsS2ESnwqwH+xR7jETxIErZcfd1u2zBSqrHbRHR7xjhSgep9x2PSKFKY//FAshA3g==} + + '@changesets/parse@0.4.0': + resolution: {integrity: sha512-TS/9KG2CdGXS27S+QxbZXgr8uPsP4yNJYb4BC2/NeFUj80Rni3TeD2qwWmabymxmrLo7JEsytXH1FbpKTbvivw==} + + '@changesets/pre@2.0.0': + resolution: {integrity: sha512-HLTNYX/A4jZxc+Sq8D1AMBsv+1qD6rmmJtjsCJa/9MSRybdxh0mjbTvE6JYZQ/ZiQ0mMlDOlGPXTm9KLTU3jyw==} + + '@changesets/read@0.6.0': + resolution: {integrity: sha512-ZypqX8+/im1Fm98K4YcZtmLKgjs1kDQ5zHpc2U1qdtNBmZZfo/IBiG162RoP0CUF05tvp2y4IspH11PLnPxuuw==} + + '@changesets/should-skip-package@0.1.0': + resolution: {integrity: sha512-FxG6Mhjw7yFStlSM7Z0Gmg3RiyQ98d/9VpQAZ3Fzr59dCOM9G6ZdYbjiSAt0XtFr9JR5U2tBaJWPjrkGGc618g==} + + '@changesets/types@4.1.0': + resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} + + '@changesets/types@6.0.0': + resolution: {integrity: sha512-b1UkfNulgKoWfqyHtzKS5fOZYSJO+77adgL7DLRDr+/7jhChN+QcHnbjiQVOz/U+Ts3PGNySq7diAItzDgugfQ==} + + '@changesets/write@0.3.1': + resolution: {integrity: sha512-SyGtMXzH3qFqlHKcvFY2eX+6b0NGiFcNav8AFsYwy5l8hejOeoeTDemu5Yjmke2V5jpzY+pBvM0vCCQ3gdZpfw==} + '@chevrotain/cst-dts-gen@11.0.3': resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} @@ -2093,6 +2139,12 @@ packages: '@leichtgewicht/ip-codec@2.0.5': resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} + '@manypkg/find-root@1.1.0': + resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} + + '@manypkg/get-packages@1.1.3': + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@mdi/font@7.4.47': resolution: {integrity: sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==} @@ -2573,6 +2625,9 @@ packages: '@types/node-forge@1.3.11': resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + '@types/node@18.19.43': resolution: {integrity: sha512-Mw/YlgXnyJdEwLoFv2dpuJaDFriX+Pc+0qOBJ57jC1H6cDxIj2xc5yUrdtArDVG0m+KV6622a4p2tenEqB3C/g==} @@ -2610,6 +2665,9 @@ packages: '@types/rollup-plugin-visualizer@4.2.4': resolution: {integrity: sha512-BW4Q6D1Qy5gno5qHWrnMDC2dOe/TAKXvqCpckOggCCu+XpS+ZZJJ1lq1+K3bvYccoO3Y7f5kglbFAgYGqCgULg==} + '@types/semver@7.5.8': + resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + '@types/send@0.17.4': resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} @@ -3362,6 +3420,10 @@ packages: bcrypt-pbkdf@1.0.2: resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + better-path-resolve@1.0.0: + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -3543,6 +3605,9 @@ packages: character-reference-invalid@1.1.4: resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + check-error@1.0.3: resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} @@ -3829,6 +3894,9 @@ packages: engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} hasBin: true + cross-spawn@5.1.0: + resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} + cross-spawn@6.0.5: resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==} engines: {node: '>=4.8'} @@ -4097,6 +4165,9 @@ packages: resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} engines: {node: '>= 0.4'} + dataloader@1.4.0: + resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} + date-fns@2.30.0: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} @@ -4242,6 +4313,10 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + detect-libc@2.0.3: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} @@ -4297,6 +4372,10 @@ packages: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} + dotenv@8.6.0: + resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} + engines: {node: '>=10'} + duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -4663,6 +4742,13 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + extendable-error@0.1.7: + resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + extract-zip@2.0.1: resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} engines: {node: '>= 10.17.0'} @@ -4810,6 +4896,9 @@ packages: resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + find-yarn-workspace-root2@1.2.16: + resolution: {integrity: sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA==} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -4903,6 +4992,10 @@ packages: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + fs-extra@9.1.0: resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} engines: {node: '>=10'} @@ -5231,6 +5324,9 @@ packages: resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==} engines: {node: '>= 14'} + human-id@1.0.2: + resolution: {integrity: sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw==} + human-signals@1.1.1: resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} engines: {node: '>=8.12.0'} @@ -5498,6 +5594,10 @@ packages: resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} engines: {node: '>= 0.4'} + is-subdir@1.2.0: + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} + engines: {node: '>=4'} + is-symbol@1.0.4: resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} engines: {node: '>= 0.4'} @@ -5958,6 +6058,10 @@ packages: resolution: {integrity: sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==} engines: {node: '>=18.0.0'} + load-yaml-file@0.2.0: + resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==} + engines: {node: '>=6'} + loader-runner@4.3.0: resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} engines: {node: '>=6.11.5'} @@ -6010,6 +6114,9 @@ packages: lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -6045,6 +6152,9 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@4.1.5: + resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -6355,6 +6465,10 @@ packages: mlly@1.7.1: resolution: {integrity: sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==} + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + mrmime@2.0.0: resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} engines: {node: '>=10'} @@ -6535,9 +6649,16 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + ospath@1.2.2: resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==} + outdent@0.5.0: + resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + p-cancelable@2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} @@ -6546,6 +6667,10 @@ packages: resolution: {integrity: sha512-dd589iCQ7m1L0bmC5NLlVYfy3TbBEsMUfWx9PyAgPeIcFZ/E2yaTZ4Rz4MiBmmJShviiftHVXOqfnfzJ6kyMrQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-filter@2.1.0: + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} + engines: {node: '>=8'} + p-filter@3.0.0: resolution: {integrity: sha512-QtoWLjXAW++uTX67HZQz1dbTpqBfiidsB6VtQUC9iR85S120+s0T5sO6s+B5MLzFcZkrEd/DGMmCjR+f2Qpxwg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -6590,6 +6715,10 @@ packages: resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + p-map@3.0.0: resolution: {integrity: sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==} engines: {node: '>=8'} @@ -6740,6 +6869,10 @@ packages: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + pino-abstract-transport@1.2.0: resolution: {integrity: sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==} @@ -6858,6 +6991,10 @@ packages: preact@10.23.1: resolution: {integrity: sha512-O5UdRsNh4vdZaTieWe3XOgSpdMAmkIYBCT3VhQDlKrzyCm8lUYsk0fmVEvoQQifoOjFRTaHZO69ylrzTW2BH+A==} + preferred-pm@3.1.4: + resolution: {integrity: sha512-lEHd+yEm22jXdCphDrkvIJQU66EuLojPPtvZkpKIkiD+l0DMThF/niqZKJSoU8Vl7iuvtmzyMhir9LdVy5WMnA==} + engines: {node: '>=10'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -6926,6 +7063,9 @@ packages: engines: {node: '>= 0.10'} hasBin: true + pseudomap@1.0.2: + resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} + psl@1.9.0: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} @@ -6991,6 +7131,10 @@ packages: resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} engines: {node: '>=8'} + read-yaml-file@1.1.0: + resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} + engines: {node: '>=6'} + readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -7440,6 +7584,9 @@ packages: resolution: {integrity: sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==} engines: {node: '>=8'} + spawndamnit@2.0.0: + resolution: {integrity: sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==} + spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -7571,6 +7718,10 @@ packages: resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} engines: {node: '>=12'} + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + strip-bom@4.0.0: resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} engines: {node: '>=8'} @@ -7669,6 +7820,10 @@ packages: resolution: {integrity: sha512-DFpBhaF5j+2f7kheKFc1ajsAUUDGOaNPpKPtiIMxlbfud6mvfFZuWGnTRpaujUa5J7yl6cIw/h6nyr4mSsENPg==} engines: {node: '>=8'} + term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + terser-webpack-plugin@5.3.10: resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} engines: {node: '>= 10.13.0'} @@ -7738,6 +7893,10 @@ packages: resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} engines: {node: '>=14.0.0'} + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + tmp@0.2.3: resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} engines: {node: '>=14.14'} @@ -8358,6 +8517,10 @@ packages: which-module@2.0.1: resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + which-pm@2.2.0: + resolution: {integrity: sha512-MOiaDbA5ZZgUjkeMWM5EkJp4loW5ZRoa5bc3/aeMox/PJelMhE6t7S/mLuiY43DBupyxH+S0U1bTui9kWUlmsw==} + engines: {node: '>=8.15'} + which-typed-array@1.1.15: resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} engines: {node: '>= 0.4'} @@ -8526,6 +8689,9 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + yallist@2.1.2: + resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -9797,6 +9963,177 @@ snapshots: '@braintree/sanitize-url@7.1.0': {} + '@changesets/apply-release-plan@7.0.4': + dependencies: + '@babel/runtime': 7.25.0 + '@changesets/config': 3.0.2 + '@changesets/get-version-range-type': 0.4.0 + '@changesets/git': 3.0.0 + '@changesets/should-skip-package': 0.1.0 + '@changesets/types': 6.0.0 + '@manypkg/get-packages': 1.1.3 + detect-indent: 6.1.0 + fs-extra: 7.0.1 + lodash.startcase: 4.4.0 + outdent: 0.5.0 + prettier: 2.8.8 + resolve-from: 5.0.0 + semver: 7.6.3 + + '@changesets/assemble-release-plan@6.0.3': + dependencies: + '@babel/runtime': 7.25.0 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.1 + '@changesets/should-skip-package': 0.1.0 + '@changesets/types': 6.0.0 + '@manypkg/get-packages': 1.1.3 + semver: 7.6.3 + + '@changesets/changelog-git@0.2.0': + dependencies: + '@changesets/types': 6.0.0 + + '@changesets/changelog-github@0.5.0(encoding@0.1.13)': + dependencies: + '@changesets/get-github-info': 0.6.0(encoding@0.1.13) + '@changesets/types': 6.0.0 + dotenv: 8.6.0 + transitivePeerDependencies: + - encoding + + '@changesets/cli@2.27.7': + dependencies: + '@babel/runtime': 7.25.0 + '@changesets/apply-release-plan': 7.0.4 + '@changesets/assemble-release-plan': 6.0.3 + '@changesets/changelog-git': 0.2.0 + '@changesets/config': 3.0.2 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.1 + '@changesets/get-release-plan': 4.0.3 + '@changesets/git': 3.0.0 + '@changesets/logger': 0.1.0 + '@changesets/pre': 2.0.0 + '@changesets/read': 0.6.0 + '@changesets/should-skip-package': 0.1.0 + '@changesets/types': 6.0.0 + '@changesets/write': 0.3.1 + '@manypkg/get-packages': 1.1.3 + '@types/semver': 7.5.8 + ansi-colors: 4.1.3 + chalk: 2.4.2 + ci-info: 3.9.0 + enquirer: 2.4.1 + external-editor: 3.1.0 + fs-extra: 7.0.1 + human-id: 1.0.2 + mri: 1.2.0 + outdent: 0.5.0 + p-limit: 2.3.0 + preferred-pm: 3.1.4 + resolve-from: 5.0.0 + semver: 7.6.3 + spawndamnit: 2.0.0 + term-size: 2.2.1 + + '@changesets/config@3.0.2': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.1 + '@changesets/logger': 0.1.0 + '@changesets/types': 6.0.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + micromatch: 4.0.7 + + '@changesets/errors@0.2.0': + dependencies: + extendable-error: 0.1.7 + + '@changesets/get-dependents-graph@2.1.1': + dependencies: + '@changesets/types': 6.0.0 + '@manypkg/get-packages': 1.1.3 + chalk: 2.4.2 + fs-extra: 7.0.1 + semver: 7.6.3 + + '@changesets/get-github-info@0.6.0(encoding@0.1.13)': + dependencies: + dataloader: 1.4.0 + node-fetch: 2.6.7(encoding@0.1.13) + transitivePeerDependencies: + - encoding + + '@changesets/get-release-plan@4.0.3': + dependencies: + '@babel/runtime': 7.25.0 + '@changesets/assemble-release-plan': 6.0.3 + '@changesets/config': 3.0.2 + '@changesets/pre': 2.0.0 + '@changesets/read': 0.6.0 + '@changesets/types': 6.0.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/get-version-range-type@0.4.0': {} + + '@changesets/git@3.0.0': + dependencies: + '@babel/runtime': 7.25.0 + '@changesets/errors': 0.2.0 + '@changesets/types': 6.0.0 + '@manypkg/get-packages': 1.1.3 + is-subdir: 1.2.0 + micromatch: 4.0.7 + spawndamnit: 2.0.0 + + '@changesets/logger@0.1.0': + dependencies: + chalk: 2.4.2 + + '@changesets/parse@0.4.0': + dependencies: + '@changesets/types': 6.0.0 + js-yaml: 3.14.1 + + '@changesets/pre@2.0.0': + dependencies: + '@babel/runtime': 7.25.0 + '@changesets/errors': 0.2.0 + '@changesets/types': 6.0.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + + '@changesets/read@0.6.0': + dependencies: + '@babel/runtime': 7.25.0 + '@changesets/git': 3.0.0 + '@changesets/logger': 0.1.0 + '@changesets/parse': 0.4.0 + '@changesets/types': 6.0.0 + chalk: 2.4.2 + fs-extra: 7.0.1 + p-filter: 2.1.0 + + '@changesets/should-skip-package@0.1.0': + dependencies: + '@babel/runtime': 7.25.0 + '@changesets/types': 6.0.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/types@4.1.0': {} + + '@changesets/types@6.0.0': {} + + '@changesets/write@0.3.1': + dependencies: + '@babel/runtime': 7.25.0 + '@changesets/types': 6.0.0 + fs-extra: 7.0.1 + human-id: 1.0.2 + prettier: 2.8.8 + '@chevrotain/cst-dts-gen@11.0.3': dependencies: '@chevrotain/gast': 11.0.3 @@ -10563,6 +10900,22 @@ snapshots: '@leichtgewicht/ip-codec@2.0.5': {} + '@manypkg/find-root@1.1.0': + dependencies: + '@babel/runtime': 7.25.0 + '@types/node': 12.20.55 + find-up: 4.1.0 + fs-extra: 8.1.0 + + '@manypkg/get-packages@1.1.3': + dependencies: + '@babel/runtime': 7.25.0 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 + fs-extra: 8.1.0 + globby: 11.1.0 + read-yaml-file: 1.1.0 + '@mdi/font@7.4.47': {} '@microsoft/tsdoc-config@0.17.0': @@ -11076,6 +11429,8 @@ snapshots: dependencies: '@types/node': 20.14.14 + '@types/node@12.20.55': {} + '@types/node@18.19.43': dependencies: undici-types: 5.26.5 @@ -11112,6 +11467,8 @@ snapshots: dependencies: rollup: 2.79.1 + '@types/semver@7.5.8': {} + '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 @@ -12084,6 +12441,10 @@ snapshots: dependencies: tweetnacl: 0.14.5 + better-path-resolve@1.0.0: + dependencies: + is-windows: 1.0.2 + binary-extensions@2.3.0: {} binary-searching@2.0.5: {} @@ -12283,6 +12644,8 @@ snapshots: character-reference-invalid@1.1.4: {} + chardet@0.7.0: {} + check-error@1.0.3: dependencies: get-func-name: 2.0.2 @@ -12587,6 +12950,12 @@ snapshots: dependencies: cross-spawn: 7.0.3 + cross-spawn@5.1.0: + dependencies: + lru-cache: 4.1.5 + shebang-command: 1.2.0 + which: 1.3.1 + cross-spawn@6.0.5: dependencies: nice-try: 1.0.5 @@ -12985,6 +13354,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.1 + dataloader@1.4.0: {} + date-fns@2.30.0: dependencies: '@babel/runtime': 7.25.0 @@ -13083,6 +13454,8 @@ snapshots: destroy@1.2.0: {} + detect-indent@6.1.0: {} + detect-libc@2.0.3: {} detect-newline@3.1.0: {} @@ -13131,6 +13504,8 @@ snapshots: dotenv@16.4.5: {} + dotenv@8.6.0: {} + duplexer@0.1.2: {} eastasianwidth@0.2.0: {} @@ -13652,6 +14027,14 @@ snapshots: extend@3.0.2: {} + extendable-error@0.1.7: {} + + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + extract-zip@2.0.1(supports-color@8.1.1): dependencies: debug: 4.3.6(supports-color@8.1.1) @@ -13838,6 +14221,11 @@ snapshots: locate-path: 7.2.0 path-exists: 5.0.0 + find-yarn-workspace-root2@1.2.16: + dependencies: + micromatch: 4.0.7 + pkg-dir: 4.2.0 + flat-cache@4.0.1: dependencies: flatted: 3.3.1 @@ -13926,6 +14314,12 @@ snapshots: jsonfile: 4.0.0 universalify: 0.1.2 + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + fs-extra@9.1.0: dependencies: at-least-node: 1.0.0 @@ -14297,6 +14691,8 @@ snapshots: transitivePeerDependencies: - supports-color + human-id@1.0.2: {} + human-signals@1.1.1: {} human-signals@2.1.0: {} @@ -14495,6 +14891,10 @@ snapshots: dependencies: has-tostringtag: 1.0.2 + is-subdir@1.2.0: + dependencies: + better-path-resolve: 1.0.0 + is-symbol@1.0.4: dependencies: has-symbols: 1.0.3 @@ -15204,6 +15604,13 @@ snapshots: rfdc: 1.4.1 wrap-ansi: 9.0.0 + load-yaml-file@0.2.0: + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.14.1 + pify: 4.0.1 + strip-bom: 3.0.0 + loader-runner@4.3.0: {} local-pkg@0.4.3: {} @@ -15246,6 +15653,8 @@ snapshots: lodash.sortby@4.7.0: {} + lodash.startcase@4.4.0: {} + lodash@4.17.21: {} log-symbols@4.1.0: @@ -15282,6 +15691,11 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@4.1.5: + dependencies: + pseudomap: 1.0.2 + yallist: 2.1.2 + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -15764,6 +16178,8 @@ snapshots: pkg-types: 1.1.3 ufo: 1.5.4 + mri@1.2.0: {} + mrmime@2.0.0: {} ms@2.0.0: {} @@ -15951,14 +16367,22 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + os-tmpdir@1.0.2: {} + ospath@1.2.2: {} + outdent@0.5.0: {} + p-cancelable@2.1.1: {} p-event@5.0.1: dependencies: p-timeout: 5.1.0 + p-filter@2.1.0: + dependencies: + p-map: 2.1.0 + p-filter@3.0.0: dependencies: p-map: 5.5.0 @@ -15999,6 +16423,8 @@ snapshots: dependencies: p-limit: 4.0.0 + p-map@2.1.0: {} + p-map@3.0.0: dependencies: aggregate-error: 3.1.0 @@ -16121,6 +16547,8 @@ snapshots: pify@2.3.0: {} + pify@4.0.1: {} + pino-abstract-transport@1.2.0: dependencies: readable-stream: 4.5.2 @@ -16242,6 +16670,13 @@ snapshots: preact@10.23.1: {} + preferred-pm@3.1.4: + dependencies: + find-up: 5.0.0 + find-yarn-workspace-root2: 1.2.16 + path-exists: 4.0.0 + which-pm: 2.2.0 + prelude-ls@1.2.1: {} prettier-plugin-jsdoc@1.3.0(prettier@3.3.3): @@ -16297,6 +16732,8 @@ snapshots: dependencies: event-stream: 3.3.4 + pseudomap@1.0.2: {} + psl@1.9.0: {} pump@3.0.0: @@ -16360,6 +16797,13 @@ snapshots: parse-json: 5.2.0 type-fest: 0.6.0 + read-yaml-file@1.1.0: + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.14.1 + pify: 4.0.1 + strip-bom: 3.0.0 + readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 @@ -16904,6 +17348,11 @@ snapshots: signal-exit: 3.0.7 which: 2.0.2 + spawndamnit@2.0.0: + dependencies: + cross-spawn: 5.1.0 + signal-exit: 3.0.7 + spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 @@ -17084,6 +17533,8 @@ snapshots: dependencies: ansi-regex: 6.0.1 + strip-bom@3.0.0: {} + strip-bom@4.0.0: {} strip-comments@2.0.1: {} @@ -17197,6 +17648,8 @@ snapshots: ansi-escapes: 4.3.2 iterm2-version: 4.2.0 + term-size@2.2.1: {} + terser-webpack-plugin@5.3.10(esbuild@0.21.5)(webpack@5.93.0(esbuild@0.21.5)(webpack-cli@4.10.0)): dependencies: '@jridgewell/trace-mapping': 0.3.25 @@ -17267,6 +17720,10 @@ snapshots: tinyspy@2.2.1: {} + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + tmp@0.2.3: {} tmpl@1.0.5: {} @@ -18049,6 +18506,11 @@ snapshots: which-module@2.0.1: {} + which-pm@2.2.0: + dependencies: + load-yaml-file: 0.2.0 + path-exists: 4.0.0 + which-typed-array@1.1.15: dependencies: available-typed-arrays: 1.0.7 @@ -18253,6 +18715,8 @@ snapshots: y18n@5.0.8: {} + yallist@2.1.2: {} + yallist@3.1.1: {} yaml@2.5.0: {} diff --git a/tests/webpack/package.json b/tests/webpack/package.json index 5d211ca1b..12bb73195 100644 --- a/tests/webpack/package.json +++ b/tests/webpack/package.json @@ -1,5 +1,5 @@ { - "name": "webpack", + "name": "@mermaid-js/webpack-test", "version": "1.0.0", "description": "", "private": true,