Merge branch 'develop' into knsv/new-shapes

This commit is contained in:
Knut Sveidqvist 2024-08-26 11:59:07 +02:00
commit e842b72aaa
92 changed files with 5886 additions and 3527 deletions

8
.changeset/README.md Normal file
View File

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

12
.changeset/config.json Normal file
View File

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

View File

@ -0,0 +1,5 @@
---
'mermaid': minor
---
feat(er): allow multi-line relationship labels

View File

@ -138,6 +138,7 @@ tsdoc
typeof
typestr
unshift
urlsafe
verifymethod
VERIFYMTHD
WARN_DOCSDIR_DOESNT_MATCH

View File

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

View File

@ -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!** 🎉

View File

@ -7,17 +7,19 @@ on:
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'
@ -40,4 +42,4 @@ jobs:
working-directory: ./packages/mermaid
run: pnpm run docs:build
- uses: autofix-ci/action@ff86a557419858bb967097bfc916833f5647fa8c
- uses: autofix-ci/action@ff86a557419858bb967097bfc916833f5647fa8c # main

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,26 +4,32 @@ on:
push:
merge_group:
pull_request:
types:
- opened
- synchronize
- ready_for_review
workflow_dispatch:
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: write
jobs:
docker-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: hadolint/hadolint-action@54c9adbab1582c2ef04b2016b760714a4bfde3cf # v3.1.0
with:
verbose: true
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 +88,10 @@ jobs:
working-directory: ./packages/mermaid
continue-on-error: ${{ github.event_name == 'push' }}
run: pnpm run docs:verify
- uses: testomatio/check-tests@0ea638fcec1820cf2e7b9854fdbdd04128a55bd4 # stable
with:
framework: cypress
tests: './cypress/e2e/**/**.spec.js'
token: ${{ secrets.GITHUB_TOKEN }}
has-tests-label: true

View File

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

View File

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

View File

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

View File

@ -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'
@ -28,7 +28,7 @@ jobs:
CYPRESS_CACHE_FOLDER: .cache/Cypress
- name: Install Json
run: npm i json --global
run: npm i json@11.0.0 --global
- name: Publish
working-directory: ./packages/mermaid

View File

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

44
.github/workflows/release.yml vendored Normal file
View File

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

37
.github/workflows/scorecard.yml vendored Normal file
View File

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

View File

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

View File

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

View File

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

2
.hadolint.yaml Normal file
View File

@ -0,0 +1,2 @@
ignored:
- DL3002 # TODO: Last USER should not be root

View File

@ -1,4 +1,2 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
#!/usr/bin/env sh
NODE_OPTIONS="--max_old_space_size=8192" pnpm run pre-commit

View File

@ -1,2 +1,10 @@
FROM node:20.12.2-alpine3.19 AS base
RUN wget -qO- https://get.pnpm.io/install.sh | ENV="$HOME/.shrc" SHELL="$(which sh)" sh -
FROM node:20.12.2-alpine3.19@sha256:7a91aa397f2e2dfbfcdad2e2d72599f374e0b0172be1d86eeb73f1d33f36a4b2
USER 0:0
RUN corepack enable \
&& corepack enable pnpm
ENV NODE_OPTIONS="--max_old_space_size=8192"
EXPOSE 9000 3333

View File

@ -36,6 +36,7 @@ Try Live Editor previews of future releases: <a href="https://develop.git.mermai
[![Join our Discord!](https://img.shields.io/static/v1?message=join%20chat&color=9cf&logo=discord&label=discord)](https://discord.gg/AgrbSrBer3)
[![Twitter Follow](https://img.shields.io/badge/Social-mermaidjs__-blue?style=social&logo=X)](https://twitter.com/mermaidjs_)
[![Covered by Argos Visual Testing](https://argos-ci.com/badge.svg)](https://argos-ci.com)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/mermaid-js/mermaid/badge)](https://securityscorecards.dev/viewer/?uri=github.com/mermaid-js/mermaid)
<img src="./img/header.png" alt="" />

View File

@ -321,4 +321,37 @@ ORDER ||--|{ LINE-ITEM : contains
{ logLevel: 1 }
);
});
it('should render relationship labels with line breaks', () => {
imgSnapshotTest(
`
erDiagram
p[Person] {
string firstName
string lastName
}
a["Customer Account"] {
string email
}
b["Customer Account Secondary"] {
string email
}
c["Customer Account Tertiary"] {
string email
}
d["Customer Account Nth"] {
string email
}
p ||--o| a : "has<br />one"
p ||--o| b : "has<br />one<br />two"
p ||--o| c : "has<br />one<br/>two<br />three"
p ||--o| d : "has<br />one<br />two<br/>three<br />...<br/>Nth"
`,
{ logLevel: 1 }
);
});
});

View File

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

View File

@ -125,6 +125,35 @@
</pre>
<hr />
<pre class="mermaid">
erDiagram
p[Person] {
string firstName
string lastName
}
a["Customer Account"] {
string email
}
b["Customer Account Secondary"] {
string email
}
c["Customer Account Tertiary"] {
string email
}
d["Customer Account Nth"] {
string email
}
p ||--o| a : "has<br />one"
p ||--o| b : "has<br />one<br />two"
p ||--o| c : "has<br />one<br />two<br />three"
p ||--o| d : "has<br />one<br />two<br />three<br />...<br />Nth"
</pre>
<hr />
<pre class="mermaid">
erDiagram
_customer_order {

View File

@ -7,9 +7,6 @@ services:
tty: true
working_dir: /mermaid
mem_limit: '8G'
entrypoint: ./docker-entrypoint.sh
environment:
- NODE_OPTIONS=--max_old_space_size=8192
volumes:
- ./:/mermaid
- root_cache:/root/.cache

View File

@ -1,3 +0,0 @@
#!/bin/sh
source /root/.shrc
exec "$@"

View File

@ -370,9 +370,9 @@ If the users have no way to know that things have changed, then you haven't real
Likewise, if users don't know that there is a new feature that you've implemented, it will forever remain unknown and unused.
The documentation has to be updated for users to know that things have been changed and added!
If you are adding a new feature, add `(v10.8.0+)` in the title or description. It will be replaced automatically with the current version number when the release happens.
If you are adding a new feature, add `(v<MERMAID_RELEASE_VERSION>+)` in the title or description. It will be replaced automatically with the current version number when the release happens.
eg: `# Feature Name (v10.8.0+)`
eg: `# Feature Name (v<MERMAID_RELEASE_VERSION>+)`
We know it can sometimes be hard to code _and_ write user documentation.

View File

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

View File

@ -63,7 +63,7 @@ import matplotlib.pyplot as plt
def mm(graph):
graphbytes = graph.encode("utf8")
base64_bytes = base64.b64encode(graphbytes)
base64_bytes = base64.urlsafe_b64encode(graphbytes)
base64_string = base64_bytes.decode("ascii")
display(Image(url="https://mermaid.ink/img/" + base64_string))

View File

@ -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. Its 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. Heres 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. Heres 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 diagrams 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 datas 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.

View File

@ -6,6 +6,24 @@
# 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

View File

@ -286,6 +286,7 @@ erDiagram
- If you want the relationship label to be more than one word, you must use double quotes around the phrase
- If you don't want a label at all on a relationship, you must use an empty double-quoted string
- (v\<MERMAID_RELEASE_VERSION>+) If you want a multi-line label on a relationship, use `<br />` between the two lines (`"first line<br />second line"`)
## Styling

View File

@ -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,8 +42,7 @@
"test": "pnpm lint && vitest run",
"test:watch": "vitest --watch",
"test:coverage": "vitest --coverage",
"prepublishOnly": "pnpm build && pnpm test",
"prepare": "husky install && pnpm build",
"prepare": "husky && pnpm build",
"pre-commit": "lint-staged"
},
"repository": {
@ -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",

View File

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

View File

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

View File

@ -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"
@ -34,13 +32,14 @@
"d3": "^7.9.0",
"elkjs": "^0.9.3"
},
"peerDependencies": {
"devDependencies": {
"@types/d3": "^7.4.3",
"mermaid": "workspace:^"
},
"peerDependencies": {
"mermaid": "^11.0.0"
},
"files": [
"dist"
],
"devDependencies": {
"@types/d3": "^7.4.3"
}
]
}

View File

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

View File

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

View File

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

View File

@ -5,23 +5,34 @@
* So contributors adding new features will only have to add the placeholder and not worry about updating the version number.
*
*/
import { readFile, writeFile } from 'fs/promises';
import { posix } from 'path';
import {
getGlobs,
getFilesFromGlobs,
SOURCE_DOCS_DIR,
readSyncedUTF8file,
getGlobs,
MERMAID_RELEASE_VERSION,
readSyncedUTF8file,
SOURCE_DOCS_DIR,
} from './docs.mjs';
import { writeFile } from 'fs/promises';
const verifyOnly: boolean = process.argv.includes('--verify');
const versionPlaceholder = '<MERMAID_RELEASE_VERSION>';
const verifyDocumentation = async () => {
const fileContent = await readFile('./src/docs/community/contributing.md', 'utf-8');
if (!fileContent.includes(versionPlaceholder)) {
console.error(
`The placeholder ${versionPlaceholder} is not present in the contributing.md file.`
);
process.exit(1);
}
};
const main = async () => {
await verifyDocumentation();
const sourceDirGlob = posix.join('.', SOURCE_DOCS_DIR, '**');
const mdFileGlobs = getGlobs([posix.join(sourceDirGlob, '*.md')]);
mdFileGlobs.push('!**/community/development.md', '!**/community/code.md');
mdFileGlobs.push('!**/community/contributing.md');
const mdFiles = await getFilesFromGlobs(mdFileGlobs);
mdFiles.sort();
const mdFilesWithPlaceholder: string[] = [];

View File

@ -519,6 +519,8 @@ const drawRelationshipFromLayout = function (svg, rel, g, insert, diagObj) {
// Append a text node containing the label
const labelId = 'rel' + relCnt;
const labelText = rel.roleA.split(/<br ?\/>/g);
const labelNode = svg
.append('text')
.classed('er relationshipLabel', true)
@ -528,8 +530,20 @@ const drawRelationshipFromLayout = function (svg, rel, g, insert, diagObj) {
.style('text-anchor', 'middle')
.style('dominant-baseline', 'middle')
.style('font-family', getConfig().fontFamily)
.style('font-size', conf.fontSize + 'px')
.text(rel.roleA);
.style('font-size', conf.fontSize + 'px');
if (labelText.length == 1) {
labelNode.text(rel.roleA);
} else {
const firstShift = -(labelText.length - 1) * 0.5;
labelText.forEach((txt, i) => {
labelNode
.append('tspan')
.attr('x', labelPoint.x)
.attr('dy', `${i === 0 ? firstShift : 1}em`)
.text(txt);
});
}
// Figure out how big the opaque 'container' rectangle needs to be
const labelBBox = labelNode.node().getBBox();

View File

@ -54,7 +54,7 @@ export const draw = async function (text: string, id: string, _version: string,
data4Layout.diagramId = id;
log.debug('REF1:', data4Layout);
await render(data4Layout, svg);
const padding = data4Layout.config.flowchart?.padding ?? 8;
const padding = data4Layout.config.flowchart?.diagramPadding ?? 8;
utils.insertTitle(
svg,
'flowchartTitleText',

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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<string, Commit>;
head: Commit | null;
branchConfig: Map<string, { name: string; order: number | undefined }>;
branches: Map<string, string | null>;
currBranch: string;
direction: DiagramOrientation;
seq: number;
options: any;
}
const DEFAULT_GITGRAPH_CONFIG: Required<GitGraphDiagramConfig> = DEFAULT_CONFIG.gitGraph;
const getConfig = (): Required<GitGraphDiagramConfig> => {
const config = cleanAndMerge({
...DEFAULT_GITGRAPH_CONFIG,
...commonGetConfig().gitGraph,
});
return config;
};
const state = new ImperativeState<GitGraphState>(() => {
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,
};

View File

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

View File

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

View File

@ -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<string, (stmt: any) => 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<void> => {
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');
});
});
}

File diff suppressed because it is too large Load Diff

View File

@ -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<string, { id: string, message: string, seq: number, type: number, tag: string, parents: string[], branch: string }>} 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|<br\s*\/?>/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,
};

File diff suppressed because it is too large Load Diff

View File

@ -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<GitGraphDiagramConfig> {
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<string, string | null>;
getCommits: () => Map<string, Commit>;
getCommitsArray: () => Commit[];
getCurrentBranch: () => string;
getDirection: () => DiagramOrientation;
getHead: () => Commit | null;
}
export interface GitGraphDBParseProvider extends Partial<GitGraphDB> {
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<GitGraphDB> {
prettyPrint: () => void;
clear: () => void;
getBranchesAsObjArray: () => { name: string }[];
getBranches: () => Map<string, string | null>;
getCommits: () => Map<string, Commit>;
getCommitsArray: () => Commit[];
getCurrentBranch: () => string;
getDirection: () => DiagramOrientation;
getHead: () => Commit | null;
getDiagramTitle: () => string;
}
export type DiagramOrientation = 'LR' | 'TB' | 'BT';

View File

@ -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'; }
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; }
<acc_descr>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_descr_value"; }
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
<acc_descr_multiline>[\}] { this.popState(); }
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
(\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"); //
<options>[ \r\n\t]+"end" this.popState(); // not used anymore in the renderer, fixed for backward compatibility
<options>[\s\S]+(?=[ \r\n\t]+"end") return 'OPT'; //
["]["] return 'EMPTYSTR';
["] this.begin("string");
<string>["] this.popState();
<string>[^"]* 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
<<EOF>> 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 }
// ;

View File

@ -371,9 +371,9 @@ If the users have no way to know that things have changed, then you haven't real
Likewise, if users don't know that there is a new feature that you've implemented, it will forever remain unknown and unused.
The documentation has to be updated for users to know that things have been changed and added!
If you are adding a new feature, add `(v10.8.0+)` in the title or description. It will be replaced automatically with the current version number when the release happens.
If you are adding a new feature, add `(v<MERMAID_RELEASE_VERSION>+)` in the title or description. It will be replaced automatically with the current version number when the release happens.
eg: `# Feature Name (v10.8.0+)`
eg: `# Feature Name (v<MERMAID_RELEASE_VERSION>+)`
We know it can sometimes be hard to code _and_ write user documentation.

View File

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

View File

@ -57,7 +57,7 @@ import matplotlib.pyplot as plt
def mm(graph):
graphbytes = graph.encode("utf8")
base64_bytes = base64.b64encode(graphbytes)
base64_bytes = base64.urlsafe_b64encode(graphbytes)
base64_string = base64_bytes.decode("ascii")
display(Image(url="https://mermaid.ink/img/" + base64_string))

View File

@ -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. Its 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. Heres 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. Heres 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 diagrams 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 datas 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.

View File

@ -1,5 +1,23 @@
# 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

View File

@ -1,6 +1,7 @@
{
"name": "docs",
"name": "@mermaid-js/docs",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vitepress --port 3333 --open",

View File

@ -192,6 +192,7 @@ erDiagram
- If you want the relationship label to be more than one word, you must use double quotes around the phrase
- If you don't want a label at all on a relationship, you must use an empty double-quoted string
- (v<MERMAID_RELEASE_VERSION>+) If you want a multi-line label on a relationship, use `<br />` between the two lines (`"first line<br />second line"`)
## Styling

View File

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

View File

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

View File

@ -105,7 +105,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;

View File

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

View File

@ -63,7 +63,14 @@ export const question = async (parent: SVGAElement, node: Node): Promise<SVGAEle
updateNodeBounds(node, polygon);
node.intersect = function (point) {
log.info('Intersect called SPLIT');
log.debug(
'APA12 Intersect called SPLIT\npoint:',
point,
'\nnode:\n',
node,
'\nres:',
intersect.polygon(node, points, point)
);
return intersect.polygon(node, points, point);
};

View File

@ -62,13 +62,13 @@ export const rectWithTitle = async (parent: SVGElement, node: Node) => {
)
);
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(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: /"[^"]*"|'[^']*'/;

View File

@ -0,0 +1 @@
export * from './module.js';

View File

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

View File

@ -0,0 +1,7 @@
import { AbstractMermaidTokenBuilder } from '../common/index.js';
export class GitGraphTokenBuilder extends AbstractMermaidTokenBuilder {
public constructor() {
super(['gitGraph']);
}
}

View File

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

View File

@ -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<string, LangiumParser> = {};
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<Info>;
export async function parse(diagramType: 'packet', text: string): Promise<Packet>;
export async function parse(diagramType: 'pie', text: string): Promise<Pie>;
export async function parse(diagramType: 'gitGraph', text: string): Promise<GitGraph>;
export async function parse<T extends DiagramAST>(
diagramType: keyof typeof initializers,
text: string

View File

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

View File

@ -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<GitGraph>(input);
};
return { services: gitGraphServices, parse };
}
export const gitGraphParse = createGitGraphTestServices().parse;

1310
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
{
"name": "webpack",
"name": "@mermaid-js/webpack-test",
"version": "1.0.0",
"description": "",
"private": true,