Merge branch 'develop' into update-class-diagram

This commit is contained in:
Yari DeWalt
2024-10-08 07:58:15 -07:00
committed by GitHub
138 changed files with 10973 additions and 3183 deletions

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
Fix for issue with calculation of label width when using in firefox

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
Add missing TypeScript dependencies

View File

@@ -26,6 +26,7 @@ concat
controlx controlx
controly controly
CSSCLASS CSSCLASS
curv
CYLINDEREND CYLINDEREND
CYLINDERSTART CYLINDERSTART
DAGA DAGA

View File

@@ -28,6 +28,9 @@ dictionaryDefinitions:
- name: suggestions - name: suggestions
words: words:
- none - none
- disp
- subproc
- tria
suggestWords: suggestWords:
- seperator:separator - seperator:separator
- vertice:vertex - vertice:vertex

View File

@@ -5,6 +5,7 @@ bmatrix
braintree braintree
catmull catmull
compositTitleSize compositTitleSize
curv
doublecircle doublecircle
elems elems
gantt gantt
@@ -24,6 +25,7 @@ multigraph
nodesep nodesep
NOTEGROUP NOTEGROUP
Pinterest Pinterest
procs
rankdir rankdir
ranksep ranksep
rect rect

5
.github/lychee.toml vendored
View File

@@ -44,7 +44,10 @@ exclude = [
"https://chromewebstore.google.com", "https://chromewebstore.google.com",
# Drupal 403 # Drupal 403
"https://(www.)?drupal.org" "https://(www.)?drupal.org",
# Swimm returns 404, eventhough the link is valid
"https://docs.swimm.io"
] ]
# Exclude all private IPs from checking. # Exclude all private IPs from checking.

View File

@@ -19,7 +19,7 @@ jobs:
# uses version from "packageManager" field in package.json # uses version from "packageManager" field in package.json
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with: with:
cache: pnpm cache: pnpm
node-version-file: '.node-version' node-version-file: '.node-version'

View File

@@ -23,7 +23,7 @@ jobs:
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with: with:
cache: pnpm cache: pnpm
node-version-file: '.node-version' node-version-file: '.node-version'

View File

@@ -36,7 +36,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 uses: github/codeql-action/init@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
with: with:
config-file: ./.github/codeql/codeql-config.yml config-file: ./.github/codeql/codeql-config.yml
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
@@ -48,7 +48,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # 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) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 uses: github/codeql-action/autobuild@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
# Command-line programs to run using the OS shell. # 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 # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -62,4 +62,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 uses: github/codeql-action/analyze@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12

View File

@@ -38,7 +38,7 @@ jobs:
# uses version from "packageManager" field in package.json # uses version from "packageManager" field in package.json
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'

53
.github/workflows/e2e-timings.yml vendored Normal file
View File

@@ -0,0 +1,53 @@
name: E2E - Generate Timings
on:
# run this workflow every night at 3am
schedule:
- cron: '28 3 * * *'
# or when the user triggers it from GitHub Actions page
workflow_dispatch:
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: write
jobs:
timings:
runs-on: ubuntu-latest
container:
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@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
- name: Setup Node.js
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with:
node-version-file: '.node-version'
- name: Install dependencies
uses: cypress-io/github-action@0da3c06ed8217b912deea9d8ee69630baed1737e # v6.7.6
with:
runTests: false
- name: Cypress run
uses: cypress-io/github-action@0da3c06ed8217b912deea9d8ee69630baed1737e # v6.7.6
id: cypress
with:
install: false
start: pnpm run dev:coverage
wait-on: 'http://localhost:9000'
browser: chrome
publish-summary: false
env:
VITEST_COVERAGE: true
CYPRESS_COMMIT: ${{ github.sha }}
SPLIT: 1
SPLIT_INDEX: 0
SPLIT_FILE: 'cypress/timings.json'
- name: Commit changes
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4
with:
add: 'cypress/timings.json'
author_name: 'github-actions[bot]'
author_email: '41898282+github-actions[bot]@users.noreply.github.com'
message: 'chore: update E2E timings'

View File

@@ -28,7 +28,6 @@ env:
) || ) ||
github.event.before github.event.before
}} }}
shouldRunParallel: ${{ secrets.CYPRESS_RECORD_KEY != '' && !(github.event_name == 'push' && github.ref == 'refs/heads/develop') }}
jobs: jobs:
cache: cache:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -39,7 +38,7 @@ jobs:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
- name: Cache snapshots - name: Cache snapshots
@@ -59,7 +58,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
if: ${{ steps.cache-snapshot.outputs.cache-hit != 'true' }} if: ${{ steps.cache-snapshot.outputs.cache-hit != 'true' }}
uses: cypress-io/github-action@df7484c5ba85def7eef30db301afa688187bc378 # v6.7.2 uses: cypress-io/github-action@0da3c06ed8217b912deea9d8ee69630baed1737e # v6.7.6
with: with:
# just perform install # just perform install
runTests: false runTests: false
@@ -80,7 +79,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
containers: [1, 2, 3, 4] containers: [1, 2, 3, 4, 5]
steps: steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
@@ -88,7 +87,7 @@ jobs:
# uses version from "packageManager" field in package.json # uses version from "packageManager" field in package.json
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
@@ -101,7 +100,7 @@ jobs:
key: ${{ runner.os }}-snapshots-${{ env.targetHash }} key: ${{ runner.os }}-snapshots-${{ env.targetHash }}
- name: Install dependencies - name: Install dependencies
uses: cypress-io/github-action@df7484c5ba85def7eef30db301afa688187bc378 # v6.7.2 uses: cypress-io/github-action@0da3c06ed8217b912deea9d8ee69630baed1737e # v6.7.6
with: with:
runTests: false runTests: false
@@ -117,11 +116,8 @@ jobs:
# Install NPM dependencies, cache them correctly # Install NPM dependencies, cache them correctly
# and run all Cypress tests # and run all Cypress tests
- name: Cypress run - name: Cypress run
uses: cypress-io/github-action@df7484c5ba85def7eef30db301afa688187bc378 # v6.7.2 uses: cypress-io/github-action@0da3c06ed8217b912deea9d8ee69630baed1737e # v6.7.6
id: cypress id: cypress
# If CYPRESS_RECORD_KEY is set, run in parallel on all containers
# Otherwise (e.g. if running from fork), we run on a single container only
if: ${{ env.shouldRunParallel == 'true' || ( matrix.containers == 1 ) }}
with: with:
install: false install: false
start: pnpm run dev:coverage start: pnpm run dev:coverage
@@ -129,16 +125,18 @@ jobs:
browser: chrome browser: chrome
# Disable recording if we don't have an API key # Disable recording if we don't have an API key
# e.g. if this action was run from a fork # e.g. if this action was run from a fork
record: ${{ env.shouldRunParallel == 'true' }} record: ${{ secrets.CYPRESS_RECORD_KEY != '' }}
parallel: ${{ env.shouldRunParallel == 'true' }}
env: env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
VITEST_COVERAGE: true VITEST_COVERAGE: true
CYPRESS_COMMIT: ${{ github.sha }} CYPRESS_COMMIT: ${{ github.sha }}
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }} ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_PARALLEL: ${{ env.shouldRunParallel == 'true' }} ARGOS_PARALLEL: true
ARGOS_PARALLEL_TOTAL: 4 ARGOS_PARALLEL_TOTAL: ${{ strategy.job-total }}
ARGOS_PARALLEL_INDEX: ${{ matrix.containers }} ARGOS_PARALLEL_INDEX: ${{ matrix.containers }}
SPLIT: ${{ strategy.job-total }}
SPLIT_INDEX: ${{ strategy.job-index }}
SPLIT_FILE: 'cypress/timings.json'
- name: Upload Coverage to Codecov - name: Upload Coverage to Codecov
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0

View File

@@ -29,7 +29,7 @@ jobs:
# uses version from "packageManager" field in package.json # uses version from "packageManager" field in package.json
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with: with:
cache: pnpm cache: pnpm
node-version-file: '.node-version' node-version-file: '.node-version'

View File

@@ -28,7 +28,7 @@ jobs:
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with: with:
cache: pnpm cache: pnpm
node-version-file: '.node-version' node-version-file: '.node-version'

View File

@@ -16,7 +16,7 @@ jobs:
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with: with:
cache: pnpm cache: pnpm
node-version-file: '.node-version' node-version-file: '.node-version'

View File

@@ -31,7 +31,7 @@ jobs:
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with: with:
cache: pnpm cache: pnpm
node-version-file: '.node-version' node-version-file: '.node-version'

View File

@@ -26,7 +26,7 @@ jobs:
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with: with:
cache: pnpm cache: pnpm
node-version-file: '.node-version' node-version-file: '.node-version'
@@ -36,7 +36,7 @@ jobs:
- name: Create Release Pull Request or Publish to npm - name: Create Release Pull Request or Publish to npm
id: changesets id: changesets
uses: changesets/action@aba318e9165b45b7948c60273e0b72fce0a64eb9 # v1.4.7 uses: changesets/action@3de3850952bec538fde60aac71731376e57b9b57 # v1.4.8
with: with:
version: pnpm changeset:version version: pnpm changeset:version
publish: pnpm changeset:publish publish: pnpm changeset:publish

View File

@@ -16,11 +16,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with: with:
persist-credentials: false persist-credentials: false
- name: Run analysis - name: Run analysis
uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.3
with: with:
results_file: results.sarif results_file: results.sarif
results_format: sarif results_format: sarif
@@ -32,6 +32,6 @@ jobs:
path: results.sarif path: results.sarif
retention-days: 5 retention-days: 5
- name: Upload to code-scanning - name: Upload to code-scanning
uses: github/codeql-action/upload-sarif@f0f3afee809481da311ca3a6ff1ff51d81dbeb24 # v3.26.4 uses: github/codeql-action/upload-sarif@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
with: with:
sarif_file: results.sarif sarif_file: results.sarif

View File

@@ -15,7 +15,7 @@ jobs:
# uses version from "packageManager" field in package.json # uses version from "packageManager" field in package.json
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with: with:
cache: pnpm cache: pnpm
node-version-file: '.node-version' node-version-file: '.node-version'

View File

@@ -1,8 +1,9 @@
import { defineConfig } from 'cypress';
import { addMatchImageSnapshotPlugin } from 'cypress-image-snapshot/plugin';
import coverage from '@cypress/code-coverage/task';
import eyesPlugin from '@applitools/eyes-cypress'; import eyesPlugin from '@applitools/eyes-cypress';
import { registerArgosTask } from '@argos-ci/cypress/task'; import { registerArgosTask } from '@argos-ci/cypress/task';
import coverage from '@cypress/code-coverage/task';
import { defineConfig } from 'cypress';
import { addMatchImageSnapshotPlugin } from 'cypress-image-snapshot/plugin';
import cypressSplit from 'cypress-split';
export default eyesPlugin( export default eyesPlugin(
defineConfig({ defineConfig({
@@ -13,6 +14,7 @@ export default eyesPlugin(
specPattern: 'cypress/integration/**/*.{js,ts}', specPattern: 'cypress/integration/**/*.{js,ts}',
setupNodeEvents(on, config) { setupNodeEvents(on, config) {
coverage(on, config); coverage(on, config);
cypressSplit(on, config);
on('before:browser:launch', (browser, launchOptions) => { on('before:browser:launch', (browser, launchOptions) => {
if (browser.name === 'chrome' && browser.isHeadless) { if (browser.name === 'chrome' && browser.isHeadless) {
launchOptions.args.push('--window-size=1440,1024', '--force-device-scale-factor=1'); launchOptions.args.push('--window-size=1440,1024', '--force-device-scale-factor=1');

View File

@@ -29,6 +29,7 @@ export const mermaidUrl = (
options: CypressMermaidConfig, options: CypressMermaidConfig,
api: boolean api: boolean
): string => { ): string => {
options.handDrawnSeed = 1;
const codeObject: CodeObject = { const codeObject: CodeObject = {
code: graphStr, code: graphStr,
mermaid: options, mermaid: options,

View File

@@ -173,7 +173,8 @@ describe.skip('architecture diagram', () => {
}); });
}); });
describe('architecture - external', () => { // Skipped as the layout is not deterministic, and causes issues in E2E tests.
describe.skip('architecture - external', () => {
it('should allow adding external icons', () => { it('should allow adding external icons', () => {
urlSnapshotTest('http://localhost:9000/architecture-external.html'); urlSnapshotTest('http://localhost:9000/architecture-external.html');
}); });

View File

@@ -581,4 +581,63 @@ class C13["With Città foreign language"]
{ logLevel: 1, flowchart: { htmlLabels: false } } { logLevel: 1, flowchart: { htmlLabels: false } }
); );
}); });
it('renders a class diagram with a generic class in a namespace', () => {
const diagramDefinition = `
classDiagram-v2
namespace Company.Project.Module {
class GenericClass~T~ {
+addItem(item: T)
+getItem() T
}
}
`;
imgSnapshotTest(diagramDefinition);
});
it('renders a class diagram with nested namespaces and relationships', () => {
const diagramDefinition = `
classDiagram-v2
namespace Company.Project.Module.SubModule {
class Report {
+generatePDF(data: List)
+generateCSV(data: List)
}
}
namespace Company.Project.Module {
class Admin {
+generateReport()
}
}
Admin --> Report : generates
`;
imgSnapshotTest(diagramDefinition);
});
it('renders a class diagram with multiple classes and relationships in a namespace', () => {
const diagramDefinition = `
classDiagram-v2
namespace Company.Project.Module {
class User {
+login(username: String, password: String)
+logout()
}
class Admin {
+addUser(user: User)
+removeUser(user: User)
+generateReport()
}
class Report {
+generatePDF(reportData: List)
+generateCSV(reportData: List)
}
}
Admin --> User : manages
Admin --> Report : generates
`;
imgSnapshotTest(diagramDefinition);
});
}); });

View File

@@ -12,7 +12,6 @@ describe('Flowchart HandDrawn', () => {
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
flowchart: { htmlLabels: false }, flowchart: { htmlLabels: false },
fontFamily: 'courier', fontFamily: 'courier',
} }
@@ -30,7 +29,6 @@ describe('Flowchart HandDrawn', () => {
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
flowchart: { htmlLabels: true }, flowchart: { htmlLabels: true },
fontFamily: 'courier', fontFamily: 'courier',
} }
@@ -47,7 +45,7 @@ describe('Flowchart HandDrawn', () => {
C -->|Two| E[iPhone] C -->|Two| E[iPhone]
C -->|Three| F[Car] C -->|Three| F[Car]
`, `,
{ look: 'handDrawn', handDrawnSeed: 1, fontFamily: 'courier' } { look: 'handDrawn', fontFamily: 'courier' }
); );
}); });
@@ -62,7 +60,7 @@ describe('Flowchart HandDrawn', () => {
C -->|Two| E[\\iPhone\\] C -->|Two| E[\\iPhone\\]
C -->|Three| F[Car] C -->|Three| F[Car]
`, `,
{ look: 'handDrawn', handDrawnSeed: 1, fontFamily: 'courier' } { look: 'handDrawn', fontFamily: 'courier' }
); );
}); });
@@ -78,7 +76,7 @@ describe('Flowchart HandDrawn', () => {
classDef processHead fill:#888888,color:white,font-weight:bold,stroke-width:3px,stroke:#001f3f classDef processHead fill:#888888,color:white,font-weight:bold,stroke-width:3px,stroke:#001f3f
class 1A,1B,D,E processHead class 1A,1B,D,E processHead
`, `,
{ look: 'handDrawn', handDrawnSeed: 1, fontFamily: 'courier' } { look: 'handDrawn', fontFamily: 'courier' }
); );
}); });
@@ -107,7 +105,7 @@ describe('Flowchart HandDrawn', () => {
35(SAM.CommonFA.PopulationFME)-->39(SAM.CommonFA.ChargeDetails) 35(SAM.CommonFA.PopulationFME)-->39(SAM.CommonFA.ChargeDetails)
36(SAM.CommonFA.PremetricCost)-->39(SAM.CommonFA.ChargeDetails) 36(SAM.CommonFA.PremetricCost)-->39(SAM.CommonFA.ChargeDetails)
`, `,
{ look: 'handDrawn', handDrawnSeed: 1, fontFamily: 'courier' } { look: 'handDrawn', fontFamily: 'courier' }
); );
}); });
@@ -178,7 +176,7 @@ describe('Flowchart HandDrawn', () => {
9a072290_1ec3_e711_8c5a_005056ad0002-->d6072290_1ec3_e711_8c5a_005056ad0002 9a072290_1ec3_e711_8c5a_005056ad0002-->d6072290_1ec3_e711_8c5a_005056ad0002
9a072290_1ec3_e711_8c5a_005056ad0002-->71082290_1ec3_e711_8c5a_005056ad0002 9a072290_1ec3_e711_8c5a_005056ad0002-->71082290_1ec3_e711_8c5a_005056ad0002
`, `,
{ look: 'handDrawn', handDrawnSeed: 1, fontFamily: 'courier' } { look: 'handDrawn', fontFamily: 'courier' }
); );
}); });
@@ -187,7 +185,7 @@ describe('Flowchart HandDrawn', () => {
` `
graph TB;subgraph "number as labels";1;end; graph TB;subgraph "number as labels";1;end;
`, `,
{ look: 'handDrawn', handDrawnSeed: 1, fontFamily: 'courier' } { look: 'handDrawn', fontFamily: 'courier' }
); );
}); });
@@ -199,7 +197,7 @@ describe('Flowchart HandDrawn', () => {
a1-->a2 a1-->a2
end end
`, `,
{ look: 'handDrawn', handDrawnSeed: 1, fontFamily: 'courier' } { look: 'handDrawn', fontFamily: 'courier' }
); );
}); });
@@ -211,7 +209,7 @@ describe('Flowchart HandDrawn', () => {
a1-->a2 a1-->a2
end end
`, `,
{ look: 'handDrawn', handDrawnSeed: 1, fontFamily: 'courier' } { look: 'handDrawn', fontFamily: 'courier' }
); );
}); });
@@ -246,7 +244,7 @@ describe('Flowchart HandDrawn', () => {
style foo fill:#F99,stroke-width:2px,stroke:#F0F,color:darkred style foo fill:#F99,stroke-width:2px,stroke:#F0F,color:darkred
style bar fill:#999,stroke-width:10px,stroke:#0F0,color:blue style bar fill:#999,stroke-width:10px,stroke:#0F0,color:blue
`, `,
{ look: 'handDrawn', handDrawnSeed: 1, fontFamily: 'courier' } { look: 'handDrawn', fontFamily: 'courier' }
); );
}); });
@@ -348,7 +346,7 @@ describe('Flowchart HandDrawn', () => {
sid-7CE72B24-E0C1-46D3-8132-8BA66BE05AA7-->sid-4DA958A0-26D9-4D47-93A7-70F39FD7D51A; sid-7CE72B24-E0C1-46D3-8132-8BA66BE05AA7-->sid-4DA958A0-26D9-4D47-93A7-70F39FD7D51A;
sid-7CE72B24-E0C1-46D3-8132-8BA66BE05AA7-->sid-4FC27B48-A6F9-460A-A675-021F5854FE22; sid-7CE72B24-E0C1-46D3-8132-8BA66BE05AA7-->sid-4FC27B48-A6F9-460A-A675-021F5854FE22;
`, `,
{ look: 'handDrawn', handDrawnSeed: 1, fontFamily: 'courier' } { look: 'handDrawn', fontFamily: 'courier' }
); );
}); });
@@ -364,7 +362,6 @@ describe('Flowchart HandDrawn', () => {
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
listUrl: false, listUrl: false,
listId: 'color styling', listId: 'color styling',
fontFamily: 'courier', fontFamily: 'courier',
@@ -390,7 +387,6 @@ describe('Flowchart HandDrawn', () => {
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
listUrl: false, listUrl: false,
listId: 'color styling', listId: 'color styling',
fontFamily: 'courier', fontFamily: 'courier',
@@ -411,7 +407,6 @@ describe('Flowchart HandDrawn', () => {
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
flowchart: { htmlLabels: false }, flowchart: { htmlLabels: false },
fontFamily: 'courier', fontFamily: 'courier',
} }
@@ -435,7 +430,6 @@ describe('Flowchart HandDrawn', () => {
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
flowchart: { htmlLabels: false }, flowchart: { htmlLabels: false },
fontFamily: 'courier', fontFamily: 'courier',
} }
@@ -457,7 +451,6 @@ describe('Flowchart HandDrawn', () => {
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
flowchart: { htmlLabels: false }, flowchart: { htmlLabels: false },
fontFamily: 'courier', fontFamily: 'courier',
} }
@@ -471,7 +464,6 @@ describe('Flowchart HandDrawn', () => {
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
flowchart: { htmlLabels: false }, flowchart: { htmlLabels: false },
fontFamily: 'courier', fontFamily: 'courier',
} }
@@ -485,7 +477,6 @@ describe('Flowchart HandDrawn', () => {
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
flowchart: { htmlLabels: false }, flowchart: { htmlLabels: false },
fontFamily: 'courier', fontFamily: 'courier',
} }
@@ -500,7 +491,6 @@ describe('Flowchart HandDrawn', () => {
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
flowchart: { htmlLabels: false }, flowchart: { htmlLabels: false },
fontFamily: 'courier', fontFamily: 'courier',
} }
@@ -527,7 +517,6 @@ describe('Flowchart HandDrawn', () => {
class A someclass;`, class A someclass;`,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
flowchart: { htmlLabels: false }, flowchart: { htmlLabels: false },
fontFamily: 'courier', fontFamily: 'courier',
} }
@@ -544,7 +533,7 @@ describe('Flowchart HandDrawn', () => {
C -->|Two| E[iPhone] C -->|Two| E[iPhone]
C -->|Three| F[fa:fa-car Car] C -->|Three| F[fa:fa-car Car]
`, `,
{ look: 'handDrawn', handDrawnSeed: 1, flowchart: { nodeSpacing: 50 }, fontFamily: 'courier' } { look: 'handDrawn', flowchart: { nodeSpacing: 50 }, fontFamily: 'courier' }
); );
}); });
@@ -560,7 +549,6 @@ describe('Flowchart HandDrawn', () => {
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
flowchart: { rankSpacing: '100' }, flowchart: { rankSpacing: '100' },
fontFamily: 'courier', fontFamily: 'courier',
} }
@@ -578,7 +566,6 @@ describe('Flowchart HandDrawn', () => {
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
flowchart: { htmlLabels: false }, flowchart: { htmlLabels: false },
fontFamily: 'courier', fontFamily: 'courier',
} }
@@ -603,7 +590,7 @@ describe('Flowchart HandDrawn', () => {
click E "notes://do-your-thing/id" "other protocol test" click E "notes://do-your-thing/id" "other protocol test"
click F "javascript:alert('test')" "script test" click F "javascript:alert('test')" "script test"
`, `,
{ look: 'handDrawn', handDrawnSeed: 1, securityLevel: 'loose', fontFamily: 'courier' } { look: 'handDrawn', securityLevel: 'loose', fontFamily: 'courier' }
); );
}); });
@@ -623,7 +610,7 @@ describe('Flowchart HandDrawn', () => {
click B "index.html#link-clicked" "link test" click B "index.html#link-clicked" "link test"
click D testClick "click test" click D testClick "click test"
`, `,
{ look: 'handDrawn', handDrawnSeed: 1, flowchart: { htmlLabels: true } } { look: 'handDrawn', flowchart: { htmlLabels: true } }
); );
}); });
@@ -645,7 +632,6 @@ describe('Flowchart HandDrawn', () => {
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
flowchart: { htmlLabels: false }, flowchart: { htmlLabels: false },
fontFamily: 'courier', fontFamily: 'courier',
} }
@@ -664,7 +650,7 @@ describe('Flowchart HandDrawn', () => {
class A myClass1 class A myClass1
class D myClass2 class D myClass2
`, `,
{ look: 'handDrawn', handDrawnSeed: 1, flowchart: { htmlLabels: true } } { look: 'handDrawn', flowchart: { htmlLabels: true } }
); );
}); });
@@ -682,7 +668,6 @@ describe('Flowchart HandDrawn', () => {
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
flowchart: { htmlLabels: false }, flowchart: { htmlLabels: false },
fontFamily: 'courier', fontFamily: 'courier',
} }
@@ -711,7 +696,6 @@ describe('Flowchart HandDrawn', () => {
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
flowchart: { htmlLabels: false }, flowchart: { htmlLabels: false },
fontFamily: 'courier', fontFamily: 'courier',
} }
@@ -728,7 +712,6 @@ describe('Flowchart HandDrawn', () => {
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
flowchart: { htmlLabels: false }, flowchart: { htmlLabels: false },
fontFamily: 'courier', fontFamily: 'courier',
} }
@@ -752,7 +735,6 @@ describe('Flowchart HandDrawn', () => {
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
flowchart: { htmlLabels: false }, flowchart: { htmlLabels: false },
fontFamily: 'courier', fontFamily: 'courier',
} }
@@ -769,7 +751,7 @@ describe('Flowchart HandDrawn', () => {
C -->|Two| E[iPhone] C -->|Two| E[iPhone]
C -->|Three| F[fa:fa-car Car] C -->|Three| F[fa:fa-car Car]
`, `,
{ look: 'handDrawn', handDrawnSeed: 1, flowchart: { diagramPadding: 0 } } { look: 'handDrawn', flowchart: { diagramPadding: 0 } }
); );
}); });
@@ -804,7 +786,7 @@ describe('Flowchart HandDrawn', () => {
`graph TD `graph TD
a["<strong>Haiya</strong>"]-->b a["<strong>Haiya</strong>"]-->b
`, `,
{ look: 'handDrawn', handDrawnSeed: 1, htmlLabels: false, flowchart: { htmlLabels: false } } { look: 'handDrawn', htmlLabels: false, flowchart: { htmlLabels: false } }
); );
}); });
it('FDH37: should render non-escaped with html labels', () => { it('FDH37: should render non-escaped with html labels', () => {
@@ -814,7 +796,6 @@ describe('Flowchart HandDrawn', () => {
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
htmlLabels: true, htmlLabels: true,
flowchart: { htmlLabels: true }, flowchart: { htmlLabels: true },
securityLevel: 'loose', securityLevel: 'loose',
@@ -830,7 +811,7 @@ describe('Flowchart HandDrawn', () => {
C -->|Two| E[iPhone] C -->|Two| E[iPhone]
C -->|Three| F[fa:fa-car Car] C -->|Three| F[fa:fa-car Car]
`, `,
{ look: 'handDrawn', handDrawnSeed: 1, flowchart: { useMaxWidth: true } } { look: 'handDrawn', flowchart: { useMaxWidth: true } }
); );
cy.get('svg').should((svg) => { cy.get('svg').should((svg) => {
expect(svg).to.have.attr('width', '100%'); expect(svg).to.have.attr('width', '100%');
@@ -853,7 +834,7 @@ describe('Flowchart HandDrawn', () => {
C -->|Two| E[iPhone] C -->|Two| E[iPhone]
C -->|Three| F[fa:fa-car Car] C -->|Three| F[fa:fa-car Car]
`, `,
{ look: 'handDrawn', handDrawnSeed: 1, flowchart: { useMaxWidth: false } } { look: 'handDrawn', flowchart: { useMaxWidth: false } }
); );
cy.get('svg').should((svg) => { cy.get('svg').should((svg) => {
// const height = parseFloat(svg.attr('height')); // const height = parseFloat(svg.attr('height'));
@@ -874,7 +855,6 @@ describe('Flowchart HandDrawn', () => {
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
htmlLabels: true, htmlLabels: true,
flowchart: { htmlLabels: true }, flowchart: { htmlLabels: true },
securityLevel: 'loose', securityLevel: 'loose',
@@ -904,7 +884,6 @@ describe('Flowchart HandDrawn', () => {
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
htmlLabels: true, htmlLabels: true,
flowchart: { htmlLabels: true }, flowchart: { htmlLabels: true },
securityLevel: 'loose', securityLevel: 'loose',
@@ -919,7 +898,6 @@ graph TD
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
htmlLabels: true, htmlLabels: true,
flowchart: { htmlLabels: true }, flowchart: { htmlLabels: true },
securityLevel: 'loose', securityLevel: 'loose',
@@ -937,7 +915,6 @@ graph TD
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
htmlLabels: true, htmlLabels: true,
flowchart: { htmlLabels: true }, flowchart: { htmlLabels: true },
securityLevel: 'loose', securityLevel: 'loose',
@@ -977,7 +954,6 @@ graph TD
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
htmlLabels: true, htmlLabels: true,
flowchart: { htmlLabels: true }, flowchart: { htmlLabels: true },
securityLevel: 'loose', securityLevel: 'loose',
@@ -999,7 +975,6 @@ graph TD
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
htmlLabels: true, htmlLabels: true,
flowchart: { htmlLabels: true }, flowchart: { htmlLabels: true },
securityLevel: 'loose', securityLevel: 'loose',
@@ -1016,7 +991,6 @@ graph TD
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
htmlLabels: true, htmlLabels: true,
flowchart: { htmlLabels: true }, flowchart: { htmlLabels: true },
securityLevel: 'loose', securityLevel: 'loose',
@@ -1032,7 +1006,6 @@ graph TD
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
htmlLabels: true, htmlLabels: true,
flowchart: { htmlLabels: true }, flowchart: { htmlLabels: true },
securityLevel: 'loose', securityLevel: 'loose',
@@ -1051,7 +1024,6 @@ graph TD
`, `,
{ {
look: 'handDrawn', look: 'handDrawn',
handDrawnSeed: 1,
flowchart: { htmlLabels: true }, flowchart: { htmlLabels: true },
securityLevel: 'loose', securityLevel: 'loose',
} }

View File

@@ -0,0 +1,142 @@
import { imgSnapshotTest } from '../../helpers/util.ts';
const aliasSet1 = ['process', 'rect', 'proc', 'rectangle'] as const;
const aliasSet2 = ['event', 'rounded'] as const;
const aliasSet3 = ['stadium', 'pill', 'terminal'] as const;
const aliasSet4 = ['fr-rect', 'subproc', 'subprocess', 'framed-rectangle', 'subroutine'] as const;
const aliasSet5 = ['db', 'database', 'cylinder', 'cyl'] as const;
const aliasSet6 = ['diam', 'decision', 'diamond'] as const;
const aliasSet7 = ['hex', 'hexagon', 'prepare'] as const;
const aliasSet8 = ['lean-r', 'lean-right', 'in-out'] as const;
const aliasSet9 = ['lean-l', 'lean-left', 'out-in'] as const;
const aliasSet10 = ['trap-b', 'trapezoid-bottom', 'priority'] as const;
const aliasSet11 = ['trap-t', 'trapezoid-top', 'manual'] as const;
const aliasSet12 = ['dbl-circ', 'double-circle'] as const;
const aliasSet13 = ['notched-rectangle', 'card', 'notch-rect'] as const;
const aliasSet14 = [
'lin-rect',
'lined-rectangle',
'lin-proc',
'lined-process',
'shaded-process',
] as const;
const aliasSet15 = ['sm-circ', 'small-circle', 'start'] as const;
const aliasSet16 = ['fr-circ', 'framed-circle', 'stop'] as const;
const aliasSet17 = ['fork', 'join'] as const;
// brace-r', 'braces'
const aliasSet18 = ['comment', 'brace-l'] as const;
const aliasSet19 = ['bolt', 'com-link', 'lightning-bolt'] as const;
const aliasSet20 = ['doc', 'document'] as const;
const aliasSet21 = ['delay', 'half-rounded-rectangle'] as const;
const aliasSet22 = ['h-cyl', 'das', 'horizontal-cylinder'] as const;
const aliasSet23 = ['lin-cyl', 'disk', 'lined-cylinder'] as const;
const aliasSet24 = ['curv-trap', 'display', 'curved-trapezoid'] as const;
const aliasSet25 = ['div-rect', 'div-proc', 'divided-rectangle', 'divided-process'] as const;
const aliasSet26 = ['extract', 'tri', 'triangle'] as const;
const aliasSet27 = ['win-pane', 'internal-storage', 'window-pane'] as const;
const aliasSet28 = ['f-circ', 'junction', 'filled-circle'] as const;
const aliasSet29 = ['lin-doc', 'lined-document'] as const;
const aliasSet30 = ['notch-pent', 'loop-limit', 'notched-pentagon'] as const;
const aliasSet31 = ['flip-tri', 'manual-file', 'flipped-triangle'] as const;
const aliasSet32 = ['sl-rect', 'manual-input', 'sloped-rectangle'] as const;
const aliasSet33 = ['docs', 'documents', 'st-doc', 'stacked-document'] as const;
const aliasSet34 = ['procs', 'processes', 'st-rect', 'stacked-rectangle'] as const;
const aliasSet35 = ['flag', 'paper-tape'] as const;
const aliasSet36 = ['bow-rect', 'stored-data', 'bow-tie-rectangle'] as const;
const aliasSet37 = ['cross-circ', 'summary', 'crossed-circle'] as const;
const aliasSet38 = ['tag-doc', 'tagged-document'] as const;
const aliasSet39 = ['tag-rect', 'tag-proc', 'tagged-rectangle', 'tagged-process'] as const;
const aliasSet40 = ['collate', 'hourglass'] as const;
// Aggregate all alias sets into a single array
const aliasSets = [
aliasSet1,
aliasSet2,
aliasSet3,
aliasSet4,
aliasSet5,
aliasSet6,
aliasSet7,
aliasSet8,
aliasSet9,
aliasSet10,
aliasSet11,
aliasSet12,
aliasSet13,
aliasSet14,
aliasSet15,
aliasSet16,
aliasSet17,
aliasSet18,
aliasSet19,
aliasSet20,
aliasSet21,
aliasSet22,
aliasSet23,
aliasSet24,
aliasSet25,
aliasSet26,
aliasSet27,
aliasSet28,
aliasSet29,
aliasSet30,
aliasSet31,
aliasSet32,
aliasSet33,
aliasSet34,
aliasSet35,
aliasSet36,
aliasSet37,
aliasSet38,
aliasSet39,
] as const;
aliasSets.forEach((aliasSet) => {
describe(`Test ${aliasSet.join(',')} `, () => {
it(`All ${aliasSet.join(',')} should render same shape`, () => {
let flowchartCode = `flowchart \n`;
aliasSet.forEach((alias, index) => {
flowchartCode += ` n${index}@{ shape: ${alias}, label: "${alias}" }\n`;
});
imgSnapshotTest(flowchartCode);
});
});
});

View File

@@ -0,0 +1,126 @@
import { imgSnapshotTest } from '../../helpers/util';
const looks = ['classic', 'handDrawn'] as const;
const directions = [
'TB',
//'BT',
'LR',
// 'RL'
] as const;
const forms = [undefined, 'square', 'circle', 'rounded'] as const;
const labelPos = [undefined, 't', 'b'] as const;
looks.forEach((look) => {
directions.forEach((direction) => {
forms.forEach((form) => {
labelPos.forEach((pos) => {
describe(`Test iconShape in ${form ? `${form} form,` : ''} ${look} look and dir ${direction} with label position ${pos ? pos : 'not defined'}`, () => {
it(`without label`, () => {
let flowchartCode = `flowchart ${direction}\n`;
flowchartCode += ` nA --> nAA@{ icon: 'fa:bell'`;
if (form) {
flowchartCode += `, form: '${form}'`;
}
flowchartCode += ` }\n`;
imgSnapshotTest(flowchartCode, { look });
});
it(`with label`, () => {
let flowchartCode = `flowchart ${direction}\n`;
flowchartCode += ` nA --> nAA@{ icon: 'fa:bell', label: 'This is a label for icon shape'`;
if (form) {
flowchartCode += `, form: '${form}'`;
}
if (pos) {
flowchartCode += `, pos: '${pos}'`;
}
flowchartCode += ` }\n`;
imgSnapshotTest(flowchartCode, { look });
});
it(`with very long label`, () => {
let flowchartCode = `flowchart ${direction}\n`;
flowchartCode += ` nA --> nAA@{ icon: 'fa:bell', label: 'This is a very very very very very long long long label for icon shape'`;
if (form) {
flowchartCode += `, form: '${form}'`;
}
if (pos) {
flowchartCode += `, pos: '${pos}'`;
}
flowchartCode += ` }\n`;
imgSnapshotTest(flowchartCode, { look });
});
it(`with markdown htmlLabels:true`, () => {
let flowchartCode = `flowchart ${direction}\n`;
flowchartCode += ` nA --> nAA@{ icon: 'fa:bell', label: 'This is **bold** </br>and <strong>strong</strong> for icon shape'`;
if (form) {
flowchartCode += `, form: '${form}'`;
}
if (pos) {
flowchartCode += `, pos: '${pos}'`;
}
flowchartCode += ` }\n`;
imgSnapshotTest(flowchartCode, { look });
});
it(`with markdown htmlLabels:false`, () => {
let flowchartCode = `flowchart ${direction}\n`;
flowchartCode += ` nA --> nAA@{ icon: 'fa:bell', label: 'This is **bold** </br>and <strong>strong</strong> for icon shape'`;
if (form) {
flowchartCode += `, form: '${form}'`;
}
if (pos) {
flowchartCode += `, pos: '${pos}'`;
}
flowchartCode += ` }\n`;
imgSnapshotTest(flowchartCode, {
look,
htmlLabels: false,
flowchart: { htmlLabels: false },
});
});
it(`with styles`, () => {
let flowchartCode = `flowchart ${direction}\n`;
flowchartCode += ` nA --> nAA@{ icon: 'fa:bell', label: 'new icon shape'`;
if (form) {
flowchartCode += `, form: '${form}'`;
}
if (pos) {
flowchartCode += `, pos: '${pos}'`;
}
flowchartCode += ` }\n`;
flowchartCode += ` style nAA fill:#f9f,stroke:#333,stroke-width:4px \n`;
imgSnapshotTest(flowchartCode, { look });
});
it(`with classDef`, () => {
let flowchartCode = `flowchart ${direction}\n`;
flowchartCode += ` classDef customClazz fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5\n`;
flowchartCode += ` nA --> nAA@{ icon: 'fa:bell', label: 'new icon shape'`;
if (form) {
flowchartCode += `, form: '${form}'`;
}
if (pos) {
flowchartCode += `, pos: '${pos}'`;
}
flowchartCode += ` }\n`;
flowchartCode += ` nAA:::customClazz\n`;
imgSnapshotTest(flowchartCode, { look });
});
});
});
});
});
});
describe('Test iconShape with different h', () => {
it('with different h', () => {
let flowchartCode = `flowchart TB\n`;
const icon = 'fa:bell';
const iconHeight = 64;
flowchartCode += ` nA --> nAA@{ icon: '${icon}', label: 'icon with different h', h: ${iconHeight} }\n`;
imgSnapshotTest(flowchartCode);
});
});

View File

@@ -0,0 +1,103 @@
import { imgSnapshotTest } from '../../helpers/util';
const looks = ['classic', 'handDrawn'] as const;
const directions = [
'TB',
//'BT',
'LR',
// 'RL'
] as const;
const labelPos = [undefined, 't', 'b'] as const;
looks.forEach((look) => {
directions.forEach((direction) => {
labelPos.forEach((pos) => {
describe(`Test imageShape in ${look} look and dir ${direction} with label position ${pos ? pos : 'not defined'}`, () => {
it(`without label`, () => {
let flowchartCode = `flowchart ${direction}\n`;
flowchartCode += ` nA --> A@{ img: 'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg', w: '100', h: '100' }\n`;
imgSnapshotTest(flowchartCode, { look });
});
it(`with label`, () => {
let flowchartCode = `flowchart ${direction}\n`;
flowchartCode += ` nA --> A@{ img: 'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg', label: 'This is a label for image shape'`;
flowchartCode += `, w: '100', h: '200'`;
if (pos) {
flowchartCode += `, pos: '${pos}'`;
}
flowchartCode += ` }\n`;
imgSnapshotTest(flowchartCode, { look });
});
it(`with very long label`, () => {
let flowchartCode = `flowchart ${direction}\n`;
flowchartCode += ` nA --> A@{ img: 'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg', label: 'This is a very very very very very long long long label for image shape'`;
flowchartCode += `, w: '100', h: '250'`;
if (pos) {
flowchartCode += `, pos: '${pos}'`;
}
flowchartCode += ` }\n`;
imgSnapshotTest(flowchartCode, { look });
});
it(`with markdown htmlLabels:true`, () => {
let flowchartCode = `flowchart ${direction}\n`;
flowchartCode += ` nA --> A@{ img: 'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg', label: 'This is **bold** </br>and <strong>strong</strong> for image shape'`;
flowchartCode += `, w: '550', h: '200'`;
if (pos) {
flowchartCode += `, pos: '${pos}'`;
}
flowchartCode += ` }\n`;
imgSnapshotTest(flowchartCode, { look, htmlLabels: true });
});
it(`with markdown htmlLabels:false`, () => {
let flowchartCode = `flowchart ${direction}\n`;
flowchartCode += ` nA --> A@{ img: 'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg', label: 'This is **bold** </br>and <strong>strong</strong> for image shape'`;
flowchartCode += `, w: '250', h: '200'`;
if (pos) {
flowchartCode += `, pos: '${pos}'`;
}
flowchartCode += ` }\n`;
imgSnapshotTest(flowchartCode, {
look,
htmlLabels: false,
flowchart: { htmlLabels: false },
});
});
it(`with styles`, () => {
let flowchartCode = `flowchart ${direction}\n`;
flowchartCode += ` nA --> A@{ img: 'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg', label: 'new image shape'`;
flowchartCode += `, w: '550', h: '200'`;
if (pos) {
flowchartCode += `, pos: '${pos}'`;
}
flowchartCode += ` }\n`;
flowchartCode += ` style A fill:#f9f,stroke:#333,stroke-width:4px \n`;
imgSnapshotTest(flowchartCode, { look });
});
it(`with classDef`, () => {
let flowchartCode = `flowchart ${direction}\n`;
flowchartCode += ` classDef customClazz fill:#bbf,stroke:#f66,stroke-width:2px,color:#000000,stroke-dasharray: 5 5\n`;
flowchartCode += ` nA --> A@{ img: 'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg', label: 'new image shape'`;
flowchartCode += `, w: '500', h: '550'`;
if (pos) {
flowchartCode += `, pos: '${pos}'`;
}
flowchartCode += ` }\n`;
flowchartCode += ` A:::customClazz\n`;
imgSnapshotTest(flowchartCode, { look });
});
});
});
});
});

View File

@@ -0,0 +1,146 @@
import { imgSnapshotTest } from '../../helpers/util.ts';
const looks = ['classic', 'handDrawn'] as const;
const directions = [
'TB',
//'BT',
'LR',
//'RL'
] as const;
const newShapesSet1 = [
'triangle',
'sloped-rectangle',
'horizontal-cylinder',
'flipped-triangle',
'hourglass',
] as const;
const newShapesSet2 = [
'tagged-rectangle',
'documents',
'lightning-bolt',
'filled-circle',
'window-pane',
] as const;
const newShapesSet3 = [
'curved-trapezoid',
'bow-rect',
'tagged-document',
'divided-rectangle',
'crossed-circle',
] as const;
const newShapesSet4 = [
'document',
'notched-pentagon',
'lined-cylinder',
'stacked-document',
'half-rounded-rectangle',
] as const;
const newShapesSet5 = [
'lined-document',
'tagged-document',
'brace-l',
'comment',
'braces',
'brace-r',
] as const;
const newShapesSet6 = ['brace-r', 'braces'] as const;
// Aggregate all shape sets into a single array
const newShapesSets = [
newShapesSet1,
newShapesSet2,
newShapesSet3,
newShapesSet4,
newShapesSet5,
newShapesSet6,
];
looks.forEach((look) => {
directions.forEach((direction) => {
newShapesSets.forEach((newShapesSet) => {
describe(`Test ${newShapesSet.join(', ')} in ${look} look and dir ${direction}`, () => {
it(`without label`, () => {
let flowchartCode = `flowchart ${direction}\n`;
newShapesSet.forEach((newShape, index) => {
flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape} }\n`;
});
imgSnapshotTest(flowchartCode, { look });
});
it(`with label`, () => {
let flowchartCode = `flowchart ${direction}\n`;
newShapesSet.forEach((newShape, index) => {
flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape}, label: 'This is a label for ${newShape} shape' }\n`;
});
imgSnapshotTest(flowchartCode, { look });
});
it(`connect all shapes with each other`, () => {
let flowchartCode = `flowchart ${direction}\n`;
newShapesSet.forEach((newShape, index) => {
flowchartCode += ` n${index}${index}@{ shape: ${newShape}, label: 'This is a label for ${newShape} shape' }\n`;
});
for (let i = 0; i < newShapesSet.length; i++) {
for (let j = i + 1; j < newShapesSet.length; j++) {
flowchartCode += ` n${i}${i} --> n${j}${j}\n`;
}
}
if (!(direction === 'TB' && look === 'handDrawn' && newShapesSet === newShapesSet1)) {
//skip this test, works in real. Need to look
imgSnapshotTest(flowchartCode, { look });
}
});
it(`with very long label`, () => {
let flowchartCode = `flowchart ${direction}\n`;
newShapesSet.forEach((newShape, index) => {
flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape}, label: 'This is a very very very very very long long long label for ${newShape} shape' }\n`;
});
imgSnapshotTest(flowchartCode, { look });
});
it(`with markdown htmlLabels:true`, () => {
let flowchartCode = `flowchart ${direction}\n`;
newShapesSet.forEach((newShape, index) => {
flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape}, label: 'This is **bold** </br>and <strong>strong</strong> for ${newShape} shape' }\n`;
});
imgSnapshotTest(flowchartCode, { look });
});
it(`with markdown htmlLabels:false`, () => {
let flowchartCode = `flowchart ${direction}\n`;
newShapesSet.forEach((newShape, index) => {
flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape}, label: 'This is **bold** </br>and <strong>strong</strong> for ${newShape} shape' }\n`;
});
imgSnapshotTest(flowchartCode, {
look,
htmlLabels: false,
flowchart: { htmlLabels: false },
});
});
it(`with styles`, () => {
let flowchartCode = `flowchart ${direction}\n`;
newShapesSet.forEach((newShape, index) => {
flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape}, label: 'new ${newShape} shape' }\n`;
flowchartCode += ` style n${index}${index} fill:#f9f,stroke:#333,stroke-width:4px \n`;
});
imgSnapshotTest(flowchartCode, { look });
});
it(`with classDef`, () => {
let flowchartCode = `flowchart ${direction}\n`;
flowchartCode += ` classDef customClazz fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5\n`;
newShapesSet.forEach((newShape, index) => {
flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape}, label: 'new ${newShape} shape' }\n`;
flowchartCode += ` n${index}${index}:::customClazz\n`;
});
imgSnapshotTest(flowchartCode, { look });
});
});
});
});
});

View File

@@ -0,0 +1,107 @@
import { imgSnapshotTest } from '../../helpers/util';
const looks = ['classic', 'handDrawn'] as const;
const directions = [
'TB',
//'BT',
'LR',
//'RL'
] as const;
const shapesSet1 = ['text', 'card', 'lin-rect', 'diamond', 'hexagon'] as const;
// removing labelRect, need have alias for it
const shapesSet2 = ['rounded', 'rect', 'start', 'stop'] as const;
const shapesSet3 = ['fork', 'choice', 'note', 'stadium', 'odd'] as const;
const shapesSet4 = ['subroutine', 'cylinder', 'circle', 'doublecircle', 'odd'] as const;
const shapesSet5 = ['anchor', 'lean-r', 'lean-l', 'trap-t', 'trap-b'] as const;
// Aggregate all shape sets into a single array
const shapesSets = [shapesSet1, shapesSet2, shapesSet3, shapesSet4, shapesSet5] as const;
looks.forEach((look) => {
directions.forEach((direction) => {
shapesSets.forEach((shapesSet) => {
describe(`Test ${shapesSet.join(', ')} in ${look} look and dir ${direction}`, () => {
it(`without label`, () => {
let flowchartCode = `flowchart ${direction}\n`;
shapesSet.forEach((newShape, index) => {
flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape} }\n`;
});
imgSnapshotTest(flowchartCode, { look });
});
it(`with label`, () => {
let flowchartCode = `flowchart ${direction}\n`;
shapesSet.forEach((newShape, index) => {
flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape}, label: 'This is a label for ${newShape} shape' }\n`;
});
imgSnapshotTest(flowchartCode, { look });
});
it(`connect all shapes with each other`, () => {
let flowchartCode = `flowchart ${direction}\n`;
shapesSet.forEach((newShape, index) => {
flowchartCode += ` n${index}${index}@{ shape: ${newShape}, label: 'This is a label for ${newShape} shape' }\n`;
});
for (let i = 0; i < shapesSet.length; i++) {
for (let j = i + 1; j < shapesSet.length; j++) {
flowchartCode += ` n${i}${i} --> n${j}${j}\n`;
}
}
imgSnapshotTest(flowchartCode, { look });
});
it(`with very long label`, () => {
let flowchartCode = `flowchart ${direction}\n`;
shapesSet.forEach((newShape, index) => {
flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape}, label: 'This is a very very very very very long long long label for ${newShape} shape' }\n`;
});
imgSnapshotTest(flowchartCode, { look });
});
it(`with markdown htmlLabels:true`, () => {
let flowchartCode = `flowchart ${direction}\n`;
shapesSet.forEach((newShape, index) => {
flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape}, label: 'This is **bold** </br>and <strong>strong</strong> for ${newShape} shape' }\n`;
});
imgSnapshotTest(flowchartCode, { look });
});
it(`with markdown htmlLabels:false`, () => {
let flowchartCode = `flowchart ${direction}\n`;
shapesSet.forEach((newShape, index) => {
flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape}, label: 'This is **bold** </br>and <strong>strong</strong> for ${newShape} shape' }\n`;
});
imgSnapshotTest(flowchartCode, {
look,
htmlLabels: false,
flowchart: { htmlLabels: false },
});
});
it(`with styles`, () => {
let flowchartCode = `flowchart ${direction}\n`;
shapesSet.forEach((newShape, index) => {
flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape}, label: 'new ${newShape} shape' }\n`;
flowchartCode += ` style n${index}${index} fill:#f9f,stroke:#333,stroke-width:4px \n`;
});
imgSnapshotTest(flowchartCode, { look });
});
it(`with classDef`, () => {
let flowchartCode = `flowchart ${direction}\n`;
flowchartCode += ` classDef customClazz fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5\n`;
shapesSet.forEach((newShape, index) => {
flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape}, label: 'new ${newShape} shape' }\n`;
flowchartCode += ` n${index}${index}:::customClazz\n`;
});
imgSnapshotTest(flowchartCode, { look });
});
});
});
});
});

View File

@@ -83,7 +83,8 @@
</head> </head>
<body> <body>
<pre id="diagram" class="mermaid"> <div class="flex">
<pre id="diagram" class="mermaid2">
--- ---
title: hello2 title: hello2
config: config:
@@ -242,8 +243,190 @@ stateDiagram-v2
</pre> </pre>
<pre id="diagram" class="mermaid2">
---
config:
look: neo
---
flowchart RL
subgraph " "
A5@{ shape: manual-file, label: "a label"}
B5@{ shape: manual-input, label: "a label" }
C5@{ shape: mul-doc, label: "a label" }
D5@{ shape: mul-proc, label: "a label" }
E5@{ shape: paper-tape, label: "a label" }
B3@{ shape: das, label: "a label" }
C3@{ shape: disk, label: "a label" }
D4@{ shape: lin-doc, label: "a label" }
E4@{ shape: loop-limit, label: "a label" }
end
subgraph " "
B6@{ shape: summary, label: "a label" }
C6@{ shape: tag-we-rect, label: "a label" }
D6@{ shape: tag-rect, label: "a label" }
A2@{ shape: fork}
B2@{ shape: hourglass }
C2@{ shape: comment, label: "I am a comment" }
D2@{ shape: bolt }
D3@{ shape: disp, label: "a label" }
C4@{ shape: junction, label: "a label" }
A4@{ shape: extract, label: "a label"}
B52[a fr]@{ shape: fr }
end
subgraph " "
A1@{ shape: text, label: This is a textblock}
B1@{ shape: card, label: "a label" }
C1@{ shape: lined-proc, label: "a label" }
D1@{ shape: start, label: "a label" }
E1@{ shape: stop, label: "a label" }
E2@{ shape: doc, label: "a label" }
A6@{ shape: stored-data, label: "a label"}
A3@{ shape: delay, label: "a label" }
E3@{ shape: div-proc, label: "a label" }
B4[a label]@{ shape: win-pane }
end
</pre>
<pre id="diagram" class="mermaid2">
---
title: hello2
config:
look: handDrawn
elk:
<!-- nodePlacementStrategy: SIMPLE -->
---
%%{init: {"flowchart": {"defaultRenderer": "elk"}} }%%
flowchart TD
A([Start]) -->|go to booking page| B("select
ISBS booking no")
A --> QQ{cancel booking}
A --> RR{no show}
A --> SS{change booking}
B -->C(wmpay_request_payment.request_type= 'partial',
wmpay_request_payment.status= 'paid',
pos_booking.booking_status= partial and 'full_deposit')
style C text-align:left
C -->D{manage booking}
D -->|cancel|E[ระบบแสดงช่องให้กรอกเหตุผล]
E -->F{กดปุ่ม 'cancel' หรือไม่}
F -->|Yes|G[ระบบบันทึกค่าใหม่
และไม่สามารถแก้ไขข้อมูลได้]
F -->|No|H[กดปุ่ม 'close']
H -->|ระบบไม่เปลี่ยนแปลงข้อมูล|Z
G -->|ระบบส่งข้อมูล|I[(POS_database)]
I -->|pos_booking.booking_status='cancel'|Z([End])
D -->|no show|J[ระบบแสดงช่องให้กรอกเหตุผล]
J -->K{กดปุ่ม 'noshow' หรือไม่}
K -->|Yes|L[ระบบสร้างใบเสร็จอัตโนมัติ
Product_id: 439,
ItemName: no show]
style L text-align:left
K -->|No|O[กดปุ่ม 'close']
O -->|ระบบไม่เปลี่ยนแปลงข้อมูล|Z
L -->M[ระบบบันทึกค่าใหม่]
M -->|ระบบส่งข้อมูล|N[(POS_database)]
N -->|pos_booking.booking_status=noshow|Z
</pre>
<pre id="diagram" class="mermaid2">
---
title: hello2
config:
look: handDrawn
layout: dagre
elk:
nodePlacementStrategy: BRANDES_KOEPF
---
flowchart
A --> A
subgraph A
B --> B
subgraph B
C
end
end
</pre
>
<pre id="diagram" class="mermaid2">
---
config:
look: handdrawn
flowchart:
htmlLabels: true
---
flowchart
A[I am a long text, where do I go??? handdrawn - true]
</pre
>
</div>
<div class="flex">
<pre id="diagram" class="mermaid2">
---
config:
flowchart:
htmlLabels: false
---
flowchart
A[I am a long text, where do I go??? classic - false]
</pre
>
<pre id="diagram" class="mermaid2">
---
config:
flowchart:
htmlLabels: true
---
flowchart
A[I am a long text, where do I go??? classic - true]
</pre
>
</div>
<pre id="diagram2" class="mermaid2">
flowchart LR
id1(Start)-->id2(Stop)
style id1 fill:#f9f,stroke:#333,stroke-width:4px
style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5
</pre>
<pre id="diagram3" class="mermaid2">
flowchart LR
A:::foo & B:::bar --> C:::foobar
classDef foo stroke:#f00
classDef bar stroke:#0f0
classDef ash color:red
class C ash
style C stroke:#00f, fill:black
</pre>
<pre id="diagram4" class="mermaid2">
stateDiagram
A:::foo
B:::bar --> C:::foobar
classDef foo stroke:#f00
classDef bar stroke:#0f0
style C stroke:#00f, fill:black, color:white
</pre>
<pre id="diagram4" class="mermaid">
flowchart TB
A@{
label: "aksljhf kasjdh"
}
</pre>
<script type="module"> <script type="module">
import mermaid from './mermaid.esm.mjs'; import mermaid from './mermaid.esm.mjs';
import layouts from './mermaid-layout-elk.esm.mjs'; import layouts from './mermaid-layout-elk.esm.mjs';

View File

@@ -0,0 +1,174 @@
<html>
<head>
<link href="https://fonts.googleapis.com/css?family=Montserrat&display=swap" rel="stylesheet" />
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
/>
<link
href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css?family=Noto+Sans+SC&display=swap"
rel="stylesheet"
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Caveat:wght@400..700&family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
rel="stylesheet"
/>
<style>
body {
/* background: rgb(221, 208, 208); */
/* background: #333; */
font-family: 'Arial';
/* font-size: 18px !important; */
}
h1 {
color: grey;
}
.mermaid2 {
display: none;
}
.mermaid svg {
/* font-size: 18px !important; */
/* background-color: #efefef;
background-image: radial-gradient(#fff 51%, transparent 91%),
radial-gradient(#fff 51%, transparent 91%);
background-size: 20px 20px;
background-position:
0 0,
10px 10px;
background-repeat: repeat; */
}
</style>
</head>
<body style="display: flex; gap: 2rem; flex-direction: row">
<pre id="diagram4" class="mermaid">
flowchart LR
A@{ icon: "fa:window-minimize", form: circle }
E@{ icon: "fa:window-minimize", form: circle }
B@{ icon: "fa:bell", form: circle }
B2@{ icon: "fa:bell", form: circle }
C@{ icon: "fa:address-book", form: square }
D@{ icon: "fa:star-half", form: square }
A --> E
B --> B2
</pre>
<pre id="diagram4" class="mermaid2">
flowchart TB
A --test2--> B2@{ icon: "fa:bell", form: "rounded", label: "B2 aiduaid uyawduad uaduabd uyduadb", pos: "b" }
B2 --test--> C
D --> B2 --> E
style B2 fill:#f9f,stroke:#333,stroke-width:4px
</pre
>
<pre id="diagram43" class="mermaid2">
flowchart BT
A --test2--> B2@{ icon: "fa:bell", form: "square", label: "B2", pos: "t", h: 40, w: 30 }
B2 --test--> C
D --> B2 --> E
</pre
>
<pre id="diagram4" class="mermaid2">
flowchart BT
A --test2--> B2@{ icon: "fa:bell", label: "B2 awiugdawu uydgayuiwd wuydguy", pos: "b", h: 40, w: 30 }
B2 --test--> C
</pre
>
<pre id="diagram43" class="mermaid2">
flowchart BT
A --test2--> B2@{ icon: "fa:bell", label: "B2 dawuygd ayuwgd uy", pos: "t", h: 40, w: 30 }
B2 --test--> C
</pre
>
<pre id="diagram6" class="mermaid2">
flowchart TB
A --> B2@{ icon: "fa:bell", form: "circle", label: "test augfuyfavf ydvaubfuac", pos: "t", w: 200, h: 100 } --> C
</pre
>
<pre id="diagram6" class="mermaid2">
flowchart TB
A --> B2@{ icon: "fa:bell", form: "circle", label: "test augfuyfavf ydvaubfuac", pos: "b", w: 200, h: 100 } --> C
D --> B2 --> E
</pre
>
<script type="module">
import mermaid from './mermaid.esm.mjs';
import layouts from './mermaid-layout-elk.esm.mjs';
mermaid.registerLayoutLoaders(layouts);
mermaid.registerIconPacks([
{
name: 'logos',
loader: () =>
fetch('https://unpkg.com/@iconify-json/logos/icons.json').then((res) => res.json()),
},
{
name: 'fa',
loader: () =>
fetch('https://unpkg.com/@iconify-json/fa6-solid/icons.json').then((res) => res.json()),
},
]);
mermaid.parseError = function (err, hash) {
console.error('Mermaid error: ', err);
};
window.callback = function () {
alert('A callback was triggered');
};
mermaid.initialize({
// theme: 'base',
// handdrawnSeed: 12,
look: 'classic',
// 'elk.nodePlacement.strategy': 'NETWORK_SIMPLEX',
// 'elk.nodePlacement.strategy': 'SIMPLE',
// 'elk.nodePlacement.strategy': 'LAYERED',
// 'elk.mergeEdges': true,
// layout: 'dagre',
// layout: 'elk',
// layout: 'fixed',
// htmlLabels: false,
flowchart: { titleTopMargin: 10, padding: 0, htmlLabels: true },
// fontFamily: 'Caveat',
// fontFamily: 'Kalam',
fontFamily: 'courier',
sequence: {
actorFontFamily: 'courier',
noteFontFamily: 'courier',
messageFontFamily: 'courier',
},
themeVariables: {
fontSize: 50,
fontFamily: 'courier',
},
logLevel: 0,
securityLevel: 'loose',
});
function callback() {
alert('It worked');
}
mermaid.parseError = function (err, hash) {
console.error('In parse error:');
console.error(err);
};
</script>
</body>
</html>

View File

@@ -50,6 +50,23 @@ const contentLoaded = async function () {
mermaid.registerLayoutLoaders(layouts); mermaid.registerLayoutLoaders(layouts);
mermaid.initialize(graphObj.mermaid); mermaid.initialize(graphObj.mermaid);
const staticBellIconPack = {
prefix: 'fa6-regular',
icons: {
bell: {
body: '<path fill="currentColor" d="M224 0c-17.7 0-32 14.3-32 32v19.2C119 66 64 130.6 64 208v25.4c0 45.4-15.5 89.5-43.8 124.9L5.3 377c-5.8 7.2-6.9 17.1-2.9 25.4S14.8 416 24 416h400c9.2 0 17.6-5.3 21.6-13.6s2.9-18.2-2.9-25.4l-14.9-18.6c-28.3-35.5-43.8-79.6-43.8-125V208c0-77.4-55-142-128-156.8V32c0-17.7-14.3-32-32-32m0 96c61.9 0 112 50.1 112 112v25.4c0 47.9 13.9 94.6 39.7 134.6H72.3c25.8-40 39.7-86.7 39.7-134.6V208c0-61.9 50.1-112 112-112m64 352H160c0 17 6.7 33.3 18.7 45.3S207 512 224 512s33.3-6.7 45.3-18.7S288 465 288 448"/>',
width: 448,
},
},
width: 512,
height: 512,
};
mermaid.registerIconPacks([
{
name: 'fa',
loader: () => staticBellIconPack,
},
]);
await mermaid.run(); await mermaid.run();
} }
}; };

152
cypress/timings.json Normal file
View File

@@ -0,0 +1,152 @@
{
"durations": [
{
"spec": "cypress/integration/other/configuration.spec.js",
"duration": 4989
},
{
"spec": "cypress/integration/other/external-diagrams.spec.js",
"duration": 1382
},
{
"spec": "cypress/integration/other/ghsa.spec.js",
"duration": 3178
},
{
"spec": "cypress/integration/other/iife.spec.js",
"duration": 1372
},
{
"spec": "cypress/integration/other/interaction.spec.js",
"duration": 8998
},
{
"spec": "cypress/integration/other/rerender.spec.js",
"duration": 1249
},
{
"spec": "cypress/integration/other/xss.spec.js",
"duration": 25664
},
{
"spec": "cypress/integration/rendering/appli.spec.js",
"duration": 1928
},
{
"spec": "cypress/integration/rendering/architecture.spec.ts",
"duration": 2330
},
{
"spec": "cypress/integration/rendering/block.spec.js",
"duration": 11156
},
{
"spec": "cypress/integration/rendering/c4.spec.js",
"duration": 3418
},
{
"spec": "cypress/integration/rendering/classDiagram-v2.spec.js",
"duration": 14866
},
{
"spec": "cypress/integration/rendering/classDiagram.spec.js",
"duration": 9894
},
{
"spec": "cypress/integration/rendering/conf-and-directives.spec.js",
"duration": 5778
},
{
"spec": "cypress/integration/rendering/current.spec.js",
"duration": 1690
},
{
"spec": "cypress/integration/rendering/erDiagram.spec.js",
"duration": 9144
},
{
"spec": "cypress/integration/rendering/errorDiagram.spec.js",
"duration": 1951
},
{
"spec": "cypress/integration/rendering/flowchart-elk.spec.js",
"duration": 2196
},
{
"spec": "cypress/integration/rendering/flowchart-handDrawn.spec.js",
"duration": 21029
},
{
"spec": "cypress/integration/rendering/flowchart-shape-alias.spec.ts",
"duration": 16087
},
{
"spec": "cypress/integration/rendering/flowchart-v2.spec.js",
"duration": 27465
},
{
"spec": "cypress/integration/rendering/flowchart.spec.js",
"duration": 20035
},
{
"spec": "cypress/integration/rendering/gantt.spec.js",
"duration": 11366
},
{
"spec": "cypress/integration/rendering/gitGraph.spec.js",
"duration": 34025
},
{
"spec": "cypress/integration/rendering/iconShape.spec.ts",
"duration": 185902
},
{
"spec": "cypress/integration/rendering/imageShape.spec.ts",
"duration": 41631
},
{
"spec": "cypress/integration/rendering/info.spec.ts",
"duration": 1736
},
{
"spec": "cypress/integration/rendering/journey.spec.js",
"duration": 2247
},
{
"spec": "cypress/integration/rendering/katex.spec.js",
"duration": 2144
},
{
"spec": "cypress/integration/rendering/marker_unique_id.spec.js",
"duration": 1646
},
{
"spec": "cypress/integration/rendering/mindmap.spec.ts",
"duration": 6406
},
{
"spec": "cypress/integration/rendering/newShapes.spec.ts",
"duration": 107219
},
{
"spec": "cypress/integration/rendering/stateDiagram.spec.js",
"duration": 15834
},
{
"spec": "cypress/integration/rendering/theme.spec.js",
"duration": 33240
},
{
"spec": "cypress/integration/rendering/timeline.spec.ts",
"duration": 7122
},
{
"spec": "cypress/integration/rendering/xyChart.spec.js",
"duration": 11127
},
{
"spec": "cypress/integration/rendering/zenuml.spec.js",
"duration": 2391
}
]
}

View File

@@ -159,30 +159,87 @@
class People List~List~Person~~ class People List~List~Person~~
</pre> </pre>
<hr /> <hr />
<pre class="mermaid"> <pre class="mermaid">
classDiagram classDiagram
A1 --> B1 namespace Company.Project.Module {
namespace A { class GenericClass~T~ {
class A1 { +addItem(item: T)
+foo : string +getItem() T
}
class A2 {
+bar : int
} }
} }
namespace B {
class B1 {
+foo : bool
}
class B2 {
+bar : float
}
}
A2 --> B2
</pre> </pre>
<hr /> <hr />
<pre class="mermaid">
classDiagram
namespace Company.Project.Module.SubModule {
class Report {
+generatePDF(data: List)
+generateCSV(data: List)
}
}
namespace Company.Project.Module {
class Admin {
+generateReport()
}
}
Admin --> Report : generates
</pre>
<pre class="mermaid">
classDiagram
namespace Company.Project.Module {
class User {
+login(username: String, password: String)
+logout()
}
class Admin {
+addUser(user: User)
+removeUser(user: User)
+generateReport()
}
class Report {
+generatePDF(reportData: List)
+generateCSV(reportData: List)
}
}
Admin --> User : manages
Admin --> Report : generates
</pre>
<hr />
<pre class="mermaid">
classDiagram
namespace Shapes {
class Shape {
+calculateArea() double
}
class Circle {
+double radius
}
class Square {
+double side
}
}
Shape <|-- Circle
Shape <|-- Square
namespace Vehicles {
class Vehicle {
+String brand
}
class Car {
+int horsepower
}
class Bike {
+boolean hasGears
}
}
Vehicle <|-- Car
Vehicle <|-- Bike
Car --> Circle : "Logo Shape"
Bike --> Square : "Logo Shape"
</pre>
<script type="module"> <script type="module">
import mermaid from './mermaid.esm.mjs'; import mermaid from './mermaid.esm.mjs';
mermaid.initialize({ mermaid.initialize({

228
docs/adding-new-shape.md Normal file
View File

@@ -0,0 +1,228 @@
> **Warning**
>
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
>
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/adding-new-shape.md](../packages/mermaid/src/docs/adding-new-shape.md).
# Custom SVG Shapes Library
This library provides a collection of custom SVG shapes, utilities, and helpers for generating diagram components. The shapes are designed to be used within an SVG container and include a variety of common and complex shapes.
## Overview
## Shape Helpers and Utilities
Before starting with shape creation, it's essential to familiarize yourself with the utilities provided in the `utils.ts` file from `packages/mermaid/src/rendering-util/rendering-elements/shapes/util.js`. These utilities are designed to assist with various aspects of SVG shape manipulation and ensure consistent and accurate rendering.
## Available Utilities
### 1. `labelHelper`
- **Purpose**: This function creates and inserts labels inside SVG shapes.
- **Features**:
- Handles both HTML labels and plain text.
- Calculates the bounding box dimensions of the label.
- Ensures proper positioning of labels within shapes.
### 2. `updateNodeBounds`
- **Purpose**: Updates the bounding box dimensions (width and height) of a node.
- **Usage**:
- Adjusts the size of the node to fit the content or shape.
- Useful for ensuring that shapes resize appropriately based on their content.
### 3. `insertPolygonShape`
- **Purpose**: Inserts a polygon shape into an SVG container.
- **Features**:
- Handles the creation and insertion of complex polygonal shapes.
- Configures the shape's appearance and positioning within the SVG container.
### 4. `getNodeClasses`
- **Purpose**: Returns the appropriate CSS classes for a node based on its configuration.
- **Usage**:
- Dynamically applies CSS classes to nodes for styling purposes.
- Ensures that nodes adhere to the desired design and theme.
### 5. `createPathFromPoints`
- **Purpose**: Generates an SVG path string from an array of points.
- **Usage**:
- Converts a list of points into a smooth path.
- Useful for creating custom shapes or paths within the SVG.
### 6. `generateFullSineWavePoints`
- **Purpose**: Generates points for a sine wave, useful for creating wavy-edged shapes.
- **Usage**:
- Facilitates the creation of shapes with wavy or sine-wave edges.
- Can be used to add decorative or dynamic edges to shapes.
## Getting Started
To utilize these utilities, simply import them from the `utils.ts` file into your shape creation script. These helpers will streamline the process of building and customizing SVG shapes, ensuring consistent results across your projects.
```typescript
import {
labelHelper,
updateNodeBounds,
insertPolygonShape,
getNodeClasses,
createPathFromPoints,
generateFullSineWavePoints,
} from './utils.ts';
```
## Example Usage
Heres a basic example of how you might use some of these utilities:
```typescript
import { labelHelper, insertPolygonShape } from './utils.ts';
const svgContainer = document.getElementById('svgContainer');
// Insert a polygon shape
insertPolygonShape(svgContainer /* shape-specific parameters */);
// Create and insert a label inside the shape
labelHelper(svgContainer /* label-specific parameters */);
```
## Adding New Shapes
### 1. Create the Shape Function
To add a new shape:
- **Create the shape function**: Create a new file of name of the shape and export a function in the `shapes` directory that generates your shape. The file and function should follow the pattern used in existing shapes and return an SVG element.
- **Example**:
```typescript
import { Node, RenderOptions } from '../../types.d.ts';
export const myNewShape = async (
parent: SVGAElement,
node: Node,
renderOptions: RenderOptions
) => {
// Create your shape here
const shape = parent.insert('g').attr('class', 'my-new-shape');
// Add other elements or styles as needed
return shape;
};
```
### 2. Register the Shape
- **Register the shape**: Add your shape to the `shapes` object in the main shapes module. This allows your shape to be recognized and used within the system.
- **Example**:
```typescript
import { myNewShape } from './shapes/myNewShape';
const shapes = {
...,
'my-new-shape': myNewShape,
// Shortened alias (if any).
'm-nsh': myNewShape
};
```
# Shape Intersection Algorithms
This contains algorithms and utilities for calculating intersection points for various shapes in SVG. Arrow intersection points are crucial for accurately determining where arrows connect with shapes. Ensuring precise intersection points enhances the clarity and accuracy of flowcharts and diagrams.
## Shape Intersection Functions
### 1. `Ellipse`
Calculates the intersection points for an ellipse.
**Usage**:
```javascript
import intersectEllipse from './intersect-ellipse.js';
const intersection = intersectEllipse(node, rx, ry, point);
```
- **Parameters**:
- `node`: The SVG node element.
- `rx`: The x-radius of the ellipse.
- `ry`: The y-radius of the ellipse.
- `point`: The point from which the intersection is calculated.
### 2. `intersectRect`
Calculates the intersection points for a rectangle.
**Usage**:
```javascript
import intersectRect from './intersect-rect.js';
const intersection = intersectRect(node, point);
```
- **Parameters**:
- `node`: The SVG node element.
- `point`: The point from which the intersection is calculated.
### 3. `intersectPolygon`
Calculates the intersection points for a polygon.
**Usage**:
```javascript
import intersectPolygon from './intersect-polygon.js';
const intersection = intersectPolygon(node, polyPoints, point);
```
- **Parameters**:
- `node`: The SVG node element.
- `polyPoints`: Array of points defining the polygon.
- `point`: The point from which the intersection is calculated.
## Cypress Tests
To ensure the robustness of the flowchart shapes, there are implementation of comprehensive Cypress test cases in `newShapes.spec.ts` file. These tests cover various aspects such as:
- **Shapes**: Testing new shapes like `bowTieRect`, `waveRectangle`, `trapezoidalPentagon`, etc.
- **Looks**: Verifying shapes under different visual styles (`classic` and `handDrawn`).
- **Directions**: Ensuring correct rendering in all flow directions of arrows :
- `TB` `(Top -> Bottom)`
- `BT` `(Bottom -> Top)`
- `LR` `(Left -> Right)`
- `RL` `(Right -> Left)`
- **Labels**: Testing shapes with different labels, including:
- No labels
- Short labels
- Very long labels
- Markdown with `htmlLabels:true` and `htmlLabels:false`
- **Styles**: Applying custom styles to shapes and verifying correct rendering.
- **Class Definitions**: Using `classDef` to apply custom classes and testing their impact.
### Running the Tests
To run the Cypress tests, follow these steps:
1. Ensure you have all dependencies installed by running:
```bash
pnpm install
```
2. Start the Cypress test runner:
```bash
cypress open --env updateSnapshots=true
```
3. Select the test suite from the Cypress interface to run all the flowchart shape tests.

View File

@@ -127,7 +127,7 @@ Error.prepareStackTrace
#### Defined in #### Defined in
node_modules/.pnpm/@types+node\@20.16.2/node_modules/@types/node/globals.d.ts:28 node_modules/.pnpm/@types+node\@20.16.11/node_modules/@types/node/globals.d.ts:98
--- ---
@@ -141,7 +141,7 @@ Error.stackTraceLimit
#### Defined in #### Defined in
node_modules/.pnpm/@types+node\@20.16.2/node_modules/@types/node/globals.d.ts:30 node_modules/.pnpm/@types+node\@20.16.11/node_modules/@types/node/globals.d.ts:100
## Methods ## Methods
@@ -168,4 +168,4 @@ Error.captureStackTrace
#### Defined in #### Defined in
node_modules/.pnpm/@types+node\@20.16.2/node_modules/@types/node/globals.d.ts:21 node_modules/.pnpm/@types+node\@20.16.11/node_modules/@types/node/globals.d.ts:91

View File

@@ -20,7 +20,7 @@
#### Defined in #### Defined in
[packages/mermaid/src/rendering-util/types.ts:120](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L120) [packages/mermaid/src/rendering-util/types.ts:125](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L125)
--- ---
@@ -30,7 +30,7 @@
#### Defined in #### Defined in
[packages/mermaid/src/rendering-util/types.ts:119](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L119) [packages/mermaid/src/rendering-util/types.ts:124](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L124)
--- ---
@@ -40,4 +40,4 @@
#### Defined in #### Defined in
[packages/mermaid/src/rendering-util/types.ts:118](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L118) [packages/mermaid/src/rendering-util/types.ts:123](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L123)

View File

@@ -19,4 +19,4 @@ The `parseError` function will not be called.
#### Defined in #### Defined in
[packages/mermaid/src/types.ts:45](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L45) [packages/mermaid/src/types.ts:56](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L56)

View File

@@ -18,7 +18,7 @@ The config passed as YAML frontmatter or directives
#### Defined in #### Defined in
[packages/mermaid/src/types.ts:56](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L56) [packages/mermaid/src/types.ts:67](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L67)
--- ---
@@ -30,4 +30,4 @@ The diagram type, e.g. 'flowchart', 'sequence', etc.
#### Defined in #### Defined in
[packages/mermaid/src/types.ts:52](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L52) [packages/mermaid/src/types.ts:63](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L63)

View File

@@ -39,7 +39,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present.
#### Defined in #### Defined in
[packages/mermaid/src/types.ts:79](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L79) [packages/mermaid/src/types.ts:90](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L90)
--- ---
@@ -51,7 +51,7 @@ The diagram type, e.g. 'flowchart', 'sequence', etc.
#### Defined in #### Defined in
[packages/mermaid/src/types.ts:69](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L69) [packages/mermaid/src/types.ts:80](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L80)
--- ---
@@ -63,4 +63,4 @@ The svg code for the rendered graph.
#### Defined in #### Defined in
[packages/mermaid/src/types.ts:65](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L65) [packages/mermaid/src/types.ts:76](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L76)

View File

@@ -14,7 +14,7 @@
#### Defined in #### Defined in
[packages/mermaid/src/defaultConfig.ts:269](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L269) [packages/mermaid/src/defaultConfig.ts:267](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L267)
--- ---

View File

@@ -64,7 +64,7 @@ To add an integration to this list, see the [Integrations - create page](./integ
- [Mermaid Flow Visual Editor](https://www.mermaidflow.app) ✅ - [Mermaid Flow Visual Editor](https://www.mermaidflow.app) ✅
- [Mermerd](https://github.com/KarnerTh/mermerd) - [Mermerd](https://github.com/KarnerTh/mermerd)
- [Slab](https://slab.com) ✅ - [Slab](https://slab.com) ✅
- [Swimm](https://docs.swimm.io/features/diagrams-and-charts/#mermaid--swimm--up-to-date-diagrams-) ✅ - [Swimm](https://docs.swimm.io/features/diagrams-and-charts) ✅
- [NotesHub](https://noteshub.app) ✅ - [NotesHub](https://noteshub.app) ✅
- [Notion](https://notion.so) ✅ - [Notion](https://notion.so) ✅
- [Observable](https://observablehq.com/@observablehq/mermaid) ✅ - [Observable](https://observablehq.com/@observablehq/mermaid) ✅

View File

@@ -12,7 +12,7 @@ Try the Ultimate AI, Mermaid, and Visual Diagramming Suite by creating an accoun
<br /> <br />
<a href="https://www.producthunt.com/products/mermaid-chart?utm_source=badge-follow&utm_medium=badge&utm_souce=badge-mermaid&#0045;chart" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=552855&theme=light" alt="Mermaid&#0032;Chart - A&#0032;smarter&#0032;way&#0032;to&#0032;create&#0032;diagrams | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/mermaid-whiteboard?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-mermaid&#0045;whiteboard" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=486720&theme=light" alt="Mermaid&#0032;Whiteboard - Drag&#0032;&#0038;&#0032;Drop&#0032;your&#0032;Nodes&#0032;with&#0032;Mermaid&#0039;s&#0032;new&#0032;Whiteboard&#0033; | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
## About ## About
@@ -22,17 +22,20 @@ Try the Ultimate AI, Mermaid, and Visual Diagramming Suite by creating an accoun
- **Editor** - A web based editor for creating and editing Mermaid diagrams. - **Editor** - A web based editor for creating and editing Mermaid diagrams.
- **Visual Editor** - The Visual Editor enables users of all skill levels to create diagrams easily and efficiently, with both GUI and code-based editing options. - **Mermaid AI** - Use our embedded AI Chat to generate diagrams from natural language descriptions.
- **AI Chat** - Use our embedded AI Chat to generate diagrams from natural language descriptions. - **Whiteboard** - A virtual whiteboard for creating and editing Mermaid diagrams.
- **Plugins** - A plugin system for extending the functionality of Mermaid. - **Plugins** - A plugin system for extending the functionality of Mermaid.
Official Mermaid Chart plugins: Official Mermaid Chart plugins:
- [Mermaid Chart GPT](https://chat.openai.com/g/g-1IRFKwq4G-mermaid-chart) - [Mermaid Chart GPT](https://chat.openai.com/g/g-1IRFKwq4G-mermaid-chart)
- [Confluence](https://marketplace.atlassian.com/apps/1234056/mermaid-chart-for-confluence?hosting=cloud&tab=overview)
- [Jira](https://marketplace.atlassian.com/apps/1234810/mermaid-chart-for-jira?tab=overview&hosting=cloud)
- [Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=MermaidChart.vscode-mermaid-chart) - [Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=MermaidChart.vscode-mermaid-chart)
- [JetBrains IDE](https://plugins.jetbrains.com/plugin/23043-mermaid-chart) - [JetBrains IDE](https://plugins.jetbrains.com/plugin/23043-mermaid-chart)
- [Google Docs](https://gsuite.google.com/marketplace/app/mermaidchart/947683068472)
- [Microsoft PowerPoint and Word](https://appsource.microsoft.com/en-us/product/office/WA200006214?tab=Overview) - [Microsoft PowerPoint and Word](https://appsource.microsoft.com/en-us/product/office/WA200006214?tab=Overview)
Visit our [Plugins](https://www.mermaidchart.com/plugins) page for more information. Visit our [Plugins](https://www.mermaidchart.com/plugins) page for more information.

View File

@@ -6,6 +6,12 @@
# Blog # Blog
## [Expanding the Horizons of Mermaid Flowcharts: Introducing 30 New Shapes!](https://www.mermaidchart.com/blog/posts/new-mermaid-flowchart-shapes/)
24 September 2024 · 5 mins
Discover 30 new shapes in Mermaid flowcharts, offering enhanced clarity, customization, and versatility for more dynamic and expressive visualizations.
## [Introducing Architecture Diagrams in Mermaid](https://www.mermaidchart.com/blog/posts/mermaid-supports-architecture-diagrams/) ## [Introducing Architecture Diagrams in Mermaid](https://www.mermaidchart.com/blog/posts/mermaid-supports-architecture-diagrams/)
2 September 2024 · 2 mins 2 September 2024 · 2 mins

View File

@@ -211,6 +211,27 @@ block-beta
This example demonstrates how Mermaid dynamically adjusts the width of the columns to accommodate the widest block, in this case, 'a' and the composite block 'e'. This dynamic adjustment is essential for creating visually balanced and easy-to-understand diagrams. This example demonstrates how Mermaid dynamically adjusts the width of the columns to accommodate the widest block, in this case, 'a' and the composite block 'e'. This dynamic adjustment is essential for creating visually balanced and easy-to-understand diagrams.
**Merging Blocks Horizontally:**
In scenarios where you need to stack blocks horizontally, you can use column width to accomplish the task. Blocks can be arranged vertically by putting them in a single column. Here is how you can create a block diagram in which 4 blocks are stacked on top of each other:
```mermaid-example
block-beta
block
columns 1
a["A label"] b c d
end
```
```mermaid
block-beta
block
columns 1
a["A label"] b c d
end
```
In this example, the width of the merged block dynamically adjusts to the width of the largest child block.
With these advanced configuration options, Mermaid's block diagrams can be tailored to represent a wide array of complex systems and structures. The flexibility offered by these features enables users to create diagrams that are both informative and visually appealing. In the following sections, we will explore further capabilities, including different block shapes and linking options. With these advanced configuration options, Mermaid's block diagrams can be tailored to represent a wide array of complex systems and structures. The flexibility offered by these features enables users to create diagrams that are both informative and visually appealing. In the following sections, we will explore further capabilities, including different block shapes and linking options.
## 4. Block Varieties and Shapes ## 4. Block Varieties and Shapes

View File

@@ -298,6 +298,634 @@ flowchart TD
id1(((This is the text in the circle))) id1(((This is the text in the circle)))
``` ```
## Expanded Node Shapes in Mermaid Flowcharts (v11.3.0+)
Mermaid introduces 30 new shapes to enhance the flexibility and precision of flowchart creation. These new shapes provide more options to represent processes, decisions, events, data storage visually, and other elements within your flowcharts, improving clarity and semantic meaning.
New Syntax for Shape Definition
Mermaid now supports a general syntax for defining shape types to accommodate the growing number of shapes. This syntax allows you to assign specific shapes to nodes using a clear and flexible format:
```
A@{ shape: rect }
```
This syntax creates a node A as a rectangle. It renders in the same way as `A["A"]`, or `A`.
### Complete List of New Shapes
Below is a comprehensive list of the newly introduced shapes and their corresponding semantic meanings, short names, and aliases:
| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** |
| ------------------------------------- | ---------------------- | -------------- | ------------------------------ | -------------------------------------------------------------- |
| **Process** | Rectangle | `rect` | Standard process shape | `proc`, `process`, `rectangle` |
| **Event** | Rounded Rectangle | `rounded` | Represents an event | `event` |
| **Terminal Point** | Stadium | `stadium` | Terminal point | `terminal`, `pill` |
| **Subprocess** | Framed Rectangle | `fr-rect` | Subprocess | `subprocess`,`subproc`, `framed-rectangle`, `subroutine` |
| **Database** | Cylinder | `cyl` | Database storage | `db`, `database`, `cylinder` |
| **Start** | Circle | `circle` | Starting point | `circ` |
| **Odd** | Odd | `odd` | Odd shape | |
| **Decision** | Diamond | `diam` | Decision-making step | `decision`, `diamond` |
| **Prepare Conditional** | Hexagon | `hex` | Preparation or condition step | `hexagon`, `prepare` |
| **Data Input/Output** | Lean Right | `lean-r` | Represents input or output | `lean-right`, `in-out` |
| **Data Input/Output** | Lean Left | `lean-l` | Represents output or input | `lean-left`, `out-in` |
| **Priority Action** | Trapezoid Base Bottom | `trap-b` | Priority action | `priority`, `trapezoid-bottom` |
| **Manual Operation** | Trapezoid Base Top | `trap-t` | Represents a manual task | `manual`, `trapezoid-top` |
| **Stop** | Double Circle | `dbl-circ` | Represents a stop point | `double-circle` |
| **Text Block** | Text Block | `text` | Text block | - |
| **Card** | Notched Rectangle | `notch-rect` | Represents a card | `card`, `notched-rectangle` |
| **Lined/Shaded Process** | Lined Rectangle | `lin-rect` | Lined process shape | `lined-rectangle`,`lined-process`, `lin-proc`,`shaded-process` |
| **Start** | Small Circle | `sm-circ` | Small starting point | `start`, `small-circle` |
| **Stop** | Framed Circle | `fr-circ` | Stop point | `stop`, `framed-circle` |
| **Fork/Join** | Filled Rectangle | `fork` | Fork or join in process flow | `join` |
| **Collate** | Hourglass | `hourglass` | Represents a collate operation | `hourglass` |
| **Comment** | Curly Brace | `brace` | Adds a comment | `comment`, `brace-l` |
| **Comment Right** | Curly Brace | `brace-r` | Adds a comment | - |
| **Comment with braces on both sides** | Curly Braces | `braces` | Adds a comment | - |
| **Com Link** | Lightning Bolt | `bolt` | Communication link | `com-link`, `lightning-bolt` |
| **Document** | Document | `doc` | Represents a document | `doc`, `document` |
| **Delay** | Half-Rounded Rectangle | `delay` | Represents a delay | `half-rounded-rectangle` |
| **Direct Access Storage** | Horizontal Cylinder | `h-cyl` | Direct access storage | `das`, `horizontal-cylinder` |
| **Disk Storage** | Lined Cylinder | `lin-cyl` | Disk storage | `disk`, `lined-cylinder` |
| **Display** | Curved Trapezoid | `curv-trap` | Represents a display | `curved-trapezoid`, `display` |
| **Divided Process** | Divided Rectangle | `div-rect` | Divided process shape | `div-proc`, `divided-rectangle`, `divided-process` |
| **Extract** | Triangle | `tri` | Extraction process | `extract`, `triangle` |
| **Internal Storage** | Window Pane | `win-pane` | Internal storage | `internal-storage`, `window-pane` |
| **Junction** | Filled Circle | `f-circ` | Junction point | `junction`, `filled-circle` |
| **Lined Document** | Lined Document | `lin-doc` | Lined document | `lined-document` |
| **Loop Limit** | Trapezoidal Pentagon | `notch-pent` | Loop limit step | `loop-limit`, `notched-pentagon` |
| **Manual File** | Flipped Triangle | `flip-tri` | Manual file operation | `manual-file`, `flipped-triangle` |
| **Manual Input** | Sloped Rectangle | `sl-rect` | Manual input step | `manual-input`, `sloped-rectangle` |
| **Multi-Document** | Stacked Document | `docs` | Multiple documents | `documents`, `st-doc`, `stacked-document` |
| **Multi-Process** | Stacked Rectangle | `st-rect` | Multiple processes | `procs`, `processes`, `stacked-rectangle` |
| **Paper Tape** | Flag | `flag` | Paper tape | `paper-tape` |
| **Stored Data** | Bow Tie Rectangle | `bow-rect` | Stored data | `stored-data`, `bow-tie-rectangle` |
| **Summary** | Crossed Circle | `cross-circ` | Summary | `summary`, `crossed-circle` |
| **Tagged Document** | Tagged Document | `tag-doc` | Tagged document | `tag-doc`, `tagged-document` |
| **Tagged Process** | Tagged Rectangle | `tag-rect` | Tagged process | `tagged-rectangle`,`tag-proc`, `tagged-process` |
### Example Flowchart with New Shapes
Heres an example flowchart that utilizes some of the newly introduced shapes:
```mermaid-example
flowchart RL
A@{ shape: manual-file, label: "File Handling"}
B@{ shape: manual-input, label: "User Input"}
C@{ shape: docs, label: "Multiple Documents"}
D@{ shape: procs, label: "Process Automation"}
E@{ shape: paper-tape, label: "Paper Records"}
```
```mermaid
flowchart RL
A@{ shape: manual-file, label: "File Handling"}
B@{ shape: manual-input, label: "User Input"}
C@{ shape: docs, label: "Multiple Documents"}
D@{ shape: procs, label: "Process Automation"}
E@{ shape: paper-tape, label: "Paper Records"}
```
### Process
```mermaid-example
flowchart TD
A@{ shape: rect, label: "This is a process" }
```
```mermaid
flowchart TD
A@{ shape: rect, label: "This is a process" }
```
### Event
```mermaid-example
flowchart TD
A@{ shape: rounded, label: "This is an event" }
```
```mermaid
flowchart TD
A@{ shape: rounded, label: "This is an event" }
```
### Terminal Point (Stadium)
```mermaid-example
flowchart TD
A@{ shape: stadium, label: "Terminal point" }
```
```mermaid
flowchart TD
A@{ shape: stadium, label: "Terminal point" }
```
### Subprocess
```mermaid-example
flowchart TD
A@{ shape: subproc, label: "This is a subprocess" }
```
```mermaid
flowchart TD
A@{ shape: subproc, label: "This is a subprocess" }
```
### Database (Cylinder)
```mermaid-example
flowchart TD
A@{ shape: cyl, label: "Database" }
```
```mermaid
flowchart TD
A@{ shape: cyl, label: "Database" }
```
### Start (Circle)
```mermaid-example
flowchart TD
A@{ shape: circle, label: "Start" }
```
```mermaid
flowchart TD
A@{ shape: circle, label: "Start" }
```
### Odd
```mermaid-example
flowchart TD
A@{ shape: odd, label: "Odd shape" }
```
```mermaid
flowchart TD
A@{ shape: odd, label: "Odd shape" }
```
### Decision (Diamond)
```mermaid-example
flowchart TD
A@{ shape: diamond, label: "Decision" }
```
```mermaid
flowchart TD
A@{ shape: diamond, label: "Decision" }
```
### Prepare Conditional (Hexagon)
```mermaid-example
flowchart TD
A@{ shape: hex, label: "Prepare conditional" }
```
```mermaid
flowchart TD
A@{ shape: hex, label: "Prepare conditional" }
```
### Data Input/Output (Lean Right)
```mermaid-example
flowchart TD
A@{ shape: lean-r, label: "Input/Output" }
```
```mermaid
flowchart TD
A@{ shape: lean-r, label: "Input/Output" }
```
### Data Input/Output (Lean Left)
```mermaid-example
flowchart TD
A@{ shape: lean-l, label: "Output/Input" }
```
```mermaid
flowchart TD
A@{ shape: lean-l, label: "Output/Input" }
```
### Priority Action (Trapezoid Base Bottom)
```mermaid-example
flowchart TD
A@{ shape: trap-b, label: "Priority action" }
```
```mermaid
flowchart TD
A@{ shape: trap-b, label: "Priority action" }
```
### Manual Operation (Trapezoid Base Top)
```mermaid-example
flowchart TD
A@{ shape: trap-t, label: "Manual operation" }
```
```mermaid
flowchart TD
A@{ shape: trap-t, label: "Manual operation" }
```
### Stop (Double Circle)
```mermaid-example
flowchart TD
A@{ shape: dbl-circ, label: "Stop" }
```
```mermaid
flowchart TD
A@{ shape: dbl-circ, label: "Stop" }
```
### Text Block
```mermaid-example
flowchart TD
A@{ shape: text, label: "This is a text block" }
```
```mermaid
flowchart TD
A@{ shape: text, label: "This is a text block" }
```
### Card (Notched Rectangle)
```mermaid-example
flowchart TD
A@{ shape: notch-rect, label: "Card" }
```
```mermaid
flowchart TD
A@{ shape: notch-rect, label: "Card" }
```
### Lined/Shaded Process
```mermaid-example
flowchart TD
A@{ shape: lin-rect, label: "Lined process" }
```
```mermaid
flowchart TD
A@{ shape: lin-rect, label: "Lined process" }
```
### Start (Small Circle)
```mermaid-example
flowchart TD
A@{ shape: sm-circ, label: "Small start" }
```
```mermaid
flowchart TD
A@{ shape: sm-circ, label: "Small start" }
```
### Stop (Framed Circle)
```mermaid-example
flowchart TD
A@{ shape: framed-circle, label: "Stop" }
```
```mermaid
flowchart TD
A@{ shape: framed-circle, label: "Stop" }
```
### Fork/Join (Long Rectangle)
```mermaid-example
flowchart TD
A@{ shape: fork, label: "Fork or Join" }
```
```mermaid
flowchart TD
A@{ shape: fork, label: "Fork or Join" }
```
### Collate (Hourglass)
```mermaid-example
flowchart TD
A@{ shape: hourglass, label: "Collate" }
```
```mermaid
flowchart TD
A@{ shape: hourglass, label: "Collate" }
```
### Comment (Curly Brace)
```mermaid-example
flowchart TD
A@{ shape: comment, label: "Comment" }
```
```mermaid
flowchart TD
A@{ shape: comment, label: "Comment" }
```
### Comment Right (Curly Brace Right)
```mermaid-example
flowchart TD
A@{ shape: brace-r, label: "Comment" }
```
```mermaid
flowchart TD
A@{ shape: brace-r, label: "Comment" }
```
### Comment with braces on both sides
```mermaid-example
flowchart TD
A@{ shape: braces, label: "Comment" }
```
```mermaid
flowchart TD
A@{ shape: braces, label: "Comment" }
```
### Com Link (Lightning Bolt)
```mermaid-example
flowchart TD
A@{ shape: bolt, label: "Communication link" }
```
```mermaid
flowchart TD
A@{ shape: bolt, label: "Communication link" }
```
### Document
```mermaid-example
flowchart TD
A@{ shape: doc, label: "Document" }
```
```mermaid
flowchart TD
A@{ shape: doc, label: "Document" }
```
### Delay (Half-Rounded Rectangle)
```mermaid-example
flowchart TD
A@{ shape: delay, label: "Delay" }
```
```mermaid
flowchart TD
A@{ shape: delay, label: "Delay" }
```
### Direct Access Storage (Horizontal Cylinder)
```mermaid-example
flowchart TD
A@{ shape: das, label: "Direct access storage" }
```
```mermaid
flowchart TD
A@{ shape: das, label: "Direct access storage" }
```
### Disk Storage (Lined Cylinder)
```mermaid-example
flowchart TD
A@{ shape: lin-cyl, label: "Disk storage" }
```
```mermaid
flowchart TD
A@{ shape: lin-cyl, label: "Disk storage" }
```
### Display (Curved Trapezoid)
```mermaid-example
flowchart TD
A@{ shape: curv-trap, label: "Display" }
```
```mermaid
flowchart TD
A@{ shape: curv-trap, label: "Display" }
```
### Divided Process (Divided Rectangle)
```mermaid-example
flowchart TD
A@{ shape: div-rect, label: "Divided process" }
```
```mermaid
flowchart TD
A@{ shape: div-rect, label: "Divided process" }
```
### Extract (Small Triangle)
```mermaid-example
flowchart TD
A@{ shape: tri, label: "Extract" }
```
```mermaid
flowchart TD
A@{ shape: tri, label: "Extract" }
```
### Internal Storage (Window Pane)
```mermaid-example
flowchart TD
A@{ shape: win-pane, label: "Internal storage" }
```
```mermaid
flowchart TD
A@{ shape: win-pane, label: "Internal storage" }
```
### Junction (Filled Circle)
```mermaid-example
flowchart TD
A@{ shape: f-circ, label: "Junction" }
```
```mermaid
flowchart TD
A@{ shape: f-circ, label: "Junction" }
```
### Lined Document
```mermaid-example
flowchart TD
A@{ shape: lin-doc, label: "Lined document" }
```
```mermaid
flowchart TD
A@{ shape: lin-doc, label: "Lined document" }
```
### Loop Limit (Notched Pentagon)
```mermaid-example
flowchart TD
A@{ shape: notch-pent, label: "Loop limit" }
```
```mermaid
flowchart TD
A@{ shape: notch-pent, label: "Loop limit" }
```
### Manual File (Flipped Triangle)
```mermaid-example
flowchart TD
A@{ shape: flip-tri, label: "Manual file" }
```
```mermaid
flowchart TD
A@{ shape: flip-tri, label: "Manual file" }
```
### Manual Input (Sloped Rectangle)
```mermaid-example
flowchart TD
A@{ shape: sl-rect, label: "Manual input" }
```
```mermaid
flowchart TD
A@{ shape: sl-rect, label: "Manual input" }
```
### Multi-Document (Stacked Document)
```mermaid-example
flowchart TD
A@{ shape: docs, label: "Multiple documents" }
```
```mermaid
flowchart TD
A@{ shape: docs, label: "Multiple documents" }
```
### Multi-Process (Stacked Rectangle)
```mermaid-example
flowchart TD
A@{ shape: processes, label: "Multiple processes" }
```
```mermaid
flowchart TD
A@{ shape: processes, label: "Multiple processes" }
```
### Paper Tape (Flag)
```mermaid-example
flowchart TD
A@{ shape: flag, label: "Paper tape" }
```
```mermaid
flowchart TD
A@{ shape: flag, label: "Paper tape" }
```
### Stored Data (Bow Tie Rectangle)
```mermaid-example
flowchart TD
A@{ shape: bow-rect, label: "Stored data" }
```
```mermaid
flowchart TD
A@{ shape: bow-rect, label: "Stored data" }
```
### Summary (Crossed Circle)
```mermaid-example
flowchart TD
A@{ shape: cross-circ, label: "Summary" }
```
```mermaid
flowchart TD
A@{ shape: cross-circ, label: "Summary" }
```
### Tagged Document
```mermaid-example
flowchart TD
A@{ shape: tag-doc, label: "Tagged document" }
```
```mermaid
flowchart TD
A@{ shape: tag-doc, label: "Tagged document" }
```
### Tagged Process (Tagged Rectangle)
```mermaid-example
flowchart TD
A@{ shape: tag-rect, label: "Tagged process" }
```
```mermaid
flowchart TD
A@{ shape: tag-rect, label: "Tagged process" }
```
## Links between nodes ## Links between nodes
Nodes can be connected with links/edges. It is possible to have different types of links or attach a text string to a link. Nodes can be connected with links/edges. It is possible to have different types of links or attach a text string to a link.

View File

@@ -155,6 +155,9 @@ end
box rgb(33,66,99) box rgb(33,66,99)
... actors ... ... actors ...
end end
box rgba(33,66,99,0.5)
... actors ...
end
``` ```
> **Note** > **Note**
@@ -580,6 +583,12 @@ sequenceDiagram
It is possible to highlight flows by providing colored background rects. This is done by the notation It is possible to highlight flows by providing colored background rects. This is done by the notation
```
rect COLOR
... content ...
end
```
The colors are defined using rgb and rgba syntax. The colors are defined using rgb and rgba syntax.
``` ```

View File

@@ -64,7 +64,7 @@
}, },
"devDependencies": { "devDependencies": {
"@applitools/eyes-cypress": "^3.44.4", "@applitools/eyes-cypress": "^3.44.4",
"@argos-ci/cypress": "^2.1.0", "@argos-ci/cypress": "^2.2.2",
"@changesets/changelog-github": "^0.5.0", "@changesets/changelog-github": "^0.5.0",
"@changesets/cli": "^2.27.7", "@changesets/cli": "^2.27.7",
"@cspell/eslint-plugin": "^8.8.4", "@cspell/eslint-plugin": "^8.8.4",
@@ -91,6 +91,7 @@
"cspell": "^8.6.0", "cspell": "^8.6.0",
"cypress": "^13.14.1", "cypress": "^13.14.1",
"cypress-image-snapshot": "^4.0.1", "cypress-image-snapshot": "^4.0.1",
"cypress-split": "^1.24.0",
"esbuild": "^0.21.5", "esbuild": "^0.21.5",
"eslint": "^9.4.0", "eslint": "^9.4.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
@@ -103,7 +104,7 @@
"eslint-plugin-markdown": "^5.0.0", "eslint-plugin-markdown": "^5.0.0",
"eslint-plugin-no-only-tests": "^3.1.0", "eslint-plugin-no-only-tests": "^3.1.0",
"eslint-plugin-tsdoc": "^0.3.0", "eslint-plugin-tsdoc": "^0.3.0",
"eslint-plugin-unicorn": "^55.0.0", "eslint-plugin-unicorn": "^56.0.0",
"express": "^4.19.1", "express": "^4.19.1",
"globals": "^15.4.0", "globals": "^15.4.0",
"globby": "^14.0.1", "globby": "^14.0.1",
@@ -131,5 +132,10 @@
}, },
"nyc": { "nyc": {
"report-dir": "coverage/cypress" "report-dir": "coverage/cypress"
},
"pnpm": {
"patchedDependencies": {
"roughjs": "patches/roughjs.patch"
}
} }
} }

View File

@@ -1,5 +1,14 @@
# @mermaid-js/layout-elk # @mermaid-js/layout-elk
## 0.1.5
### Patch Changes
- [#5825](https://github.com/mermaid-js/mermaid/pull/5825) [`233e36c`](https://github.com/mermaid-js/mermaid/commit/233e36c9884fcce141a72ce7c845179781e18632) Thanks [@ashishjain0512](https://github.com/ashishjain0512)! - chore: Update render options
- Updated dependencies [[`6c5b7ce`](https://github.com/mermaid-js/mermaid/commit/6c5b7ce9f41c0fbd59fe03dbefc8418d97697f0a), [`9e3aa70`](https://github.com/mermaid-js/mermaid/commit/9e3aa705ae21fd4898504ab22d775a9e437b898e), [`de2c05c`](https://github.com/mermaid-js/mermaid/commit/de2c05cd5463af68d19dd7b6b3f1303d69ddb2dd)]:
- mermaid@11.3.0
## 0.1.4 ## 0.1.4
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mermaid-js/layout-elk", "name": "@mermaid-js/layout-elk",
"version": "0.1.4", "version": "0.1.5",
"description": "ELK layout engine for mermaid", "description": "ELK layout engine for mermaid",
"module": "dist/mermaid-layout-elk.core.mjs", "module": "dist/mermaid-layout-elk.core.mjs",
"types": "dist/layouts.d.ts", "types": "dist/layouts.d.ts",

View File

@@ -33,10 +33,11 @@ export const render = async (
}; };
graph.children.push(child); graph.children.push(child);
nodeDb[node.id] = child; nodeDb[node.id] = child;
const config = getConfig();
// Add the element to the DOM // Add the element to the DOM
if (!node.isGroup) { if (!node.isGroup) {
const childNodeEl = await insertNode(nodeEl, node, node.dir); const childNodeEl = await insertNode(nodeEl, node, { config, dir: node.dir });
boundingBox = childNodeEl.node().getBBox(); boundingBox = childNodeEl.node().getBBox();
child.domId = childNodeEl; child.domId = childNodeEl;
child.width = boundingBox.width; child.width = boundingBox.width;
@@ -50,7 +51,7 @@ export const render = async (
// @ts-ignore TODO: fix this // @ts-ignore TODO: fix this
const { shapeSvg, bbox } = await labelHelper(nodeEl, node, undefined, true); const { shapeSvg, bbox } = await labelHelper(nodeEl, node, undefined, true);
labelData.width = bbox.width; labelData.width = bbox.width;
labelData.wrappingWidth = getConfig().flowchart!.wrappingWidth; labelData.wrappingWidth = config.flowchart!.wrappingWidth;
// Give some padding for elk // Give some padding for elk
labelData.height = bbox.height - 2; labelData.height = bbox.height - 2;
labelData.labelNode = shapeSvg.node(); labelData.labelNode = shapeSvg.node();

View File

@@ -1,5 +1,35 @@
# mermaid # mermaid
## 11.3.0
### Minor Changes
- [#5825](https://github.com/mermaid-js/mermaid/pull/5825) [`9e3aa70`](https://github.com/mermaid-js/mermaid/commit/9e3aa705ae21fd4898504ab22d775a9e437b898e) Thanks [@ashishjain0512](https://github.com/ashishjain0512)! - New Flowchart Shapes (with new syntax)
### Patch Changes
- [#5849](https://github.com/mermaid-js/mermaid/pull/5849) [`6c5b7ce`](https://github.com/mermaid-js/mermaid/commit/6c5b7ce9f41c0fbd59fe03dbefc8418d97697f0a) Thanks [@ReneLombard](https://github.com/ReneLombard)! - Fixed an issue when the mermaid classdiagram crashes when adding a . to the namespace.
Forexample
```mermaid
classDiagram
namespace Company.Project.Module {
class GenericClass~T~ {
+addItem(item: T)
+getItem() T
}
}
```
- [#5914](https://github.com/mermaid-js/mermaid/pull/5914) [`de2c05c`](https://github.com/mermaid-js/mermaid/commit/de2c05cd5463af68d19dd7b6b3f1303d69ddb2dd) Thanks [@aloisklink](https://github.com/aloisklink)! - Ban DOMPurify v3.1.7 as a dependency
## 11.2.1
### Patch Changes
- [#5856](https://github.com/mermaid-js/mermaid/pull/5856) [`bfd8c63`](https://github.com/mermaid-js/mermaid/commit/bfd8c63daaa8420e57da9953922b9f0c94123064) Thanks [@knsv](https://github.com/knsv)! - Fix for issue with calculation of label width when using in firefox
## 11.2.0 ## 11.2.0
### Minor Changes ### Minor Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "mermaid", "name": "mermaid",
"version": "11.2.0", "version": "11.3.0",
"description": "Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.", "description": "Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.",
"type": "module", "type": "module",
"module": "./dist/mermaid.core.mjs", "module": "./dist/mermaid.core.mjs",
@@ -70,14 +70,16 @@
"@braintree/sanitize-url": "^7.0.1", "@braintree/sanitize-url": "^7.0.1",
"@iconify/utils": "^2.1.32", "@iconify/utils": "^2.1.32",
"@mermaid-js/parser": "workspace:^", "@mermaid-js/parser": "workspace:^",
"@types/d3": "^7.4.3",
"@types/dompurify": "^3.0.5",
"cytoscape": "^3.29.2", "cytoscape": "^3.29.2",
"cytoscape-cose-bilkent": "^4.1.0", "cytoscape-cose-bilkent": "^4.1.0",
"cytoscape-fcose": "^2.2.0", "cytoscape-fcose": "^2.2.0",
"d3": "^7.9.0", "d3": "^7.9.0",
"d3-sankey": "^0.12.3", "d3-sankey": "^0.12.3",
"dagre-d3-es": "7.0.10", "dagre-d3-es": "7.0.11",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"dompurify": "^3.0.11", "dompurify": "^3.0.11 <3.1.7",
"katex": "^0.16.9", "katex": "^0.16.9",
"khroma": "^2.1.0", "khroma": "^2.1.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
@@ -92,13 +94,11 @@
"@iconify/types": "^2.0.0", "@iconify/types": "^2.0.0",
"@types/cytoscape": "^3.21.4", "@types/cytoscape": "^3.21.4",
"@types/cytoscape-fcose": "^2.2.4", "@types/cytoscape-fcose": "^2.2.4",
"@types/d3": "^7.4.3",
"@types/d3-sankey": "^0.12.4", "@types/d3-sankey": "^0.12.4",
"@types/d3-scale": "^4.0.8", "@types/d3-scale": "^4.0.8",
"@types/d3-scale-chromatic": "^3.0.3", "@types/d3-scale-chromatic": "^3.0.3",
"@types/d3-selection": "^3.0.10", "@types/d3-selection": "^3.0.10",
"@types/d3-shape": "^3.1.6", "@types/d3-shape": "^3.1.6",
"@types/dompurify": "^3.0.5",
"@types/jsdom": "^21.1.6", "@types/jsdom": "^21.1.6",
"@types/katex": "^0.16.7", "@types/katex": "^0.16.7",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",

View File

@@ -87,7 +87,7 @@ const recursiveRender = async (_elem, graph, diagramType, id, parentCluster, sit
// insertCluster(clusters, graph.node(v)); // insertCluster(clusters, graph.node(v));
} else { } else {
log.info('Node - the non recursive path', v, node.id, node); log.info('Node - the non recursive path', v, node.id, node);
await insertNode(nodes, graph.node(v), dir); await insertNode(nodes, graph.node(v), { config: siteConfig, dir });
} }
} }
}) })

View File

@@ -1131,7 +1131,7 @@ const shapes = {
let nodeElems = {}; let nodeElems = {};
export const insertNode = async (elem, node, dir) => { export const insertNode = async (elem, node, renderOptions) => {
let newEl; let newEl;
let el; let el;
@@ -1144,9 +1144,9 @@ export const insertNode = async (elem, node, dir) => {
target = node.linkTarget || '_blank'; target = node.linkTarget || '_blank';
} }
newEl = elem.insert('svg:a').attr('xlink:href', node.link).attr('target', target); newEl = elem.insert('svg:a').attr('xlink:href', node.link).attr('target', target);
el = await shapes[node.shape](newEl, node, dir); el = await shapes[node.shape](newEl, node, renderOptions);
} else { } else {
el = await shapes[node.shape](elem, node, dir); el = await shapes[node.shape](elem, node, renderOptions);
newEl = el; newEl = el;
} }
if (node.tooltip) { if (node.tooltip) {

View File

@@ -21,8 +21,9 @@ const config: RequiredDeep<MermaidConfig> = {
// TODO: Should we replace these with `null` so that they can go in the JSON Schema? // TODO: Should we replace these with `null` so that they can go in the JSON Schema?
deterministicIDSeed: undefined, deterministicIDSeed: undefined,
elk: { elk: {
// mergeEdges is needed here to be considered
mergeEdges: false, mergeEdges: false,
nodePlacementStrategy: 'SIMPLE', nodePlacementStrategy: 'BRANDES_KOEPF',
}, },
themeCSS: undefined, themeCSS: undefined,

View File

@@ -124,7 +124,8 @@ async function calculateBlockSize(
} }
// Add the element to the DOM to size it // Add the element to the DOM to size it
const nodeEl = await insertNode(elem, node); const config = getConfig();
const nodeEl = await insertNode(elem, node, { config });
const boundingBox = nodeEl.node().getBBox(); const boundingBox = nodeEl.node().getBBox();
const obj = db.getBlock(node.id); const obj = db.getBlock(node.id);
obj.size = { width: boundingBox.width, height: boundingBox.height, x: 0, y: 0, node: nodeEl }; obj.size = { width: boundingBox.width, height: boundingBox.height, x: 0, y: 0, node: nodeEl };
@@ -138,7 +139,8 @@ export async function insertBlockPositioned(elem: any, block: Block, db: any) {
// Add the element to the DOM to size it // Add the element to the DOM to size it
const obj = db.getBlock(node.id); const obj = db.getBlock(node.id);
if (obj.type !== 'space') { if (obj.type !== 'space') {
await insertNode(elem, node); const config = getConfig();
await insertNode(elem, node, { config });
block.intersect = node?.intersect; block.intersect = node?.intersect;
positionNode(node); positionNode(node);
} }

View File

@@ -14,6 +14,78 @@ describe('given a basic class diagram, ', function () {
classDb.clear(); classDb.clear();
parser.yy = classDb; parser.yy = classDb;
}); });
it('should handle classes within namespaces', () => {
const str = `classDiagram
namespace Company.Project {
class User {
+login(username: String, password: String)
+logout()
}
}
namespace Company.Project.Module {
class Admin {
+addUser(user: User)
+removeUser(user: User)
}
}`;
parser.parse(str);
const user = classDb.getClass('User');
const admin = classDb.getClass('Admin');
// Check if the classes are correctly registered under their respective namespaces
expect(user.parent).toBe('Company.Project');
expect(admin.parent).toBe('Company.Project.Module');
expect(user.methods.length).toBe(2);
expect(admin.methods.length).toBe(2);
});
it('should handle generic classes within namespaces', () => {
const str = `classDiagram
namespace Company.Project.Module {
class GenericClass~T~ {
+addItem(item: T)
+getItem() T
}
}`;
parser.parse(str);
const genericClass = classDb.getClass('GenericClass');
expect(genericClass.type).toBe('T');
expect(genericClass.methods[0].getDisplayDetails().displayText).toBe('+addItem(item: T)');
expect(genericClass.methods[1].getDisplayDetails().displayText).toBe('+getItem() : T');
});
it('should handle nested namespaces and relationships', () => {
const str = ` classDiagram
namespace Company.Project.Module.SubModule {
class Report {
+generatePDF(data: List)
+generateCSV(data: List)
}
}
namespace Company.Project.Module {
class Admin {
+generateReport()
}
}
Admin --> Report : generates`;
parser.parse(str);
const report = classDb.getClass('Report');
const admin = classDb.getClass('Admin');
const relations = classDb.getRelations();
expect(report.parent).toBe('Company.Project.Module.SubModule');
expect(admin.parent).toBe('Company.Project.Module');
expect(relations[0].id1).toBe('Admin');
expect(relations[0].id2).toBe('Report');
expect(relations[0].title).toBe('generates');
});
it('should handle accTitle and accDescr', function () { it('should handle accTitle and accDescr', function () {
const str = `classDiagram const str = `classDiagram
accTitle: My Title accTitle: My Title
@@ -48,6 +120,22 @@ describe('given a basic class diagram, ', function () {
} }
}); });
it('should handle fully qualified class names in relationships', () => {
const str = `classDiagram
namespace Company.Project.Module {
class User
class Admin
}
Admin --> User : manages`;
parser.parse(str);
const relations = classDb.getRelations();
expect(relations[0].id1).toBe('Admin');
expect(relations[0].id2).toBe('User');
expect(relations[0].title).toBe('manages');
});
it('should handle backquoted class names', function () { it('should handle backquoted class names', function () {
const str = 'classDiagram\n' + 'class `Car`'; const str = 'classDiagram\n' + 'class `Car`';
@@ -393,6 +481,22 @@ class C13["With Città foreign language"]
Student "1" --o "1" IdCard : carries Student "1" --o "1" IdCard : carries
Student "1" --o "1" Bike : rides`); Student "1" --o "1" Bike : rides`);
const studentClass = classDb.getClasses().get('Student');
// Check that the important properties match, but ignore the domId
expect(studentClass).toMatchObject({
id: 'Student',
label: 'Student',
members: [
expect.objectContaining({
id: 'idCard : IdCard',
visibility: '-',
}),
],
methods: [],
annotations: [],
cssClasses: [],
});
expect(classDb.getClasses().size).toBe(3); expect(classDb.getClasses().size).toBe(3);
expect(classDb.getClasses().get('Student')).toMatchInlineSnapshot(` expect(classDb.getClasses().get('Student')).toMatchInlineSnapshot(`
{ {

View File

@@ -242,11 +242,13 @@ classLabel
namespaceName namespaceName
: alphaNumToken { $$=$1; } : alphaNumToken { $$=$1; }
| alphaNumToken DOT namespaceName { $$=$1+'.'+$3; }
| alphaNumToken namespaceName { $$=$1+$2; } | alphaNumToken namespaceName { $$=$1+$2; }
; ;
className className
: alphaNumToken { $$=$1; } : alphaNumToken { $$=$1; }
| alphaNumToken DOT className { $$=$1+'.'+$3; }
| classLiteralName { $$=$1; } | classLiteralName { $$=$1; }
| alphaNumToken className { $$=$1+$2; } | alphaNumToken className { $$=$1+$2; }
| alphaNumToken GENERICTYPE { $$=$1+'~'+$2+'~'; } | alphaNumToken GENERICTYPE { $$=$1+'~'+$2+'~'; }
@@ -272,12 +274,12 @@ statement
; ;
namespaceStatement namespaceStatement
: namespaceIdentifier STRUCT_START classStatements STRUCT_STOP {yy.addClassesToNamespace($1, $3);} : namespaceIdentifier STRUCT_START classStatements STRUCT_STOP { yy.addClassesToNamespace($1, $3); }
| namespaceIdentifier STRUCT_START NEWLINE classStatements STRUCT_STOP {yy.addClassesToNamespace($1, $4);} | namespaceIdentifier STRUCT_START NEWLINE classStatements STRUCT_STOP { yy.addClassesToNamespace($1, $4); }
; ;
namespaceIdentifier namespaceIdentifier
: NAMESPACE namespaceName {$$=$2; yy.addNamespace($2);} : NAMESPACE namespaceName { $$=$2; yy.addNamespace($2); }
; ;
classStatements classStatements

View File

@@ -4,6 +4,7 @@ import { getConfig, defaultConfig } from '../../diagram-api/diagramAPI.js';
import common from '../common/common.js'; import common from '../common/common.js';
import type { Node, Edge } from '../../rendering-util/types.js'; import type { Node, Edge } from '../../rendering-util/types.js';
import { log } from '../../logger.js'; import { log } from '../../logger.js';
import * as yaml from 'js-yaml';
import { import {
setAccTitle, setAccTitle,
getAccTitle, getAccTitle,
@@ -14,6 +15,7 @@ import {
getDiagramTitle, getDiagramTitle,
} from '../common/commonDb.js'; } from '../common/commonDb.js';
import type { FlowVertex, FlowClass, FlowSubGraph, FlowText, FlowEdge, FlowLink } from './types.js'; import type { FlowVertex, FlowClass, FlowSubGraph, FlowText, FlowEdge, FlowLink } from './types.js';
import type { NodeMetaData } from '../../types.js';
const MERMAID_DOM_ID_PREFIX = 'flowchart-'; const MERMAID_DOM_ID_PREFIX = 'flowchart-';
let vertexCounter = 0; let vertexCounter = 0;
@@ -60,8 +62,10 @@ export const addVertex = function (
style: string[], style: string[],
classes: string[], classes: string[],
dir: string, dir: string,
props = {} props = {},
shapeData: any
) { ) {
// console.log('addVertex', id, shapeData);
if (!id || id.trim().length === 0) { if (!id || id.trim().length === 0) {
return; return;
} }
@@ -115,6 +119,59 @@ export const addVertex = function (
} else if (props !== undefined) { } else if (props !== undefined) {
Object.assign(vertex.props, props); Object.assign(vertex.props, props);
} }
if (shapeData !== undefined) {
let yamlData;
// detect if shapeData contains a newline character
// console.log('shapeData', shapeData);
if (!shapeData.includes('\n')) {
// console.log('yamlData shapeData has no new lines', shapeData);
yamlData = '{\n' + shapeData + '\n}';
} else {
// console.log('yamlData shapeData has new lines', shapeData);
yamlData = shapeData + '\n';
}
// console.log('yamlData', yamlData);
const doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA }) as NodeMetaData;
if (doc.shape && (doc.shape !== doc.shape.toLowerCase() || doc.shape.includes('_'))) {
throw new Error(`No such shape: ${doc.shape}. Shape names should be lowercase.`);
}
// console.log('yamlData doc', doc);
if (doc?.shape) {
vertex.type = doc?.shape;
}
if (doc?.label) {
vertex.text = doc?.label;
}
if (doc?.icon) {
vertex.icon = doc?.icon;
if (!doc.label?.trim() && vertex.text === id) {
vertex.text = '';
}
}
if (doc?.form) {
vertex.form = doc?.form;
}
if (doc?.pos) {
vertex.pos = doc?.pos;
}
if (doc?.img) {
vertex.img = doc?.img;
if (!doc.label?.trim() && vertex.text === id) {
vertex.text = '';
}
}
if (doc?.constraint) {
vertex.constraint = doc.constraint;
}
if (doc.w) {
vertex.assetWidth = Number(doc.w);
}
if (doc.h) {
vertex.assetHeight = Number(doc.h);
}
}
}; };
/** /**
@@ -760,6 +817,21 @@ export const lex = {
}; };
const getTypeFromVertex = (vertex: FlowVertex) => { const getTypeFromVertex = (vertex: FlowVertex) => {
if (vertex.img) {
return 'imageSquare';
}
if (vertex.icon) {
if (vertex.form === 'circle') {
return 'iconCircle';
}
if (vertex.form === 'square') {
return 'iconSquare';
}
if (vertex.form === 'rounded') {
return 'iconRounded';
}
return 'icon';
}
if (vertex.type === 'square') { if (vertex.type === 'square') {
return 'squareRect'; return 'squareRect';
} }
@@ -825,6 +897,12 @@ const addNodeFromVertex = (
link: vertex.link, link: vertex.link,
linkTarget: vertex.linkTarget, linkTarget: vertex.linkTarget,
tooltip: getTooltip(vertex.id), tooltip: getTooltip(vertex.id),
icon: vertex.icon,
pos: vertex.pos,
img: vertex.img,
assetWidth: vertex.assetWidth,
assetHeight: vertex.assetHeight,
constraint: vertex.constraint,
}); });
} }
}; };

View File

@@ -0,0 +1,278 @@
import flowDb from '../flowDb.js';
import flow from './flow.jison';
import { setConfig } from '../../../config.js';
setConfig({
securityLevel: 'strict',
});
describe('when parsing directions', function () {
beforeEach(function () {
flow.parser.yy = flowDb;
flow.parser.yy.clear();
flow.parser.yy.setGen('gen-2');
});
it('should handle basic shape data statements', function () {
const res = flow.parser.parse(`flowchart TB
D@{ shape: rounded}`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('D');
});
it('should handle basic shape data statements', function () {
const res = flow.parser.parse(`flowchart TB
D@{ shape: rounded }`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('D');
});
it('should handle basic shape data statements with &', function () {
const res = flow.parser.parse(`flowchart TB
D@{ shape: rounded } & E`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(2);
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('D');
expect(data4Layout.nodes[1].label).toEqual('E');
});
it('should handle shape data statements with edges', function () {
const res = flow.parser.parse(`flowchart TB
D@{ shape: rounded } --> E`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(2);
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('D');
expect(data4Layout.nodes[1].label).toEqual('E');
});
it('should handle basic shape data statements with amp and edges 1', function () {
const res = flow.parser.parse(`flowchart TB
D@{ shape: rounded } & E --> F`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(3);
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('D');
expect(data4Layout.nodes[1].label).toEqual('E');
});
it('should handle basic shape data statements with amp and edges 2', function () {
const res = flow.parser.parse(`flowchart TB
D@{ shape: rounded } & E@{ shape: rounded } --> F`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(3);
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('D');
expect(data4Layout.nodes[1].label).toEqual('E');
});
it('should handle basic shape data statements with amp and edges 3', function () {
const res = flow.parser.parse(`flowchart TB
D@{ shape: rounded } & E@{ shape: rounded } --> F & G@{ shape: rounded }`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(4);
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('D');
expect(data4Layout.nodes[1].label).toEqual('E');
});
it('should handle basic shape data statements with amp and edges 4', function () {
const res = flow.parser.parse(`flowchart TB
D@{ shape: rounded } & E@{ shape: rounded } --> F@{ shape: rounded } & G@{ shape: rounded }`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(4);
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('D');
expect(data4Layout.nodes[1].label).toEqual('E');
});
it('should handle basic shape data statements with amp and edges 5, trailing space', function () {
const res = flow.parser.parse(`flowchart TB
D@{ shape: rounded } & E@{ shape: rounded } --> F{ shape: rounded } & G{ shape: rounded } `);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(4);
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('D');
expect(data4Layout.nodes[1].label).toEqual('E');
});
it('should no matter of there are no leading spaces', function () {
const res = flow.parser.parse(`flowchart TB
D@{shape: rounded}`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('D');
});
it('should no matter of there are many leading spaces', function () {
const res = flow.parser.parse(`flowchart TB
D@{ shape: rounded}`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('D');
});
it('should be forgiving with many spaces before teh end', function () {
const res = flow.parser.parse(`flowchart TB
D@{ shape: rounded }`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('D');
});
it('should be possible to add multiple properties on the same line', function () {
const res = flow.parser.parse(`flowchart TB
D@{ shape: rounded , label: "DD"}`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('DD');
});
it('should be possible to link to a node with more data', function () {
const res = flow.parser.parse(`flowchart TB
A --> D@{
shape: circle
other: "clock"
}
`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(2);
expect(data4Layout.nodes[0].shape).toEqual('squareRect');
expect(data4Layout.nodes[0].label).toEqual('A');
expect(data4Layout.nodes[1].label).toEqual('D');
expect(data4Layout.nodes[1].shape).toEqual('circle');
expect(data4Layout.edges.length).toBe(1);
});
it('should not disturb adding multiple nodes after each other', function () {
const res = flow.parser.parse(`flowchart TB
A[hello]
B@{
shape: circle
other: "clock"
}
C[Hello]@{
shape: circle
other: "clock"
}
`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(3);
expect(data4Layout.nodes[0].shape).toEqual('squareRect');
expect(data4Layout.nodes[0].label).toEqual('hello');
expect(data4Layout.nodes[1].shape).toEqual('circle');
expect(data4Layout.nodes[1].label).toEqual('B');
expect(data4Layout.nodes[2].shape).toEqual('circle');
expect(data4Layout.nodes[2].label).toEqual('Hello');
});
it('should use handle bracket end (}) character inside the shape data', function () {
const res = flow.parser.parse(`flowchart TB
A@{
label: "This is }"
other: "clock"
}
`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('squareRect');
expect(data4Layout.nodes[0].label).toEqual('This is }');
});
it('Diamond shapes should work as usual', function () {
const res = flow.parser.parse(`flowchart TB
A{This is a label}
`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('diamond');
expect(data4Layout.nodes[0].label).toEqual('This is a label');
});
it('Multi line strings should be supported', function () {
const res = flow.parser.parse(`flowchart TB
A@{
label: |
This is a
multiline string
other: "clock"
}
`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('squareRect');
expect(data4Layout.nodes[0].label).toEqual('This is a\nmultiline string\n');
});
it('Multi line strings should be supported', function () {
const res = flow.parser.parse(`flowchart TB
A@{
label: "This is a
multiline string"
other: "clock"
}
`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('squareRect');
expect(data4Layout.nodes[0].label).toEqual('This is a<br/>multiline string');
});
it(' should be possible to use } in strings', function () {
const res = flow.parser.parse(`flowchart TB
A@{
label: "This is a string with }"
other: "clock"
}
`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('squareRect');
expect(data4Layout.nodes[0].label).toEqual('This is a string with }');
});
it(' should be possible to use @ in strings', function () {
const res = flow.parser.parse(`flowchart TB
A@{
label: "This is a string with @"
other: "clock"
}
`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('squareRect');
expect(data4Layout.nodes[0].label).toEqual('This is a string with @');
});
it(' should be possible to use @ in strings', function () {
const res = flow.parser.parse(`flowchart TB
A@{
label: "This is a string with}"
other: "clock"
}
`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('squareRect');
expect(data4Layout.nodes[0].label).toEqual('This is a string with}');
});
});

View File

@@ -23,6 +23,9 @@
%x href %x href
%x callbackname %x callbackname
%x callbackargs %x callbackargs
%x shapeData
%x shapeDataStr
%x shapeDataEndBracket
%% %%
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; } accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
@@ -34,6 +37,32 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multilin
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value"; <acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
// <acc_descr_multiline>.*[^\n]* { return "acc_descr_line"} // <acc_descr_multiline>.*[^\n]* { return "acc_descr_line"}
\@\{ {
// console.log('=> shapeData', yytext);
this.pushState("shapeData"); yytext=""; return 'SHAPE_DATA' }
<shapeData>["] {
// console.log('=> shapeDataStr', yytext);
this.pushState("shapeDataStr");
return 'SHAPE_DATA';
}
<shapeDataStr>["] {
// console.log('shapeData <==', yytext);
this.popState(); return 'SHAPE_DATA'}
<shapeDataStr>[^\"]+ {
// console.log('shapeData', yytext);
const re = /\n\s*/g;
yytext = yytext.replace(re,"<br/>");
return 'SHAPE_DATA'}
<shapeData>[^}^"]+ {
// console.log('shapeData', yytext);
return 'SHAPE_DATA';
}
<shapeData>"}" {
// console.log('<== root', yytext)
this.popState();
}
/* /*
---interactivity command--- ---interactivity command---
'call' adds a callback to the specified node. 'call' can only be specified when 'call' adds a callback to the specified node. 'call' can only be specified when
@@ -49,10 +78,11 @@ Function arguments are optional: 'call <callbackname>()' simply executes 'callba
<callbackargs>\) this.popState(); <callbackargs>\) this.popState();
<callbackargs>[^)]* return 'CALLBACKARGS'; <callbackargs>[^)]* return 'CALLBACKARGS';
<md_string>[^`"]+ { return "MD_STR";} <md_string>[^`"]+ { return "MD_STR";}
<md_string>[`]["] { this.popState();} <md_string>[`]["] { this.popState();}
<*>["][`] { this.begin("md_string");} <*>["][`] { this.begin("md_string");}
<string>[^"]+ return "STR"; <string>[^"]+ { return "STR"; }
<string>["] this.popState(); <string>["] this.popState();
<*>["] this.pushState("string"); <*>["] this.pushState("string");
"style" return 'STYLE'; "style" return 'STYLE';
@@ -62,6 +92,8 @@ Function arguments are optional: 'call <callbackname>()' simply executes 'callba
"classDef" return 'CLASSDEF'; "classDef" return 'CLASSDEF';
"class" return 'CLASS'; "class" return 'CLASS';
/* /*
---interactivity command--- ---interactivity command---
'href' adds a link to the specified node. 'href' can only be specified when the 'href' adds a link to the specified node. 'href' can only be specified when the
@@ -356,23 +388,38 @@ statement
separator: NEWLINE | SEMI | EOF ; separator: NEWLINE | SEMI | EOF ;
shapeData:
shapeData SHAPE_DATA
{ $$ = $1 + $2; }
| SHAPE_DATA
{ $$ = $1; }
;
vertexStatement: vertexStatement link node vertexStatement: vertexStatement link node shapeData
{ /* console.warn('vs',$vertexStatement.stmt,$node); */ yy.addLink($vertexStatement.stmt,$node,$link); $$ = { stmt: $node, nodes: $node.concat($vertexStatement.nodes) } } { /* console.warn('vs shapeData',$vertexStatement.stmt,$node, $shapeData);*/ yy.addVertex($node[0],undefined,undefined,undefined, undefined,undefined, undefined,$shapeData); yy.addLink($vertexStatement.stmt,$node,$link); $$ = { stmt: $node, nodes: $node.concat($vertexStatement.nodes) } }
| vertexStatement link node
{ /*console.warn('vs',$vertexStatement.stmt,$node);*/ yy.addLink($vertexStatement.stmt,$node,$link); $$ = { stmt: $node, nodes: $node.concat($vertexStatement.nodes) } }
| vertexStatement link node spaceList | vertexStatement link node spaceList
{ /* console.warn('vs',$vertexStatement.stmt,$node); */ yy.addLink($vertexStatement.stmt,$node,$link); $$ = { stmt: $node, nodes: $node.concat($vertexStatement.nodes) } } { /* console.warn('vs',$vertexStatement.stmt,$node); */ yy.addLink($vertexStatement.stmt,$node,$link); $$ = { stmt: $node, nodes: $node.concat($vertexStatement.nodes) } }
|node spaceList {/*console.warn('noda', $node);*/ $$ = {stmt: $node, nodes:$node }} |node spaceList { /*console.warn('vertexStatement: node spaceList', $node);*/ $$ = {stmt: $node, nodes:$node }}
|node { /*console.warn('noda', $node);*/ $$ = {stmt: $node, nodes:$node }} |node shapeData {
/*console.warn('vertexStatement: node shapeData', $node[0], $shapeData);*/
yy.addVertex($node[0],undefined,undefined,undefined, undefined,undefined, undefined,$shapeData);
$$ = {stmt: $node, nodes:$node, shapeData: $shapeData}
}
|node { /* console.warn('vertexStatement: single node', $node); */ $$ = {stmt: $node, nodes:$node }}
; ;
node: styledVertex node: styledVertex
{ /* console.warn('nod', $styledVertex); */ $$ = [$styledVertex];} { /*console.warn('nod', $styledVertex);*/ $$ = [$styledVertex];}
| node shapeData spaceList AMP spaceList styledVertex
{ yy.addVertex($node[0],undefined,undefined,undefined, undefined,undefined, undefined,$shapeData); $$ = $node.concat($styledVertex); /*console.warn('pip2', $node[0], $styledVertex, $$);*/ }
| node spaceList AMP spaceList styledVertex | node spaceList AMP spaceList styledVertex
{ $$ = $node.concat($styledVertex); /* console.warn('pip', $node[0], $styledVertex, $$); */ } { $$ = $node.concat($styledVertex); /*console.warn('pip', $node[0], $styledVertex, $$);*/ }
; ;
styledVertex: vertex styledVertex: vertex
{ /* console.warn('nod', $vertex); */ $$ = $vertex;} { /* console.warn('nodc', $vertex);*/ $$ = $vertex;}
| vertex STYLE_SEPARATOR idString | vertex STYLE_SEPARATOR idString
{$$ = $vertex;yy.setClass($vertex,$idString)} {$$ = $vertex;yy.setClass($vertex,$idString)}
; ;

View File

@@ -59,7 +59,7 @@ const getStyles = (options: FlowChartStyleOptions) =>
stroke: ${options.nodeBorder}; stroke: ${options.nodeBorder};
stroke-width: 1px; stroke-width: 1px;
} }
.rough-node .label text , .node .label text { .rough-node .label text , .node .label text, .image-shape .label, .icon-shape .label {
text-anchor: middle; text-anchor: middle;
} }
// .flowchart-label .text-outer-tspan { // .flowchart-label .text-outer-tspan {
@@ -75,13 +75,20 @@ const getStyles = (options: FlowChartStyleOptions) =>
stroke-width: 1px; stroke-width: 1px;
} }
.node .label { .rough-node .label,.node .label, .image-shape .label, .icon-shape .label {
text-align: center; text-align: center;
} }
.node.clickable { .node.clickable {
cursor: pointer; cursor: pointer;
} }
.root .anchor path {
fill: ${options.lineColor} !important;
stroke-width: 0;
stroke: ${options.lineColor};
}
.arrowheadPath { .arrowheadPath {
fill: ${options.arrowheadColor}; fill: ${options.arrowheadColor};
} }
@@ -151,6 +158,25 @@ const getStyles = (options: FlowChartStyleOptions) =>
font-size: 18px; font-size: 18px;
fill: ${options.textColor}; fill: ${options.textColor};
} }
rect.text {
fill: none;
stroke-width: 0;
}
.icon-shape, .image-shape {
background-color: ${options.edgeLabelBackground};
p {
background-color: ${options.edgeLabelBackground};
padding: 2px;
}
rect {
opacity: 0.5;
background-color: ${options.edgeLabelBackground};
fill: ${options.edgeLabelBackground};
}
text-align: center;
}
`; `;
export default getStyles; export default getStyles;

View File

@@ -11,6 +11,15 @@ export interface FlowVertex {
styles: string[]; styles: string[];
text?: string; text?: string;
type?: string; type?: string;
icon?: string;
form?: string;
pos?: 't' | 'b';
img?: string;
assetWidth?: number;
assetHeight?: number;
defaultWidth?: number;
imageAspectRatio?: number;
constraint?: 'on' | 'off';
} }
export interface FlowText { export interface FlowText {

View File

@@ -1,11 +1,11 @@
<template> <template>
<div class="-mt-6 mb-8"> <div class="-mt-6 mb-8">
<a <a
href="https://www.producthunt.com/products/mermaid-chart?utm_source=badge-follow&utm_medium=badge&utm_souce=badge-mermaid&#0045;chart" href="https://www.producthunt.com/posts/mermaid-whiteboard?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-mermaid&#0045;whiteboard"
target="_blank" target="_blank"
><img ><img
src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=552855&theme=light" src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=486720&theme=light"
alt="Mermaid&#0032;Chart - A&#0032;smarter&#0032;way&#0032;to&#0032;create&#0032;diagrams | Product Hunt" alt="Mermaid&#0032;Whiteboard - Drag&#0032;&#0038;&#0032;Drop&#0032;your&#0032;Nodes&#0032;with&#0032;Mermaid&#0039;s&#0032;new&#0032;Whiteboard&#0033; | Product Hunt"
style="width: 250px; height: 54px" style="width: 250px; height: 54px"
width="250" width="250"
height="54" height="54"

View File

@@ -0,0 +1,220 @@
# Custom SVG Shapes Library
This library provides a collection of custom SVG shapes, utilities, and helpers for generating diagram components. The shapes are designed to be used within an SVG container and include a variety of common and complex shapes.
## Overview
## Shape Helpers and Utilities
Before starting with shape creation, it's essential to familiarize yourself with the utilities provided in the `utils.ts` file from `packages/mermaid/src/rendering-util/rendering-elements/shapes/util.js`. These utilities are designed to assist with various aspects of SVG shape manipulation and ensure consistent and accurate rendering.
## Available Utilities
### 1. `labelHelper`
- **Purpose**: This function creates and inserts labels inside SVG shapes.
- **Features**:
- Handles both HTML labels and plain text.
- Calculates the bounding box dimensions of the label.
- Ensures proper positioning of labels within shapes.
### 2. `updateNodeBounds`
- **Purpose**: Updates the bounding box dimensions (width and height) of a node.
- **Usage**:
- Adjusts the size of the node to fit the content or shape.
- Useful for ensuring that shapes resize appropriately based on their content.
### 3. `insertPolygonShape`
- **Purpose**: Inserts a polygon shape into an SVG container.
- **Features**:
- Handles the creation and insertion of complex polygonal shapes.
- Configures the shape's appearance and positioning within the SVG container.
### 4. `getNodeClasses`
- **Purpose**: Returns the appropriate CSS classes for a node based on its configuration.
- **Usage**:
- Dynamically applies CSS classes to nodes for styling purposes.
- Ensures that nodes adhere to the desired design and theme.
### 5. `createPathFromPoints`
- **Purpose**: Generates an SVG path string from an array of points.
- **Usage**:
- Converts a list of points into a smooth path.
- Useful for creating custom shapes or paths within the SVG.
### 6. `generateFullSineWavePoints`
- **Purpose**: Generates points for a sine wave, useful for creating wavy-edged shapes.
- **Usage**:
- Facilitates the creation of shapes with wavy or sine-wave edges.
- Can be used to add decorative or dynamic edges to shapes.
## Getting Started
To utilize these utilities, simply import them from the `utils.ts` file into your shape creation script. These helpers will streamline the process of building and customizing SVG shapes, ensuring consistent results across your projects.
```typescript
import {
labelHelper,
updateNodeBounds,
insertPolygonShape,
getNodeClasses,
createPathFromPoints,
generateFullSineWavePoints,
} from './utils.ts';
```
## Example Usage
Heres a basic example of how you might use some of these utilities:
```typescript
import { labelHelper, insertPolygonShape } from './utils.ts';
const svgContainer = document.getElementById('svgContainer');
// Insert a polygon shape
insertPolygonShape(svgContainer /* shape-specific parameters */);
// Create and insert a label inside the shape
labelHelper(svgContainer /* label-specific parameters */);
```
## Adding New Shapes
### 1. Create the Shape Function
To add a new shape:
- **Create the shape function**: Create a new file of name of the shape and export a function in the `shapes` directory that generates your shape. The file and function should follow the pattern used in existing shapes and return an SVG element.
- **Example**:
```typescript
import { Node, RenderOptions } from '../../types.d.ts';
export const myNewShape = async (
parent: SVGAElement,
node: Node,
renderOptions: RenderOptions
) => {
// Create your shape here
const shape = parent.insert('g').attr('class', 'my-new-shape');
// Add other elements or styles as needed
return shape;
};
```
### 2. Register the Shape
- **Register the shape**: Add your shape to the `shapes` object in the main shapes module. This allows your shape to be recognized and used within the system.
- **Example**:
```typescript
import { myNewShape } from './shapes/myNewShape';
const shapes = {
...,
'my-new-shape': myNewShape,
// Shortened alias (if any).
'm-nsh': myNewShape
};
```
# Shape Intersection Algorithms
This contains algorithms and utilities for calculating intersection points for various shapes in SVG. Arrow intersection points are crucial for accurately determining where arrows connect with shapes. Ensuring precise intersection points enhances the clarity and accuracy of flowcharts and diagrams.
## Shape Intersection Functions
### 1. `Ellipse`
Calculates the intersection points for an ellipse.
**Usage**:
```javascript
import intersectEllipse from './intersect-ellipse.js';
const intersection = intersectEllipse(node, rx, ry, point);
```
- **Parameters**:
- `node`: The SVG node element.
- `rx`: The x-radius of the ellipse.
- `ry`: The y-radius of the ellipse.
- `point`: The point from which the intersection is calculated.
### 2. `intersectRect`
Calculates the intersection points for a rectangle.
**Usage**:
```javascript
import intersectRect from './intersect-rect.js';
const intersection = intersectRect(node, point);
```
- **Parameters**:
- `node`: The SVG node element.
- `point`: The point from which the intersection is calculated.
### 3. `intersectPolygon`
Calculates the intersection points for a polygon.
**Usage**:
```javascript
import intersectPolygon from './intersect-polygon.js';
const intersection = intersectPolygon(node, polyPoints, point);
```
- **Parameters**:
- `node`: The SVG node element.
- `polyPoints`: Array of points defining the polygon.
- `point`: The point from which the intersection is calculated.
## Cypress Tests
To ensure the robustness of the flowchart shapes, there are implementation of comprehensive Cypress test cases in `newShapes.spec.ts` file. These tests cover various aspects such as:
- **Shapes**: Testing new shapes like `bowTieRect`, `waveRectangle`, `trapezoidalPentagon`, etc.
- **Looks**: Verifying shapes under different visual styles (`classic` and `handDrawn`).
- **Directions**: Ensuring correct rendering in all flow directions of arrows :
- `TB` `(Top -> Bottom)`
- `BT` `(Bottom -> Top)`
- `LR` `(Left -> Right)`
- `RL` `(Right -> Left)`
- **Labels**: Testing shapes with different labels, including:
- No labels
- Short labels
- Very long labels
- Markdown with `htmlLabels:true` and `htmlLabels:false`
- **Styles**: Applying custom styles to shapes and verifying correct rendering.
- **Class Definitions**: Using `classDef` to apply custom classes and testing their impact.
### Running the Tests
To run the Cypress tests, follow these steps:
1. Ensure you have all dependencies installed by running:
```bash
pnpm install
```
2. Start the Cypress test runner:
```bash
cypress open --env updateSnapshots=true
```
3. Select the test suite from the Cypress interface to run all the flowchart shape tests.

View File

@@ -59,7 +59,7 @@ To add an integration to this list, see the [Integrations - create page](./integ
- [Mermaid Flow Visual Editor](https://www.mermaidflow.app) ✅ - [Mermaid Flow Visual Editor](https://www.mermaidflow.app) ✅
- [Mermerd](https://github.com/KarnerTh/mermerd) - [Mermerd](https://github.com/KarnerTh/mermerd)
- [Slab](https://slab.com) ✅ - [Slab](https://slab.com) ✅
- [Swimm](https://docs.swimm.io/features/diagrams-and-charts/#mermaid--swimm--up-to-date-diagrams-) ✅ - [Swimm](https://docs.swimm.io/features/diagrams-and-charts) ✅
- [NotesHub](https://noteshub.app) ✅ - [NotesHub](https://noteshub.app) ✅
- [Notion](https://notion.so) ✅ - [Notion](https://notion.so) ✅
- [Observable](https://observablehq.com/@observablehq/mermaid) ✅ - [Observable](https://observablehq.com/@observablehq/mermaid) ✅

View File

@@ -6,7 +6,7 @@ Try the Ultimate AI, Mermaid, and Visual Diagramming Suite by creating an accoun
<br /> <br />
<a href="https://www.producthunt.com/products/mermaid-chart?utm_source=badge-follow&utm_medium=badge&utm_souce=badge-mermaid&#0045;chart" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=552855&theme=light" alt="Mermaid&#0032;Chart - A&#0032;smarter&#0032;way&#0032;to&#0032;create&#0032;diagrams | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/mermaid-whiteboard?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-mermaid&#0045;whiteboard" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=486720&theme=light" alt="Mermaid&#0032;Whiteboard - Drag&#0032;&#0038;&#0032;Drop&#0032;your&#0032;Nodes&#0032;with&#0032;Mermaid&#0039;s&#0032;new&#0032;Whiteboard&#0033; | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
## About ## About
@@ -16,17 +16,20 @@ Try the Ultimate AI, Mermaid, and Visual Diagramming Suite by creating an accoun
- **Editor** - A web based editor for creating and editing Mermaid diagrams. - **Editor** - A web based editor for creating and editing Mermaid diagrams.
- **Visual Editor** - The Visual Editor enables users of all skill levels to create diagrams easily and efficiently, with both GUI and code-based editing options. - **Mermaid AI** - Use our embedded AI Chat to generate diagrams from natural language descriptions.
- **AI Chat** - Use our embedded AI Chat to generate diagrams from natural language descriptions. - **Whiteboard** - A virtual whiteboard for creating and editing Mermaid diagrams.
- **Plugins** - A plugin system for extending the functionality of Mermaid. - **Plugins** - A plugin system for extending the functionality of Mermaid.
Official Mermaid Chart plugins: Official Mermaid Chart plugins:
- [Mermaid Chart GPT](https://chat.openai.com/g/g-1IRFKwq4G-mermaid-chart) - [Mermaid Chart GPT](https://chat.openai.com/g/g-1IRFKwq4G-mermaid-chart)
- [Confluence](https://marketplace.atlassian.com/apps/1234056/mermaid-chart-for-confluence?hosting=cloud&tab=overview)
- [Jira](https://marketplace.atlassian.com/apps/1234810/mermaid-chart-for-jira?tab=overview&hosting=cloud)
- [Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=MermaidChart.vscode-mermaid-chart) - [Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=MermaidChart.vscode-mermaid-chart)
- [JetBrains IDE](https://plugins.jetbrains.com/plugin/23043-mermaid-chart) - [JetBrains IDE](https://plugins.jetbrains.com/plugin/23043-mermaid-chart)
- [Google Docs](https://gsuite.google.com/marketplace/app/mermaidchart/947683068472)
- [Microsoft PowerPoint and Word](https://appsource.microsoft.com/en-us/product/office/WA200006214?tab=Overview) - [Microsoft PowerPoint and Word](https://appsource.microsoft.com/en-us/product/office/WA200006214?tab=Overview)
Visit our [Plugins](https://www.mermaidchart.com/plugins) page for more information. Visit our [Plugins](https://www.mermaidchart.com/plugins) page for more information.

View File

@@ -1,5 +1,11 @@
# Blog # Blog
## [Expanding the Horizons of Mermaid Flowcharts: Introducing 30 New Shapes!](https://www.mermaidchart.com/blog/posts/new-mermaid-flowchart-shapes/)
24 September 2024 · 5 mins
Discover 30 new shapes in Mermaid flowcharts, offering enhanced clarity, customization, and versatility for more dynamic and expressive visualizations.
## [Introducing Architecture Diagrams in Mermaid](https://www.mermaidchart.com/blog/posts/mermaid-supports-architecture-diagrams/) ## [Introducing Architecture Diagrams in Mermaid](https://www.mermaidchart.com/blog/posts/mermaid-supports-architecture-diagrams/)
2 September 2024 · 2 mins 2 September 2024 · 2 mins

View File

@@ -153,6 +153,19 @@ block-beta
This example demonstrates how Mermaid dynamically adjusts the width of the columns to accommodate the widest block, in this case, 'a' and the composite block 'e'. This dynamic adjustment is essential for creating visually balanced and easy-to-understand diagrams. This example demonstrates how Mermaid dynamically adjusts the width of the columns to accommodate the widest block, in this case, 'a' and the composite block 'e'. This dynamic adjustment is essential for creating visually balanced and easy-to-understand diagrams.
**Merging Blocks Horizontally:**
In scenarios where you need to stack blocks horizontally, you can use column width to accomplish the task. Blocks can be arranged vertically by putting them in a single column. Here is how you can create a block diagram in which 4 blocks are stacked on top of each other:
```mermaid-example
block-beta
block
columns 1
a["A label"] b c d
end
```
In this example, the width of the merged block dynamically adjusts to the width of the largest child block.
With these advanced configuration options, Mermaid's block diagrams can be tailored to represent a wide array of complex systems and structures. The flexibility offered by these features enables users to create diagrams that are both informative and visually appealing. In the following sections, we will explore further capabilities, including different block shapes and linking options. With these advanced configuration options, Mermaid's block diagrams can be tailored to represent a wide array of complex systems and structures. The flexibility offered by these features enables users to create diagrams that are both informative and visually appealing. In the following sections, we will explore further capabilities, including different block shapes and linking options.
## 4. Block Varieties and Shapes ## 4. Block Varieties and Shapes

View File

@@ -194,6 +194,400 @@ flowchart TD
id1(((This is the text in the circle))) id1(((This is the text in the circle)))
``` ```
## Expanded Node Shapes in Mermaid Flowcharts (v11.3.0+)
Mermaid introduces 30 new shapes to enhance the flexibility and precision of flowchart creation. These new shapes provide more options to represent processes, decisions, events, data storage visually, and other elements within your flowcharts, improving clarity and semantic meaning.
New Syntax for Shape Definition
Mermaid now supports a general syntax for defining shape types to accommodate the growing number of shapes. This syntax allows you to assign specific shapes to nodes using a clear and flexible format:
```
A@{ shape: rect }
```
This syntax creates a node A as a rectangle. It renders in the same way as `A["A"]`, or `A`.
### Complete List of New Shapes
Below is a comprehensive list of the newly introduced shapes and their corresponding semantic meanings, short names, and aliases:
| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** |
| ------------------------------------- | ---------------------- | -------------- | ------------------------------ | -------------------------------------------------------------- |
| **Process** | Rectangle | `rect` | Standard process shape | `proc`, `process`, `rectangle` |
| **Event** | Rounded Rectangle | `rounded` | Represents an event | `event` |
| **Terminal Point** | Stadium | `stadium` | Terminal point | `terminal`, `pill` |
| **Subprocess** | Framed Rectangle | `fr-rect` | Subprocess | `subprocess`,`subproc`, `framed-rectangle`, `subroutine` |
| **Database** | Cylinder | `cyl` | Database storage | `db`, `database`, `cylinder` |
| **Start** | Circle | `circle` | Starting point | `circ` |
| **Odd** | Odd | `odd` | Odd shape | |
| **Decision** | Diamond | `diam` | Decision-making step | `decision`, `diamond` |
| **Prepare Conditional** | Hexagon | `hex` | Preparation or condition step | `hexagon`, `prepare` |
| **Data Input/Output** | Lean Right | `lean-r` | Represents input or output | `lean-right`, `in-out` |
| **Data Input/Output** | Lean Left | `lean-l` | Represents output or input | `lean-left`, `out-in` |
| **Priority Action** | Trapezoid Base Bottom | `trap-b` | Priority action | `priority`, `trapezoid-bottom` |
| **Manual Operation** | Trapezoid Base Top | `trap-t` | Represents a manual task | `manual`, `trapezoid-top` |
| **Stop** | Double Circle | `dbl-circ` | Represents a stop point | `double-circle` |
| **Text Block** | Text Block | `text` | Text block | - |
| **Card** | Notched Rectangle | `notch-rect` | Represents a card | `card`, `notched-rectangle` |
| **Lined/Shaded Process** | Lined Rectangle | `lin-rect` | Lined process shape | `lined-rectangle`,`lined-process`, `lin-proc`,`shaded-process` |
| **Start** | Small Circle | `sm-circ` | Small starting point | `start`, `small-circle` |
| **Stop** | Framed Circle | `fr-circ` | Stop point | `stop`, `framed-circle` |
| **Fork/Join** | Filled Rectangle | `fork` | Fork or join in process flow | `join` |
| **Collate** | Hourglass | `hourglass` | Represents a collate operation | `hourglass` |
| **Comment** | Curly Brace | `brace` | Adds a comment | `comment`, `brace-l` |
| **Comment Right** | Curly Brace | `brace-r` | Adds a comment | - |
| **Comment with braces on both sides** | Curly Braces | `braces` | Adds a comment | - |
| **Com Link** | Lightning Bolt | `bolt` | Communication link | `com-link`, `lightning-bolt` |
| **Document** | Document | `doc` | Represents a document | `doc`, `document` |
| **Delay** | Half-Rounded Rectangle | `delay` | Represents a delay | `half-rounded-rectangle` |
| **Direct Access Storage** | Horizontal Cylinder | `h-cyl` | Direct access storage | `das`, `horizontal-cylinder` |
| **Disk Storage** | Lined Cylinder | `lin-cyl` | Disk storage | `disk`, `lined-cylinder` |
| **Display** | Curved Trapezoid | `curv-trap` | Represents a display | `curved-trapezoid`, `display` |
| **Divided Process** | Divided Rectangle | `div-rect` | Divided process shape | `div-proc`, `divided-rectangle`, `divided-process` |
| **Extract** | Triangle | `tri` | Extraction process | `extract`, `triangle` |
| **Internal Storage** | Window Pane | `win-pane` | Internal storage | `internal-storage`, `window-pane` |
| **Junction** | Filled Circle | `f-circ` | Junction point | `junction`, `filled-circle` |
| **Lined Document** | Lined Document | `lin-doc` | Lined document | `lined-document` |
| **Loop Limit** | Trapezoidal Pentagon | `notch-pent` | Loop limit step | `loop-limit`, `notched-pentagon` |
| **Manual File** | Flipped Triangle | `flip-tri` | Manual file operation | `manual-file`, `flipped-triangle` |
| **Manual Input** | Sloped Rectangle | `sl-rect` | Manual input step | `manual-input`, `sloped-rectangle` |
| **Multi-Document** | Stacked Document | `docs` | Multiple documents | `documents`, `st-doc`, `stacked-document` |
| **Multi-Process** | Stacked Rectangle | `st-rect` | Multiple processes | `procs`, `processes`, `stacked-rectangle` |
| **Paper Tape** | Flag | `flag` | Paper tape | `paper-tape` |
| **Stored Data** | Bow Tie Rectangle | `bow-rect` | Stored data | `stored-data`, `bow-tie-rectangle` |
| **Summary** | Crossed Circle | `cross-circ` | Summary | `summary`, `crossed-circle` |
| **Tagged Document** | Tagged Document | `tag-doc` | Tagged document | `tag-doc`, `tagged-document` |
| **Tagged Process** | Tagged Rectangle | `tag-rect` | Tagged process | `tagged-rectangle`,`tag-proc`, `tagged-process` |
### Example Flowchart with New Shapes
Heres an example flowchart that utilizes some of the newly introduced shapes:
```mermaid-example
flowchart RL
A@{ shape: manual-file, label: "File Handling"}
B@{ shape: manual-input, label: "User Input"}
C@{ shape: docs, label: "Multiple Documents"}
D@{ shape: procs, label: "Process Automation"}
E@{ shape: paper-tape, label: "Paper Records"}
```
### Process
```mermaid-example
flowchart TD
A@{ shape: rect, label: "This is a process" }
```
### Event
```mermaid-example
flowchart TD
A@{ shape: rounded, label: "This is an event" }
```
### Terminal Point (Stadium)
```mermaid-example
flowchart TD
A@{ shape: stadium, label: "Terminal point" }
```
### Subprocess
```mermaid-example
flowchart TD
A@{ shape: subproc, label: "This is a subprocess" }
```
### Database (Cylinder)
```mermaid-example
flowchart TD
A@{ shape: cyl, label: "Database" }
```
### Start (Circle)
```mermaid-example
flowchart TD
A@{ shape: circle, label: "Start" }
```
### Odd
```mermaid-example
flowchart TD
A@{ shape: odd, label: "Odd shape" }
```
### Decision (Diamond)
```mermaid-example
flowchart TD
A@{ shape: diamond, label: "Decision" }
```
### Prepare Conditional (Hexagon)
```mermaid-example
flowchart TD
A@{ shape: hex, label: "Prepare conditional" }
```
### Data Input/Output (Lean Right)
```mermaid-example
flowchart TD
A@{ shape: lean-r, label: "Input/Output" }
```
### Data Input/Output (Lean Left)
```mermaid-example
flowchart TD
A@{ shape: lean-l, label: "Output/Input" }
```
### Priority Action (Trapezoid Base Bottom)
```mermaid-example
flowchart TD
A@{ shape: trap-b, label: "Priority action" }
```
### Manual Operation (Trapezoid Base Top)
```mermaid-example
flowchart TD
A@{ shape: trap-t, label: "Manual operation" }
```
### Stop (Double Circle)
```mermaid-example
flowchart TD
A@{ shape: dbl-circ, label: "Stop" }
```
### Text Block
```mermaid-example
flowchart TD
A@{ shape: text, label: "This is a text block" }
```
### Card (Notched Rectangle)
```mermaid-example
flowchart TD
A@{ shape: notch-rect, label: "Card" }
```
### Lined/Shaded Process
```mermaid-example
flowchart TD
A@{ shape: lin-rect, label: "Lined process" }
```
### Start (Small Circle)
```mermaid-example
flowchart TD
A@{ shape: sm-circ, label: "Small start" }
```
### Stop (Framed Circle)
```mermaid-example
flowchart TD
A@{ shape: framed-circle, label: "Stop" }
```
### Fork/Join (Long Rectangle)
```mermaid-example
flowchart TD
A@{ shape: fork, label: "Fork or Join" }
```
### Collate (Hourglass)
```mermaid-example
flowchart TD
A@{ shape: hourglass, label: "Collate" }
```
### Comment (Curly Brace)
```mermaid-example
flowchart TD
A@{ shape: comment, label: "Comment" }
```
### Comment Right (Curly Brace Right)
```mermaid-example
flowchart TD
A@{ shape: brace-r, label: "Comment" }
```
### Comment with braces on both sides
```mermaid-example
flowchart TD
A@{ shape: braces, label: "Comment" }
```
### Com Link (Lightning Bolt)
```mermaid-example
flowchart TD
A@{ shape: bolt, label: "Communication link" }
```
### Document
```mermaid-example
flowchart TD
A@{ shape: doc, label: "Document" }
```
### Delay (Half-Rounded Rectangle)
```mermaid-example
flowchart TD
A@{ shape: delay, label: "Delay" }
```
### Direct Access Storage (Horizontal Cylinder)
```mermaid-example
flowchart TD
A@{ shape: das, label: "Direct access storage" }
```
### Disk Storage (Lined Cylinder)
```mermaid-example
flowchart TD
A@{ shape: lin-cyl, label: "Disk storage" }
```
### Display (Curved Trapezoid)
```mermaid-example
flowchart TD
A@{ shape: curv-trap, label: "Display" }
```
### Divided Process (Divided Rectangle)
```mermaid-example
flowchart TD
A@{ shape: div-rect, label: "Divided process" }
```
### Extract (Small Triangle)
```mermaid-example
flowchart TD
A@{ shape: tri, label: "Extract" }
```
### Internal Storage (Window Pane)
```mermaid-example
flowchart TD
A@{ shape: win-pane, label: "Internal storage" }
```
### Junction (Filled Circle)
```mermaid-example
flowchart TD
A@{ shape: f-circ, label: "Junction" }
```
### Lined Document
```mermaid-example
flowchart TD
A@{ shape: lin-doc, label: "Lined document" }
```
### Loop Limit (Notched Pentagon)
```mermaid-example
flowchart TD
A@{ shape: notch-pent, label: "Loop limit" }
```
### Manual File (Flipped Triangle)
```mermaid-example
flowchart TD
A@{ shape: flip-tri, label: "Manual file" }
```
### Manual Input (Sloped Rectangle)
```mermaid-example
flowchart TD
A@{ shape: sl-rect, label: "Manual input" }
```
### Multi-Document (Stacked Document)
```mermaid-example
flowchart TD
A@{ shape: docs, label: "Multiple documents" }
```
### Multi-Process (Stacked Rectangle)
```mermaid-example
flowchart TD
A@{ shape: processes, label: "Multiple processes" }
```
### Paper Tape (Flag)
```mermaid-example
flowchart TD
A@{ shape: flag, label: "Paper tape" }
```
### Stored Data (Bow Tie Rectangle)
```mermaid-example
flowchart TD
A@{ shape: bow-rect, label: "Stored data" }
```
### Summary (Crossed Circle)
```mermaid-example
flowchart TD
A@{ shape: cross-circ, label: "Summary" }
```
### Tagged Document
```mermaid-example
flowchart TD
A@{ shape: tag-doc, label: "Tagged document" }
```
### Tagged Process (Tagged Rectangle)
```mermaid-example
flowchart TD
A@{ shape: tag-rect, label: "Tagged process" }
```
## Links between nodes ## Links between nodes
Nodes can be connected with links/edges. It is possible to have different types of links or attach a text string to a link. Nodes can be connected with links/edges. It is possible to have different types of links or attach a text string to a link.

View File

@@ -105,6 +105,9 @@ end
box rgb(33,66,99) box rgb(33,66,99)
... actors ... ... actors ...
end end
box rgba(33,66,99,0.5)
... actors ...
end
``` ```
```note ```note
@@ -394,6 +397,12 @@ sequenceDiagram
It is possible to highlight flows by providing colored background rects. This is done by the notation It is possible to highlight flows by providing colored background rects. This is done by the notation
```
rect COLOR
... content ...
end
```
The colors are defined using rgb and rgba syntax. The colors are defined using rgb and rgba syntax.
``` ```

View File

@@ -141,8 +141,8 @@ function createFormattedText(
const bbox = textElement.node().getBBox(); const bbox = textElement.node().getBBox();
const padding = 2; const padding = 2;
bkg bkg
.attr('x', -padding) .attr('x', bbox.x - padding)
.attr('y', -padding) .attr('y', bbox.y - padding)
.attr('width', bbox.width + 2 * padding) .attr('width', bbox.width + 2 * padding)
.attr('height', bbox.height + 2 * padding); .attr('height', bbox.height + 2 * padding);

View File

@@ -1,5 +1,4 @@
import { select } from 'd3'; import { select } from 'd3';
import { insertNode } from '../dagre-wrapper/nodes.js';
export const getDiagramElement = (id, securityLevel) => { export const getDiagramElement = (id, securityLevel) => {
let sandboxElement; let sandboxElement;
@@ -17,36 +16,3 @@ export const getDiagramElement = (id, securityLevel) => {
return svg; return svg;
}; };
export function insertElementsForSize(el, data) {
const nodesElem = el.insert('g').attr('class', 'nodes');
el.insert('g').attr('class', 'edges');
data.nodes.forEach(async (item) => {
item.shape = 'rect';
await insertNode(nodesElem, {
...item,
class: 'default flowchart-label',
labelStyle: '',
x: 0,
y: 0,
width: 100,
rx: 0,
ry: 0,
height: 100,
shape: 'rect',
padding: 8,
});
// Create a new DOM element
// const element = document.createElement('div');
// // Set the content of the element to the name of the item
// element.textContent = item.name;
// // Set the size of the element to the size of the item
// element.style.width = `${item.size}px`;
// element.style.height = `${item.size}px`;
// Append the element to the body of the document
// document.body.appendChild(element);
});
}

View File

@@ -125,7 +125,7 @@ const recursiveRender = async (_elem, graph, diagramType, id, parentCluster, sit
// insertCluster(clusters, graph.node(v)); // insertCluster(clusters, graph.node(v));
} else { } else {
log.trace('Node - the non recursive path XAX', v, nodes, graph.node(v), dir); log.trace('Node - the non recursive path XAX', v, nodes, graph.node(v), dir);
await insertNode(nodes, graph.node(v), dir); await insertNode(nodes, graph.node(v), { config: siteConfig, dir });
} }
} }
}) })

View File

@@ -9,7 +9,6 @@ import { choice } from './shapes/choice.ts';
import { note } from './shapes/note.ts'; import { note } from './shapes/note.ts';
import { stadium } from './shapes/stadium.js'; import { stadium } from './shapes/stadium.js';
import { rectWithTitle } from './shapes/rectWithTitle.js'; import { rectWithTitle } from './shapes/rectWithTitle.js';
import { getConfig } from '../../diagram-api/diagramAPI.js';
import { subroutine } from './shapes/subroutine.js'; import { subroutine } from './shapes/subroutine.js';
import { cylinder } from './shapes/cylinder.js'; import { cylinder } from './shapes/cylinder.js';
import { circle } from './shapes/circle.js'; import { circle } from './shapes/circle.js';
@@ -17,46 +16,301 @@ import { doublecircle } from './shapes/doubleCircle.js';
import { rect_left_inv_arrow } from './shapes/rectLeftInvArrow.js'; import { rect_left_inv_arrow } from './shapes/rectLeftInvArrow.js';
import { question } from './shapes/question.js'; import { question } from './shapes/question.js';
import { hexagon } from './shapes/hexagon.js'; import { hexagon } from './shapes/hexagon.js';
import { text } from './shapes/text.js';
import { card } from './shapes/card.js';
import { shadedProcess } from './shapes/shadedProcess.js';
import { anchor } from './shapes/anchor.js';
import { lean_right } from './shapes/leanRight.js'; import { lean_right } from './shapes/leanRight.js';
import { lean_left } from './shapes/leanLeft.js'; import { lean_left } from './shapes/leanLeft.js';
import { trapezoid } from './shapes/trapezoid.js'; import { trapezoid } from './shapes/trapezoid.js';
import { inv_trapezoid } from './shapes/invertedTrapezoid.js'; import { inv_trapezoid } from './shapes/invertedTrapezoid.js';
import { labelRect } from './shapes/labelRect.js'; import { labelRect } from './shapes/labelRect.js';
import { classBox } from './shapes/classBox.js'; import { classBox } from './shapes/classBox.js';
import { triangle } from './shapes/triangle.js';
import { halfRoundedRectangle } from './shapes/halfRoundedRectangle.js';
import { curvedTrapezoid } from './shapes/curvedTrapezoid.js';
import { slopedRect } from './shapes/slopedRect.js';
import { bowTieRect } from './shapes/bowTieRect.js';
import { dividedRectangle } from './shapes/dividedRect.js';
import { crossedCircle } from './shapes/crossedCircle.js';
import { waveRectangle } from './shapes/waveRectangle.js';
import { tiltedCylinder } from './shapes/tiltedCylinder.js';
import { trapezoidalPentagon } from './shapes/trapezoidalPentagon.js';
import { flippedTriangle } from './shapes/flippedTriangle.js';
import { hourglass } from './shapes/hourglass.js';
import { taggedRect } from './shapes/taggedRect.js';
import { multiRect } from './shapes/multiRect.js';
import { linedCylinder } from './shapes/linedCylinder.js';
import { waveEdgedRectangle } from './shapes/waveEdgedRectangle.js';
import { lightningBolt } from './shapes/lightningBolt.js';
import { filledCircle } from './shapes/filledCircle.js';
import { multiWaveEdgedRectangle } from './shapes/multiWaveEdgedRectangle.js';
import { windowPane } from './shapes/windowPane.js';
import { linedWaveEdgedRect } from './shapes/linedWaveEdgedRect.js';
import { taggedWaveEdgedRectangle } from './shapes/taggedWaveEdgedRectangle.js';
import { curlyBraceLeft } from './shapes/curlyBraceLeft.js';
import { curlyBraceRight } from './shapes/curlyBraceRight.js';
import { curlyBraces } from './shapes/curlyBraces.js';
import { iconSquare } from './shapes/iconSquare.js';
import { iconCircle } from './shapes/iconCircle.js';
import { icon } from './shapes/icon.js';
import { imageSquare } from './shapes/imageSquare.js';
import { iconRounded } from './shapes/iconRounded.js';
const shapes = { //Use these names as the left side to render shapes.
export const shapes = {
// States
state, state,
stateStart, stateStart,
stateEnd, stateEnd,
fork: forkJoin, forkJoin,
join: forkJoin,
choice, choice,
note, note,
roundedRect,
// Rectangles
rectWithTitle, rectWithTitle,
roundedRect,
squareRect, squareRect,
// Rectangle with alias: 'process', 'rect', 'proc', 'rectangle'
rectangle: squareRect,
rect: squareRect,
process: squareRect,
proc: squareRect,
// Rounded Rectangle with alias: 'event', 'rounded'
rounded: roundedRect,
event: roundedRect,
// Stadium with alias: 'terminal','pill', 'stadium'
stadium, stadium,
pill: stadium,
terminal: stadium,
// Subprocess with alias: 'fr-rect', 'subproc', 'subprocess', 'framed-rectangle', 'subroutine'
subroutine, subroutine,
'framed-rectangle': subroutine,
'fr-rect': subroutine,
subprocess: subroutine,
subproc: subroutine,
// Cylinder with alias: 'db', 'database', 'cylinder', 'cyl'
cylinder, cylinder,
circle, db: cylinder,
doublecircle, cyl: cylinder,
odd: rect_left_inv_arrow, database: cylinder,
// Diamond with alias: 'diam', 'decision', 'diamond'
question,
diam: question,
diamond: question, diamond: question,
decision: question,
// Hexagon with alias: 'hex', 'hexagon', 'prepare'
hexagon, hexagon,
lean_right, hex: hexagon,
lean_left, prepare: hexagon,
trapezoid,
inv_trapezoid, // Lean Right with alias: 'lean-r', 'lean-right', 'in-out'
lean_right, // used in old syntax for flowchart
'lean-r': lean_right,
'lean-right': lean_right,
'in-out': lean_right,
// Lean Left with alias: 'lean-l', 'lean-left', 'out-in'
lean_left, // used in old syntax for flowchart
'lean-l': lean_left,
'lean-left': lean_left,
'out-in': lean_left,
// Trapezoid with alias: 'trap-b', 'trapezoid-bottom', 'priority'
trapezoid, // used in old syntax for flowchart
'trap-b': trapezoid,
'trapezoid-bottom': trapezoid,
priority: trapezoid,
// Inverted Trapezoid with alias: 'inv-trapezoid', 'trapezoid-top', 'trap-t', 'manual'
inv_trapezoid, // used in old syntax for flowchart
'inv-trapezoid': inv_trapezoid,
'trapezoid-top': inv_trapezoid,
'trap-t': inv_trapezoid,
manual: inv_trapezoid,
// Double Circle with alias: 'dbl-circ', 'double-circle'
doublecircle, // used in old syntax for flowchart
'dbl-circ': doublecircle,
'double-circle': doublecircle,
// circle with alias: 'circ', 'circle'
circle,
circ: circle,
// Rect Left Inv Arrow with alias: 'odd', 'rect-left-inv-arrow'
rect_left_inv_arrow,
odd: rect_left_inv_arrow,
// Notched Rectangle with alias: 'notched-rectangle', 'notch-rect', 'card'
card,
'notched-rectangle': card,
'notch-rect': card,
// Lined rectangle with alias: 'lin-rect', 'lined-rectangle', 'lin-proc', lined-process', 'shaded-process'
'lined-rectangle': shadedProcess,
'lin-rect': shadedProcess,
'lin-proc': shadedProcess,
'lined-process': shadedProcess,
'shaded-process': shadedProcess,
// Small circle with alias: 'sm-circ', 'small-circle', 'start'
'small-circle': stateStart,
'sm-circ': stateStart,
start: stateStart,
// framed circle with alias: 'stop', 'framed-circle', 'fr-circ'
stop: stateEnd,
'framed-circle': stateEnd,
'fr-circ': stateEnd,
// fork with alias: 'join', 'fork'
join: forkJoin,
fork: forkJoin,
// comment with alias: 'comment', 'brace-l'
comment: curlyBraceLeft,
'brace-l': curlyBraceLeft,
// lightening bolt with alias: 'bolt', 'com-link', 'lightning-bolt'
bolt: lightningBolt,
'com-link': lightningBolt,
'lightning-bolt': lightningBolt,
// document with alias: 'doc', 'document'
doc: waveEdgedRectangle,
document: waveEdgedRectangle,
// delay with alias: 'delay', 'half-rounded-rectangle'
delay: halfRoundedRectangle,
'half-rounded-rectangle': halfRoundedRectangle,
// horizontal cylinder with alias: 'h-cyl', 'das', 'horizontal-cylinder'
'horizontal-cylinder': tiltedCylinder,
'h-cyl': tiltedCylinder,
das: tiltedCylinder,
// lined cylinder with alias: 'lin-cyl', 'lined-cylinder', 'disk'
'lined-cylinder': linedCylinder,
'lin-cyl': linedCylinder,
disk: linedCylinder,
// curved trapezoid with alias: 'curv-trap', 'curved-trapezoid', 'display'
'curved-trapezoid': curvedTrapezoid,
'curv-trap': curvedTrapezoid,
display: curvedTrapezoid,
// divided rectangle with alias: 'div-rect', 'divided-rectangle', 'div-proc', 'divided-process'
'divided-rectangle': dividedRectangle,
'div-rect': dividedRectangle,
'div-proc': dividedRectangle,
'divided-process': dividedRectangle,
// triangle with alias: 'tri', 'triangle', 'extract'
triangle,
tri: triangle,
extract: triangle,
// window pane with alias: 'window-pane', 'win-pane', 'internal-storage'
'window-pane': windowPane,
'win-pane': windowPane,
'internal-storage': windowPane,
// filled circle with alias: 'f-circ', 'filled-circle', 'junction'
'f-circ': filledCircle,
junction: filledCircle,
'filled-circle': filledCircle,
// lined document with alias: 'lin-doc', 'lined-document'
'lin-doc': linedWaveEdgedRect,
'lined-document': linedWaveEdgedRect,
// notched pentagon with alias: 'notch-pent', 'notched-pentagon', 'loop-limit'
'notched-pentagon': trapezoidalPentagon,
'notch-pent': trapezoidalPentagon,
'loop-limit': trapezoidalPentagon,
// flipped triangle with alias: 'flip-tri', 'flipped-triangle', 'manual-file'
'flipped-triangle': flippedTriangle,
'flip-tri': flippedTriangle,
'manual-file': flippedTriangle,
// sloped rectangle with alias: 'sl-rect', 'sloped-rectangle', 'manual-input'
'sloped-rectangle': slopedRect,
'sl-rect': slopedRect,
'manual-input': slopedRect,
// documents with alias: 'docs', 'documents', 'st-doc', 'stacked-document'
docs: multiWaveEdgedRectangle,
documents: multiWaveEdgedRectangle,
'st-doc': multiWaveEdgedRectangle,
'stacked-document': multiWaveEdgedRectangle,
// 'processes' with alias: 'procs', 'processes', 'st-rect', 'stacked-rectangle'
processes: multiRect,
procs: multiRect,
'stacked-rectangle': multiRect,
'st-rect': multiRect,
// flag with alias: 'flag', 'paper-tape'
flag: waveRectangle,
'paper-tape': waveRectangle,
// bow tie rectangle with alias: 'bow-rect', 'bow-tie-rectangle', 'stored-data'
'bow-tie-rectangle': bowTieRect,
'bow-rect': bowTieRect,
'stored-data': bowTieRect,
// crossed circle with alias: 'cross-circ', 'crossed-circle', 'summary'
'crossed-circle': crossedCircle,
'cross-circ': crossedCircle,
summary: crossedCircle,
// tagged document with alias: 'tag-doc', 'tagged-document'
'tag-doc': taggedWaveEdgedRectangle,
'tagged-document': taggedWaveEdgedRectangle,
// tagged rectangle with alias: 'tag-rect', 'tagged-rectangle', 'tag-proc', 'tagged-process'
'tag-rect': taggedRect,
'tagged-rectangle': taggedRect,
'tag-proc': taggedRect,
'tagged-process': taggedRect,
// hourglass with alias: 'hourglass', 'collate'
hourglass,
collate: hourglass,
text,
anchor,
brace: curlyBraceLeft,
labelRect, labelRect,
classBox, classBox,
'brace-r': curlyBraceRight,
braces: curlyBraces,
iconSquare,
iconCircle,
icon,
iconRounded,
imageSquare,
}; };
const nodeElems = new Map(); const nodeElems = new Map();
export const insertNode = async (elem, node, dir) => { export const insertNode = async (elem, node, renderOptions) => {
let newEl; let newEl;
let el; let el;
// console.log("node is ", node.icon, node.shape)
//special check for rect shape (with or without rounded corners) //special check for rect shape (with or without rounded corners)
if (node.shape === 'rect') { if (node.shape === 'rect') {
if (node.rx && node.ry) { if (node.rx && node.ry) {
@@ -66,18 +320,22 @@ export const insertNode = async (elem, node, dir) => {
} }
} }
// Add link when appropriate if (!shapes[node.shape]) {
throw new Error(`No such shape: ${node.shape}. Please check your syntax.`);
}
if (node.link) { if (node.link) {
// Add link when appropriate
let target; let target;
if (getConfig().securityLevel === 'sandbox') { if (renderOptions.config.securityLevel === 'sandbox') {
target = '_top'; target = '_top';
} else if (node.linkTarget) { } else if (node.linkTarget) {
target = node.linkTarget || '_blank'; target = node.linkTarget || '_blank';
} }
newEl = elem.insert('svg:a').attr('xlink:href', node.link).attr('target', target); newEl = elem.insert('svg:a').attr('xlink:href', node.link).attr('target', target);
el = await shapes[node.shape](newEl, node, dir); el = await shapes[node.shape](newEl, node, renderOptions);
} else { } else {
el = await shapes[node.shape](elem, node, dir); el = await shapes[node.shape](elem, node, renderOptions);
newEl = el; newEl = el;
} }
if (node.tooltip) { if (node.tooltip) {

View File

@@ -0,0 +1,236 @@
import exp from 'constants';
import { shapes } from './nodes.js';
describe('Test Alias for shapes', function () {
// for each shape in docs/syntax/flowchart.md, along with its semantic name, short name, and alias name, add a test case
// Process | rect | proc | rectangle
it('should support alias for rectangle shape ', function () {
expect(shapes.process).toBe(shapes.rect);
expect(shapes.proc).toBe(shapes.rect);
expect(shapes.rectangle).toBe(shapes.rect);
});
// event | rounded
it('should support alias for rounded shape ', function () {
expect(shapes.event).toBe(shapes.rounded);
});
// stadium | pill | term
it('should support alias for stadium shape ', function () {
expect(shapes.pill).toBe(shapes.stadium);
expect(shapes.terminal).toBe(shapes.stadium);
});
// fr-rect | subproc | framed-rectangle | subroutine
it('should support alias for subroutine shape ', function () {
expect(shapes['framed-rectangle']).toBe(shapes['fr-rect']);
expect(shapes.subproc).toBe(shapes['fr-rect']);
expect(shapes.subroutine).toBe(shapes['fr-rect']);
});
// cyl | db | cylinder
it('should support alias for cylinder shape ', function () {
expect(shapes.db).toBe(shapes.cylinder);
expect(shapes.cyl).toBe(shapes.cylinder);
});
// diam | decision | diamond
it('should support alias for diamond shape ', function () {
expect(shapes.diam).toBe(shapes.decision);
expect(shapes.diamond).toBe(shapes.decision);
});
// hex | hexagon | prepare
it('should support alias for hexagon shape ', function () {
expect(shapes.hex).toBe(shapes.hexagon);
expect(shapes.prepare).toBe(shapes.hexagon);
});
// l-r | lean-right | in-out
it('should support alias for lean-right shape ', function () {
expect(shapes['lean-r']).toBe(shapes['lean-right']);
expect(shapes['in-out']).toBe(shapes['lean-right']);
});
// l-l | lean-left | out-in
it('should support alias for lean-left shape ', function () {
expect(shapes['lean-l']).toBe(shapes['lean-left']);
expect(shapes['out-in']).toBe(shapes['lean-left']);
});
// trap-b | trapezoid-bottom | priority | trapezoid
it('should support alias for trapezoid shape ', function () {
expect(shapes['trapezoid-bottom']).toBe(shapes['trap-b']);
expect(shapes.priority).toBe(shapes['trap-b']);
expect(shapes.trapezoid).toBe(shapes['trap-b']);
});
// trap-t | trapezoid-top | manual | inv-trapezoid
it('should support alias for inv_trapezoid shape ', function () {
expect(shapes['trapezoid-top']).toBe(shapes['trap-t']);
expect(shapes.manual).toBe(shapes['trap-t']);
expect(shapes['inv-trapezoid']).toBe(shapes['trap-t']);
});
// dbl-circ| double-circle
it('should support alias for doublecircle shape ', function () {
expect(shapes['double-circle']).toBe(shapes['dbl-circ']);
});
// notched-rectangle | card | notch-rect
it('should support alias for notched-rectangle shape ', function () {
expect(shapes.card).toBe(shapes['notched-rectangle']);
expect(shapes['notch-rect']).toBe(shapes['notched-rectangle']);
});
it('should support alias for shadedProcess shape ', function () {
const aliases = ['lined-process', 'lined-rectangle', 'lin-proc', 'lin-rect'];
for (const alias of aliases) {
expect(shapes[alias]).toBe(shapes['shaded-process']);
}
});
// sm-circ | small-circle | start
it('should support alias for smallCircle shape ', function () {
expect(shapes['small-circle']).toBe(shapes['sm-circ']);
expect(shapes.start).toBe(shapes['sm-circ']);
});
// framed-circle | stop
it('should support alias for framed circle shape ', function () {
expect(shapes.stop).toBe(shapes['framed-circle']);
});
// fork | join
it('should support alias for fork shape ', function () {
expect(shapes.join).toBe(shapes.fork);
});
// brace | comment | brace-l
it('should support alias for brace shape ', function () {
expect(shapes.comment).toBe(shapes.brace);
expect(shapes['brace-l']).toBe(shapes.brace);
});
// bolt | com-link | lightning-bolt
it('should support alias for bolt shape ', function () {
expect(shapes['com-link']).toBe(shapes.bolt);
expect(shapes['lightning-bolt']).toBe(shapes.bolt);
});
// document | doc
it('should support alias for waveEdgedRectangle shape ', function () {
expect(shapes.doc).toBe(shapes.document);
});
// delay | half-rounded-rectangle
it('should support alias for halfRoundedRectangle shape ', function () {
expect(shapes.delay).toBe(shapes['half-rounded-rectangle']);
});
// h-cyl | das | horizontal-cylinder
it('should support alias for horizontal-cylinder shape ', function () {
expect(shapes.das).toBe(shapes['h-cyl']);
expect(shapes['horizontal-cylinder']).toBe(shapes['h-cyl']);
});
// lin-cyl | disk | lined-cylinder
it('should support alias for linedCylinder shape ', function () {
expect(shapes.disk).toBe(shapes['lin-cyl']);
expect(shapes['lined-cylinder']).toBe(shapes['lin-cyl']);
});
// curv-trap | display | curved-trapezoid
it('should support alias for curvedTrapezoid shape ', function () {
expect(shapes.display).toBe(shapes['curv-trap']);
expect(shapes['curved-trapezoid']).toBe(shapes['curv-trap']);
});
// div-rect | div-proc | divided-rectangle
it('should support alias for dividedRectangle shape ', function () {
expect(shapes['div-proc']).toBe(shapes['div-rect']);
expect(shapes['divided-rectangle']).toBe(shapes['div-rect']);
});
// sm-tri | extract | small-triangle | triangle
it('should support alias for smallTriangle shape ', function () {
expect(shapes.extract).toBe(shapes.tri);
expect(shapes.triangle).toBe(shapes.tri);
});
// win-pane | internal-storage | window-pane
it('should support alias for windowPane shape ', function () {
expect(shapes['internal-storage']).toBe(shapes['win-pane']);
expect(shapes['window-pane']).toBe(shapes['win-pane']);
});
// fc | junction | filled-circle
it('should support alias for filledCircle shape ', function () {
expect(shapes.junction).toBe(shapes['f-circ']);
expect(shapes['filled-circle']).toBe(shapes['f-circ']);
});
// | lin-doc | lined-document
it('should support alias for linedWaveEdgedRectangle shape ', function () {
expect(shapes['lin-doc']).toBe(shapes['lined-document']);
});
// notch-pent | loop-limit | notched-pentagon
it('should support alias for notchedPentagon shape ', function () {
expect(shapes['loop-limit']).toBe(shapes['notch-pent']);
expect(shapes['notched-pentagon']).toBe(shapes['notch-pent']);
});
// flip-tri | manual-file | flipped-triangle
it('should support alias for flippedTriangle shape ', function () {
expect(shapes['manual-file']).toBe(shapes['flip-tri']);
expect(shapes['flipped-triangle']).toBe(shapes['flip-tri']);
});
//sl-rect | manual-input | sloped-rectangle
it('should support alias for slopedRectangle shape ', function () {
expect(shapes['manual-input']).toBe(shapes['sl-rect']);
expect(shapes['sloped-rectangle']).toBe(shapes['sl-rect']);
});
// docs | documents | st-doc | stacked-document
it('should support alias for multiWaveEdgedRectangle shape ', function () {
expect(shapes.docs).toBe(shapes.documents);
expect(shapes['st-doc']).toBe(shapes['stacked-document']);
});
// procs | processes | st-rect | stacked-rectangle
it('should support alias for multiRect shape ', function () {
expect(shapes.procs).toBe(shapes.processes);
expect(shapes['st-rect']).toBe(shapes['stacked-rectangle']);
});
// flag | paper-tape
it('should support alias for paperTape shape ', function () {
expect(shapes['paper-tape']).toBe(shapes.flag);
});
// bow-rect| stored-data | bow-tie-rectangle
it('should support alias for bowTieRect shape ', function () {
expect(shapes['stored-data']).toBe(shapes['bow-rect']);
expect(shapes['bow-tie-rectangle']).toBe(shapes['bow-rect']);
});
// cross-circ | summary | crossed-circle
it('should support alias for crossedCircle shape ', function () {
expect(shapes.summary).toBe(shapes['cross-circ']);
expect(shapes['crossed-circle']).toBe(shapes['cross-circ']);
});
// tag-doc| tag-document
it('should support alias for taggedDocument shape ', function () {
expect(shapes['tag-doc']).toBe(shapes['tagged-document']);
});
// tag-rect | tag-proc | tagged-rectangle | tagged-process
it('should support alias for taggedRect shape ', function () {
expect(shapes['tag-proc']).toBe(shapes['tag-rect']);
expect(shapes['tagged-rectangle']).toBe(shapes['tag-rect']);
expect(shapes['tagged-process']).toBe(shapes['tag-rect']);
});
});

View File

@@ -0,0 +1,44 @@
import { log } from '../../../logger.js';
import { updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.d.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
export const anchor = (parent: SVGAElement, node: Node): Promise<SVGAElement> => {
const { labelStyles } = styles2String(node);
node.labelStyle = labelStyles;
const classes = getNodeClasses(node);
let cssClasses = classes;
if (!classes) {
cssClasses = 'anchor';
}
const shapeSvg = parent
// @ts-ignore - SVGElement is not typed
.insert('g')
.attr('class', cssClasses)
.attr('id', node.domId || node.id);
const radius = 1;
const { cssStyles } = node;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, { fill: 'black', stroke: 'none', fillStyle: 'solid' });
if (node.look !== 'handDrawn') {
options.roughness = 0;
}
const roughNode = rc.circle(0, 0, radius * 2, options);
const circleElem = shapeSvg.insert(() => roughNode, ':first-child');
circleElem.attr('class', 'anchor').attr('style', cssStyles);
updateNodeBounds(node, circleElem);
node.intersect = function (point) {
log.info('Circle intersect', node, radius, point);
return intersect.circle(node, radius, point);
};
return shapeSvg;
};

View File

@@ -0,0 +1,125 @@
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.d.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
function generateArcPoints(
x1: number,
y1: number,
x2: number,
y2: number,
rx: number,
ry: number,
clockwise: boolean
) {
const numPoints = 20;
// Calculate midpoint
const midX = (x1 + x2) / 2;
const midY = (y1 + y2) / 2;
// Calculate the angle of the line connecting the points
const angle = Math.atan2(y2 - y1, x2 - x1);
// Calculate transformed coordinates for the ellipse
const dx = (x2 - x1) / 2;
const dy = (y2 - y1) / 2;
// Scale to unit circle
const transformedX = dx / rx;
const transformedY = dy / ry;
// Calculate the distance between points on the unit circle
const distance = Math.sqrt(transformedX ** 2 + transformedY ** 2);
// Check if the ellipse can be drawn with the given radii
if (distance > 1) {
throw new Error('The given radii are too small to create an arc between the points.');
}
// Calculate the distance from the midpoint to the center of the ellipse
const scaledCenterDistance = Math.sqrt(1 - distance ** 2);
// Calculate the center of the ellipse
const centerX = midX + scaledCenterDistance * ry * Math.sin(angle) * (clockwise ? -1 : 1);
const centerY = midY - scaledCenterDistance * rx * Math.cos(angle) * (clockwise ? -1 : 1);
// Calculate the start and end angles on the ellipse
const startAngle = Math.atan2((y1 - centerY) / ry, (x1 - centerX) / rx);
const endAngle = Math.atan2((y2 - centerY) / ry, (x2 - centerX) / rx);
// Adjust angles for clockwise/counterclockwise
let angleRange = endAngle - startAngle;
if (clockwise && angleRange < 0) {
angleRange += 2 * Math.PI;
}
if (!clockwise && angleRange > 0) {
angleRange -= 2 * Math.PI;
}
// Generate points
const points = [];
for (let i = 0; i < numPoints; i++) {
const t = i / (numPoints - 1);
const angle = startAngle + t * angleRange;
const x = centerX + rx * Math.cos(angle);
const y = centerY + ry * Math.sin(angle);
points.push({ x, y });
}
return points;
}
export const bowTieRect = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
const w = bbox.width + node.padding + 20;
const h = bbox.height + node.padding;
const ry = h / 2;
const rx = ry / (2.5 + h / 50);
// let shape: d3.Selection<SVGPathElement | SVGGElement, unknown, null, undefined>;
const { cssStyles } = node;
const points = [
{ x: w / 2, y: -h / 2 },
{ x: -w / 2, y: -h / 2 },
...generateArcPoints(-w / 2, -h / 2, -w / 2, h / 2, rx, ry, false),
{ x: w / 2, y: h / 2 },
...generateArcPoints(w / 2, h / 2, w / 2, -h / 2, rx, ry, true),
];
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const bowTieRectPath = createPathFromPoints(points);
const bowTieRectShapePath = rc.path(bowTieRectPath, options);
const bowTieRectShape = shapeSvg.insert(() => bowTieRectShapePath, ':first-child');
bowTieRectShape.attr('class', 'basic label-container');
if (cssStyles && node.look !== 'handDrawn') {
bowTieRectShape.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
bowTieRectShape.selectAll('path').attr('style', nodeStyles);
}
bowTieRectShape.attr('transform', `translate(${rx / 2}, 0)`);
updateNodeBounds(node, bowTieRectShape);
node.intersect = function (point) {
const pos = intersect.polygon(node, points, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,68 @@
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.d.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
import { insertPolygonShape } from './insertPolygonShape.js';
import { createPathFromPoints } from './util.js';
// const createPathFromPoints = (points: { x: number; y: number }[]): string => {
// const pointStrings = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`);
// pointStrings.push('Z');
// return pointStrings.join(' ');
// };
export async function card(parent: SVGAElement, node: Node): Promise<SVGAElement> {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
const h = bbox.height + node.padding;
const padding = 12;
const w = bbox.width + node.padding + padding;
const left = 0;
const right = w;
const top = -h;
const bottom = 0;
const points = [
{ x: left + padding, y: top },
{ x: right, y: top },
{ x: right, y: bottom },
{ x: left, y: bottom },
{ x: left, y: top + padding },
{ x: left + padding, y: top },
];
let polygon: d3.Selection<SVGPolygonElement | SVGGElement, unknown, null, undefined>;
const { cssStyles } = node;
if (node.look === 'handDrawn') {
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
const pathData = createPathFromPoints(points);
const roughNode = rc.path(pathData, options);
polygon = shapeSvg
.insert(() => roughNode, ':first-child')
.attr('transform', `translate(${-w / 2}, ${h / 2})`);
if (cssStyles) {
polygon.attr('style', cssStyles);
}
} else {
polygon = insertPolygonShape(shapeSvg, w, h, points);
}
if (nodeStyles) {
polygon.attr('style', nodeStyles);
}
updateNodeBounds(node, polygon);
node.intersect = function (point) {
return intersect.polygon(node, points, point);
};
return shapeSvg;
}

View File

@@ -3,20 +3,20 @@ import type { Node } from '../../types.js';
import type { SVG } from '../../../diagram-api/types.js'; import type { SVG } from '../../../diagram-api/types.js';
// @ts-ignore TODO: Fix rough typings // @ts-ignore TODO: Fix rough typings
import rough from 'roughjs'; import rough from 'roughjs';
import { solidStateFill, styles2String } from './handDrawnShapeStyles.js'; import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import { getConfig } from '../../../diagram-api/diagramAPI.js'; import { createPathFromPoints, getNodeClasses } from './util.js';
export const choice = (parent: SVG, node: Node) => { export const choice = (parent: SVG, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node); const { nodeStyles } = styles2String(node);
node.labelStyle = labelStyles; node.label = '';
const { themeVariables } = getConfig();
const { lineColor } = themeVariables;
const shapeSvg = parent const shapeSvg = parent
.insert('g') .insert('g')
.attr('class', 'node default') .attr('class', getNodeClasses(node))
.attr('id', node.domId || node.id); .attr('id', node.domId ?? node.id);
const { cssStyles } = node;
const s = Math.max(28, node.width ?? 0);
const s = 28;
const points = [ const points = [
{ x: 0, y: s / 2 }, { x: 0, y: s / 2 },
{ x: s / 2, y: 0 }, { x: s / 2, y: 0 },
@@ -24,40 +24,32 @@ export const choice = (parent: SVG, node: Node) => {
{ x: -s / 2, y: 0 }, { x: -s / 2, y: 0 },
]; ];
let choice; // @ts-ignore TODO: Fix rough typings
if (node.look === 'handDrawn') { const rc = rough.svg(shapeSvg);
// @ts-ignore TODO: Fix rough typings const options = userNodeOverrides(node, {});
const rc = rough.svg(shapeSvg);
const pointArr = points.map(function (d) { if (node.look !== 'handDrawn') {
return [d.x, d.y]; options.roughness = 0;
}); options.fillStyle = 'solid';
const roughNode = rc.polygon(pointArr, solidStateFill(lineColor));
choice = shapeSvg.insert(() => roughNode);
} else {
choice = shapeSvg.insert('polygon', ':first-child').attr(
'points',
points
.map(function (d) {
return d.x + ',' + d.y;
})
.join(' ')
);
} }
// center the circle around its coordinate const choicePath = createPathFromPoints(points);
choice const roughNode = rc.path(choicePath, options);
.attr('class', 'state-start') const choiceShape = shapeSvg.insert(() => roughNode, ':first-child');
// @ts-ignore TODO: Fix rough typings
.attr('r', 7) if (cssStyles && node.look !== 'handDrawn') {
.attr('width', 28) choiceShape.selectAll('path').attr('style', cssStyles);
.attr('height', 28) }
.attr('style', nodeStyles);
if (nodeStyles && node.look !== 'handDrawn') {
choiceShape.selectAll('path').attr('style', nodeStyles);
}
node.width = 28; node.width = 28;
node.height = 28; node.height = 28;
node.intersect = function (point) { node.intersect = function (point) {
return intersect.circle(node, 14, point); return intersect.polygon(node, points, point);
}; };
return shapeSvg; return shapeSvg;

View File

@@ -15,7 +15,6 @@ export const circle = async (parent: SVGAElement, node: Node): Promise<SVGAEleme
const { cssStyles } = node; const { cssStyles } = node;
if (node.look === 'handDrawn') { if (node.look === 'handDrawn') {
// @ts-ignore - rough is not typed
const rc = rough.svg(shapeSvg); const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {}); const options = userNodeOverrides(node, {});
const roughNode = rc.circle(0, 0, radius * 2, options); const roughNode = rc.circle(0, 0, radius * 2, options);

View File

@@ -0,0 +1,67 @@
import { log } from '../../../logger.js';
import { getNodeClasses, updateNodeBounds } from './util.js';
import type { SVG } from '../../../diagram-api/types.js';
import type { Node } from '../../types.d.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
import intersect from '../intersect/index.js';
function createLine(r: number) {
const xAxis45 = Math.cos(Math.PI / 4); // cosine of 45 degrees
const yAxis45 = Math.sin(Math.PI / 4); // sine of 45 degrees
const lineLength = r * 2;
const pointQ1 = { x: (lineLength / 2) * xAxis45, y: (lineLength / 2) * yAxis45 }; // Quadrant I
const pointQ2 = { x: -(lineLength / 2) * xAxis45, y: (lineLength / 2) * yAxis45 }; // Quadrant II
const pointQ3 = { x: -(lineLength / 2) * xAxis45, y: -(lineLength / 2) * yAxis45 }; // Quadrant III
const pointQ4 = { x: (lineLength / 2) * xAxis45, y: -(lineLength / 2) * yAxis45 }; // Quadrant IV
return `M ${pointQ2.x},${pointQ2.y} L ${pointQ4.x},${pointQ4.y}
M ${pointQ1.x},${pointQ1.y} L ${pointQ3.x},${pointQ3.y}`;
}
export const crossedCircle = (parent: SVG, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
node.label = '';
const shapeSvg = parent
.insert('g')
.attr('class', getNodeClasses(node))
.attr('id', node.domId ?? node.id);
const radius = Math.max(30, node?.width ?? 0);
const { cssStyles } = node;
// @ts-expect-error shapeSvg d3 class is incorrect?
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const circleNode = rc.circle(0, 0, radius * 2, options);
const linePath = createLine(radius);
const lineNode = rc.path(linePath, options);
const crossedCircle = shapeSvg.insert(() => circleNode, ':first-child');
crossedCircle.insert(() => lineNode);
if (cssStyles && node.look !== 'handDrawn') {
crossedCircle.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
crossedCircle.selectAll('path').attr('style', nodeStyles);
}
updateNodeBounds(node, crossedCircle);
node.intersect = function (point) {
log.info('crossedCircle intersect', node, { radius, point });
const pos = intersect.circle(node, radius, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,110 @@
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.d.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
function generateCirclePoints(
centerX: number,
centerY: number,
radius: number,
numPoints = 100,
startAngle = 0,
endAngle = 180
) {
const points = [];
// Convert angles to radians
const startAngleRad = (startAngle * Math.PI) / 180;
const endAngleRad = (endAngle * Math.PI) / 180;
// Calculate the angle range in radians
const angleRange = endAngleRad - startAngleRad;
// Calculate the angle step
const angleStep = angleRange / (numPoints - 1);
for (let i = 0; i < numPoints; i++) {
const angle = startAngleRad + i * angleStep;
const x = centerX + radius * Math.cos(angle);
const y = centerY + radius * Math.sin(angle);
points.push({ x: -x, y: -y });
}
return points;
}
export const curlyBraceLeft = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
const w = bbox.width + (node.padding ?? 0);
const h = bbox.height + (node.padding ?? 0);
const radius = Math.max(5, h * 0.1);
const { cssStyles } = node;
const points = [
...generateCirclePoints(w / 2, -h / 2, radius, 30, -90, 0),
{ x: -w / 2 - radius, y: radius },
...generateCirclePoints(w / 2 + radius * 2, -radius, radius, 20, -180, -270),
...generateCirclePoints(w / 2 + radius * 2, radius, radius, 20, -90, -180),
{ x: -w / 2 - radius, y: -h / 2 },
...generateCirclePoints(w / 2, h / 2, radius, 20, 0, 90),
];
const rectPoints = [
{ x: w / 2, y: -h / 2 - radius },
{ x: -w / 2, y: -h / 2 - radius },
...generateCirclePoints(w / 2, -h / 2, radius, 20, -90, 0),
{ x: -w / 2 - radius, y: -radius },
...generateCirclePoints(w / 2 + w * 0.1, -radius, radius, 20, -180, -270),
...generateCirclePoints(w / 2 + w * 0.1, radius, radius, 20, -90, -180),
{ x: -w / 2 - radius, y: h / 2 },
...generateCirclePoints(w / 2, h / 2, radius, 20, 0, 90),
{ x: -w / 2, y: h / 2 + radius },
{ x: w / 2, y: h / 2 + radius },
];
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, { fill: 'none' });
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const curlyBraceLeftPath = createPathFromPoints(points);
const newCurlyBracePath = curlyBraceLeftPath.replace('Z', '');
const curlyBraceLeftNode = rc.path(newCurlyBracePath, options);
const rectPath = createPathFromPoints(rectPoints);
const rectShape = rc.path(rectPath, { ...options });
const curlyBraceLeftShape = shapeSvg.insert('g', ':first-child');
curlyBraceLeftShape.insert(() => rectShape, ':first-child').attr('stroke-opacity', 0);
curlyBraceLeftShape.insert(() => curlyBraceLeftNode, ':first-child');
curlyBraceLeftShape.attr('class', 'text');
if (cssStyles && node.look !== 'handDrawn') {
curlyBraceLeftShape.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
curlyBraceLeftShape.selectAll('path').attr('style', nodeStyles);
}
curlyBraceLeftShape.attr('transform', `translate(${radius}, 0)`);
label.attr(
'transform',
`translate(${-w / 2 + radius - (bbox.x - (bbox.left ?? 0))},${-h / 2 + (node.padding ?? 0) / 2 - (bbox.y - (bbox.top ?? 0))})`
);
updateNodeBounds(node, curlyBraceLeftShape);
node.intersect = function (point) {
const pos = intersect.polygon(node, rectPoints, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,110 @@
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.d.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
function generateCirclePoints(
centerX: number,
centerY: number,
radius: number,
numPoints = 100,
startAngle = 0,
endAngle = 180
) {
const points = [];
// Convert angles to radians
const startAngleRad = (startAngle * Math.PI) / 180;
const endAngleRad = (endAngle * Math.PI) / 180;
// Calculate the angle range in radians
const angleRange = endAngleRad - startAngleRad;
// Calculate the angle step
const angleStep = angleRange / (numPoints - 1);
for (let i = 0; i < numPoints; i++) {
const angle = startAngleRad + i * angleStep;
const x = centerX + radius * Math.cos(angle);
const y = centerY + radius * Math.sin(angle);
points.push({ x, y });
}
return points;
}
export const curlyBraceRight = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
const w = bbox.width + (node.padding ?? 0);
const h = bbox.height + (node.padding ?? 0);
const radius = Math.max(5, h * 0.1);
const { cssStyles } = node;
const points = [
...generateCirclePoints(w / 2, -h / 2, radius, 20, -90, 0),
{ x: w / 2 + radius, y: -radius },
...generateCirclePoints(w / 2 + radius * 2, -radius, radius, 20, -180, -270),
...generateCirclePoints(w / 2 + radius * 2, radius, radius, 20, -90, -180),
{ x: w / 2 + radius, y: h / 2 },
...generateCirclePoints(w / 2, h / 2, radius, 20, 0, 90),
];
const rectPoints = [
{ x: -w / 2, y: -h / 2 - radius },
{ x: w / 2, y: -h / 2 - radius },
...generateCirclePoints(w / 2, -h / 2, radius, 20, -90, 0),
{ x: w / 2 + radius, y: -radius },
...generateCirclePoints(w / 2 + radius * 2, -radius, radius, 20, -180, -270),
...generateCirclePoints(w / 2 + radius * 2, radius, radius, 20, -90, -180),
{ x: w / 2 + radius, y: h / 2 },
...generateCirclePoints(w / 2, h / 2, radius, 20, 0, 90),
{ x: w / 2, y: h / 2 + radius },
{ x: -w / 2, y: h / 2 + radius },
];
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, { fill: 'none' });
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const curlyBraceRightPath = createPathFromPoints(points);
const newCurlyBracePath = curlyBraceRightPath.replace('Z', '');
const curlyBraceRightNode = rc.path(newCurlyBracePath, options);
const rectPath = createPathFromPoints(rectPoints);
const rectShape = rc.path(rectPath, { ...options });
const curlyBraceRightShape = shapeSvg.insert('g', ':first-child');
curlyBraceRightShape.insert(() => rectShape, ':first-child').attr('stroke-opacity', 0);
curlyBraceRightShape.insert(() => curlyBraceRightNode, ':first-child');
curlyBraceRightShape.attr('class', 'text');
if (cssStyles && node.look !== 'handDrawn') {
curlyBraceRightShape.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
curlyBraceRightShape.selectAll('path').attr('style', nodeStyles);
}
curlyBraceRightShape.attr('transform', `translate(${-radius}, 0)`);
label.attr(
'transform',
`translate(${-w / 2 + (node.padding ?? 0) / 2 - (bbox.x - (bbox.left ?? 0))},${-h / 2 + (node.padding ?? 0) / 2 - (bbox.y - (bbox.top ?? 0))})`
);
updateNodeBounds(node, curlyBraceRightShape);
node.intersect = function (point) {
const pos = intersect.polygon(node, rectPoints, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,129 @@
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.d.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
function generateCirclePoints(
centerX: number,
centerY: number,
radius: number,
numPoints = 100,
startAngle = 0,
endAngle = 180
) {
const points = [];
// Convert angles to radians
const startAngleRad = (startAngle * Math.PI) / 180;
const endAngleRad = (endAngle * Math.PI) / 180;
// Calculate the angle range in radians
const angleRange = endAngleRad - startAngleRad;
// Calculate the angle step
const angleStep = angleRange / (numPoints - 1);
for (let i = 0; i < numPoints; i++) {
const angle = startAngleRad + i * angleStep;
const x = centerX + radius * Math.cos(angle);
const y = centerY + radius * Math.sin(angle);
points.push({ x: -x, y: -y });
}
return points;
}
export const curlyBraces = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
const w = bbox.width + (node.padding ?? 0);
const h = bbox.height + (node.padding ?? 0);
const radius = Math.max(5, h * 0.1);
const { cssStyles } = node;
const leftCurlyBracePoints = [
...generateCirclePoints(w / 2, -h / 2, radius, 30, -90, 0),
{ x: -w / 2 - radius, y: radius },
...generateCirclePoints(w / 2 + radius * 2, -radius, radius, 20, -180, -270),
...generateCirclePoints(w / 2 + radius * 2, radius, radius, 20, -90, -180),
{ x: -w / 2 - radius, y: -h / 2 },
...generateCirclePoints(w / 2, h / 2, radius, 20, 0, 90),
];
const rightCurlyBracePoints = [
...generateCirclePoints(-w / 2 + radius + radius / 2, -h / 2, radius, 20, -90, -180),
{ x: w / 2 - radius / 2, y: radius },
...generateCirclePoints(-w / 2 - radius / 2, -radius, radius, 20, 0, 90),
...generateCirclePoints(-w / 2 - radius / 2, radius, radius, 20, -90, 0),
{ x: w / 2 - radius / 2, y: -radius },
...generateCirclePoints(-w / 2 + radius + radius / 2, h / 2, radius, 30, -180, -270),
];
const rectPoints = [
{ x: w / 2, y: -h / 2 - radius },
{ x: -w / 2, y: -h / 2 - radius },
...generateCirclePoints(w / 2, -h / 2, radius, 20, -90, 0),
{ x: -w / 2 - radius, y: -radius },
...generateCirclePoints(w / 2 + radius * 2, -radius, radius, 20, -180, -270),
...generateCirclePoints(w / 2 + radius * 2, radius, radius, 20, -90, -180),
{ x: -w / 2 - radius, y: h / 2 },
...generateCirclePoints(w / 2, h / 2, radius, 20, 0, 90),
{ x: -w / 2, y: h / 2 + radius },
{ x: w / 2 - radius - radius / 2, y: h / 2 + radius },
...generateCirclePoints(-w / 2 + radius + radius / 2, -h / 2, radius, 20, -90, -180),
{ x: w / 2 - radius / 2, y: radius },
...generateCirclePoints(-w / 2 - radius / 2, -radius, radius, 20, 0, 90),
...generateCirclePoints(-w / 2 - radius / 2, radius, radius, 20, -90, 0),
{ x: w / 2 - radius / 2, y: -radius },
...generateCirclePoints(-w / 2 + radius + radius / 2, h / 2, radius, 30, -180, -270),
];
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, { fill: 'none' });
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const leftCurlyBracePath = createPathFromPoints(leftCurlyBracePoints);
const newLeftCurlyBracePath = leftCurlyBracePath.replace('Z', '');
const leftCurlyBraceNode = rc.path(newLeftCurlyBracePath, options);
const rightCurlyBracePath = createPathFromPoints(rightCurlyBracePoints);
const newRightCurlyBracePath = rightCurlyBracePath.replace('Z', '');
const rightCurlyBraceNode = rc.path(newRightCurlyBracePath, options);
const rectPath = createPathFromPoints(rectPoints);
const rectShape = rc.path(rectPath, { ...options });
const curlyBracesShape = shapeSvg.insert('g', ':first-child');
curlyBracesShape.insert(() => rectShape, ':first-child').attr('stroke-opacity', 0);
curlyBracesShape.insert(() => leftCurlyBraceNode, ':first-child');
curlyBracesShape.insert(() => rightCurlyBraceNode, ':first-child');
curlyBracesShape.attr('class', 'text');
if (cssStyles && node.look !== 'handDrawn') {
curlyBracesShape.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
curlyBracesShape.selectAll('path').attr('style', nodeStyles);
}
curlyBracesShape.attr('transform', `translate(${radius - radius / 4}, 0)`);
label.attr(
'transform',
`translate(${-w / 2 + (node.padding ?? 0) / 2 - (bbox.x - (bbox.left ?? 0))},${-h / 2 + (node.padding ?? 0) / 2 - (bbox.y - (bbox.top ?? 0))})`
);
updateNodeBounds(node, curlyBracesShape);
node.intersect = function (point) {
const pos = intersect.polygon(node, rectPoints, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,70 @@
import {
labelHelper,
updateNodeBounds,
getNodeClasses,
createPathFromPoints,
generateCirclePoints,
} from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.d.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
export const curvedTrapezoid = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
const minWidth = 80,
minHeight = 20;
const w = Math.max(minWidth, (bbox.width + (node.padding ?? 0) * 2) * 1.25, node?.width ?? 0);
const h = Math.max(minHeight, bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0);
const radius = h / 2;
const { cssStyles } = node;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const totalWidth = w,
totalHeight = h;
const rw = totalWidth - radius;
const tw = totalHeight / 4;
const points = [
{ x: rw, y: 0 },
{ x: tw, y: 0 },
{ x: 0, y: totalHeight / 2 },
{ x: tw, y: totalHeight },
{ x: rw, y: totalHeight },
...generateCirclePoints(-rw, -totalHeight / 2, radius, 50, 270, 90),
];
const pathData = createPathFromPoints(points);
const shapeNode = rc.path(pathData, options);
const polygon = shapeSvg.insert(() => shapeNode, ':first-child');
polygon.attr('class', 'basic label-container');
if (cssStyles && node.look !== 'handDrawn') {
polygon.selectChildren('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
polygon.selectChildren('path').attr('style', nodeStyles);
}
polygon.attr('transform', `translate(${-w / 2}, ${-h / 2})`);
updateNodeBounds(node, polygon);
node.intersect = function (point) {
const pos = intersect.polygon(node, points, point);
return pos;
};
return shapeSvg;
};

View File

@@ -51,17 +51,16 @@ export const createInnerCylinderPathD = (
export const cylinder = async (parent: SVGAElement, node: Node) => { export const cylinder = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node); const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles; node.labelStyle = labelStyles;
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
const w = bbox.width + node.padding; const w = Math.max(bbox.width + node.padding, node.width ?? 0);
const rx = w / 2; const rx = w / 2;
const ry = rx / (2.5 + w / 50); const ry = rx / (2.5 + w / 50);
const h = bbox.height + ry + node.padding; const h = Math.max(bbox.height + ry + node.padding, node.height ?? 0);
let cylinder: d3.Selection<SVGPathElement | SVGGElement, unknown, null, undefined>; let cylinder: d3.Selection<SVGPathElement | SVGGElement, unknown, null, undefined>;
const { cssStyles } = node; const { cssStyles } = node;
if (node.look === 'handDrawn') { if (node.look === 'handDrawn') {
// @ts-ignore - rough is not typed
const rc = rough.svg(shapeSvg); const rc = rough.svg(shapeSvg);
const outerPathData = createOuterCylinderPathD(0, 0, w, h, rx, ry); const outerPathData = createOuterCylinderPathD(0, 0, w, h, rx, ry);
const innerPathData = createInnerCylinderPathD(0, ry, w, h, rx, ry); const innerPathData = createInnerCylinderPathD(0, ry, w, h, rx, ry);
@@ -89,6 +88,11 @@ export const cylinder = async (parent: SVGAElement, node: Node) => {
updateNodeBounds(node, cylinder); updateNodeBounds(node, cylinder);
label.attr(
'transform',
`translate(${-(bbox.width / 2) - (bbox.x - (bbox.left ?? 0))}, ${-(bbox.height / 2) + (node.padding ?? 0) / 1.5 - (bbox.y - (bbox.top ?? 0))})`
);
node.intersect = function (point) { node.intersect = function (point) {
const pos = intersect.rect(node, point); const pos = intersect.rect(node, point);
const x = pos.x - (node.x ?? 0); const x = pos.x - (node.x ?? 0);

View File

@@ -0,0 +1,66 @@
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.d.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
export const dividedRectangle = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
const w = bbox.width + node.padding;
const h = bbox.height + node.padding;
const rectOffset = h * 0.2;
const x = -w / 2;
const y = -h / 2 - rectOffset / 2;
const { cssStyles } = node;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const pts = [
{ x, y: y + rectOffset },
{ x: -x, y: y + rectOffset },
{ x: -x, y: -y },
{ x, y: -y },
{ x, y },
{ x: -x, y },
{ x: -x, y: y + rectOffset },
];
const poly = rc.polygon(
pts.map((p) => [p.x, p.y]),
options
);
const polygon = shapeSvg.insert(() => poly, ':first-child');
polygon.attr('class', 'basic label-container');
if (cssStyles && node.look !== 'handDrawn') {
polygon.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
polygon.selectAll('path').attr('style', nodeStyles);
}
label.attr(
'transform',
`translate(${x + (node.padding ?? 0) / 2 - (bbox.x - (bbox.left ?? 0))}, ${y + rectOffset + (node.padding ?? 0) / 2 - (bbox.y - (bbox.top ?? 0))})`
);
updateNodeBounds(node, polygon);
node.intersect = function (point) {
const pos = intersect.rect(node, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,117 @@
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.d.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
export const createCylinderPathD = (
x: number,
y: number,
width: number,
height: number,
rx: number,
ry: number
): string => {
return [
`M${x},${y + ry}`,
`a${rx},${ry} 0,0,0 ${width},0`,
`a${rx},${ry} 0,0,0 ${-width},0`,
`l0,${height}`,
`a${rx},${ry} 0,0,0 ${width},0`,
`l0,${-height}`,
].join(' ');
};
export const createOuterCylinderPathD = (
x: number,
y: number,
width: number,
height: number,
rx: number,
ry: number
): string => {
return [
`M${x},${y + ry}`,
`M${x + width},${y + ry}`,
`a${rx},${ry} 0,0,0 ${-width},0`,
`l0,${height}`,
`a${rx},${ry} 0,0,0 ${width},0`,
`l0,${-height}`,
].join(' ');
};
export const createInnerCylinderPathD = (
x: number,
y: number,
width: number,
height: number,
rx: number,
ry: number
): string => {
return [`M${x - width / 2},${-height / 2}`, `a${rx},${ry} 0,0,0 ${width},0`].join(' ');
};
export const cylinder = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
const w = bbox.width + node.padding;
const rx = w / 2;
const ry = rx / (2.5 + w / 50);
const h = bbox.height + ry + node.padding;
let cylinder: d3.Selection<SVGPathElement | SVGGElement, unknown, null, undefined>;
const { cssStyles } = node;
if (node.look === 'handDrawn') {
const rc = rough.svg(shapeSvg);
const outerPathData = createOuterCylinderPathD(0, 0, w, h, rx, ry);
const innerPathData = createInnerCylinderPathD(0, ry, w, h, rx, ry);
const outerNode = rc.path(outerPathData, userNodeOverrides(node, {}));
const innerLine = rc.path(innerPathData, userNodeOverrides(node, { fill: 'none' }));
cylinder = shapeSvg.insert(() => innerLine, ':first-child');
cylinder = shapeSvg.insert(() => outerNode, ':first-child');
cylinder.attr('class', 'basic label-container');
if (cssStyles) {
cylinder.attr('style', cssStyles);
}
} else {
const pathData = createCylinderPathD(0, 0, w, h, rx, ry);
cylinder = shapeSvg
.insert('path', ':first-child')
.attr('d', pathData)
.attr('class', 'basic label-container')
.attr('style', cssStyles)
.attr('style', nodeStyles);
}
cylinder.attr('label-offset-y', ry);
cylinder.attr('transform', `translate(${-w / 2}, ${-(h / 2 + ry)})`);
updateNodeBounds(node, cylinder);
node.intersect = function (point) {
const pos = intersect.rect(node, point);
const x = pos.x - (node.x ?? 0);
if (
rx != 0 &&
(Math.abs(x) < (node.width ?? 0) / 2 ||
(Math.abs(x) == (node.width ?? 0) / 2 &&
Math.abs(pos.y - (node.y ?? 0)) > (node.height ?? 0) / 2 - ry))
) {
let y = ry * ry * (1 - (x * x) / (rx * rx));
if (y != 0) {
y = Math.sqrt(y);
}
y = ry - y;
if (point.y - (node.y ?? 0) > 0) {
y = -y;
}
pos.y += y;
}
return pos;
};
return shapeSvg;
};

View File

@@ -17,7 +17,6 @@ export const doublecircle = async (parent: SVGAElement, node: Node): Promise<SVG
const { cssStyles } = node; const { cssStyles } = node;
if (node.look === 'handDrawn') { if (node.look === 'handDrawn') {
// @ts-ignore - rough is not typed
const rc = rough.svg(shapeSvg); const rc = rough.svg(shapeSvg);
const outerOptions = userNodeOverrides(node, { roughness: 0.2, strokeWidth: 2.5 }); const outerOptions = userNodeOverrides(node, { roughness: 0.2, strokeWidth: 2.5 });

View File

@@ -47,8 +47,6 @@ export const drawRect = async (parent: SVGAElement, node: Node, options: RectOpt
.attr('class', 'basic label-container') .attr('class', 'basic label-container')
.attr('style', nodeStyles) .attr('style', nodeStyles)
.attr('rx', rx) .attr('rx', rx)
.attr('data-id', 'abc')
.attr('data-et', 'node')
.attr('ry', ry) .attr('ry', ry)
.attr('x', x) .attr('x', x)
.attr('y', y) .attr('y', y)

View File

@@ -0,0 +1,56 @@
import rough from 'roughjs';
import type { SVG } from '../../../diagram-api/types.js';
import { log } from '../../../logger.js';
import type { Node, ShapeRenderOptions } from '../../types.d.ts';
import intersect from '../intersect/index.js';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import { getNodeClasses, updateNodeBounds } from './util.js';
export const filledCircle = (
parent: SVG,
node: Node,
{ config: { themeVariables } }: ShapeRenderOptions
) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.label = '';
node.labelStyle = labelStyles;
const shapeSvg = parent
.insert('g')
.attr('class', getNodeClasses(node))
.attr('id', node.domId ?? node.id);
const radius = 7;
const { cssStyles } = node;
// @ts-expect-error shapeSvg d3 class is incorrect?
const rc = rough.svg(shapeSvg);
const { nodeBorder } = themeVariables;
const options = userNodeOverrides(node, { fillStyle: 'solid' });
if (node.look !== 'handDrawn') {
options.roughness = 0;
}
const circleNode = rc.circle(0, 0, radius * 2, options);
const filledCircle = shapeSvg.insert(() => circleNode, ':first-child');
filledCircle.selectAll('path').attr('style', `fill: ${nodeBorder} !important;`);
if (cssStyles && cssStyles.length > 0 && node.look !== 'handDrawn') {
filledCircle.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
filledCircle.selectAll('path').attr('style', nodeStyles);
}
updateNodeBounds(node, filledCircle);
node.intersect = function (point) {
log.info('filledCircle intersect', node, { radius, point });
const pos = intersect.circle(node, radius, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,63 @@
import { log } from '../../../logger.js';
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.d.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
import { createPathFromPoints } from './util.js';
export const flippedTriangle = async (parent: SVGAElement, node: Node): Promise<SVGAElement> => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
const w = bbox.width + (node.padding ?? 0);
const h = w + bbox.height;
const tw = w + bbox.height;
const points = [
{ x: 0, y: -h },
{ x: tw, y: -h },
{ x: tw / 2, y: 0 },
];
const { cssStyles } = node;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const pathData = createPathFromPoints(points);
const roughNode = rc.path(pathData, options);
const flippedTriangle = shapeSvg
.insert(() => roughNode, ':first-child')
.attr('transform', `translate(${-h / 2}, ${h / 2})`);
if (cssStyles && node.look !== 'handDrawn') {
flippedTriangle.selectChildren('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
flippedTriangle.selectChildren('path').attr('style', nodeStyles);
}
node.width = w;
node.height = h;
updateNodeBounds(node, flippedTriangle);
label.attr(
'transform',
`translate(${-bbox.width / 2 - (bbox.x - (bbox.left ?? 0))}, ${-h / 2 + (node.padding ?? 0) / 2 + (bbox.y - (bbox.top ?? 0))})`
);
node.intersect = function (point) {
log.info('Triangle intersect', node, points, point);
return intersect.polygon(node, points, point);
};
return shapeSvg;
};

View File

@@ -1,65 +1,66 @@
import { updateNodeBounds } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.js';
import type { SVG } from '../../../diagram-api/types.js';
import rough from 'roughjs'; import rough from 'roughjs';
import { solidStateFill } from './handDrawnShapeStyles.js'; import type { SVG } from '../../../diagram-api/types.js';
import { getConfig } from '../../../diagram-api/diagramAPI.js'; import type { Node, ShapeRenderOptions } from '../../types.js';
import intersect from '../intersect/index.js';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import { getNodeClasses, updateNodeBounds } from './util.js';
export const forkJoin = (parent: SVG, node: Node, dir: string) => { export const forkJoin = (
const { themeVariables } = getConfig(); parent: SVG,
const { lineColor } = themeVariables; node: Node,
{ dir, config: { state, themeVariables } }: ShapeRenderOptions
) => {
const { nodeStyles } = styles2String(node);
node.label = '';
const shapeSvg = parent const shapeSvg = parent
.insert('g') .insert('g')
.attr('class', 'node default') .attr('class', getNodeClasses(node))
.attr('id', node.domId || node.id); .attr('id', node.domId ?? node.id);
let width = 70; const { cssStyles } = node;
let height = 10; let width = Math.max(70, node?.width ?? 0);
let height = Math.max(10, node?.height ?? 0);
if (dir === 'LR') { if (dir === 'LR') {
width = 10; width = Math.max(10, node?.width ?? 0);
height = 70; height = Math.max(70, node?.height ?? 0);
} }
const x = (-1 * width) / 2; const x = (-1 * width) / 2;
const y = (-1 * height) / 2; const y = (-1 * height) / 2;
let shape; // @ts-ignore TODO: Fix rough typings
if (node.look === 'handDrawn') { const rc = rough.svg(shapeSvg);
// @ts-ignore TODO: Fix rough typings const options = userNodeOverrides(node, {
const rc = rough.svg(shapeSvg); stroke: themeVariables.lineColor,
const roughNode = rc.rectangle(x, y, width, height, solidStateFill(lineColor)); fill: themeVariables.lineColor,
shape = shapeSvg.insert(() => roughNode); });
} else {
shape = shapeSvg if (node.look !== 'handDrawn') {
.append('rect') options.roughness = 0;
.attr('x', x) options.fillStyle = 'solid';
.attr('y', y) }
.attr('width', width)
.attr('height', height) const roughNode = rc.rectangle(x, y, width, height, options);
.attr('class', 'fork-join');
const shape = shapeSvg.insert(() => roughNode, ':first-child');
if (cssStyles && node.look !== 'handDrawn') {
shape.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
shape.selectAll('path').attr('style', nodeStyles);
} }
updateNodeBounds(node, shape); updateNodeBounds(node, shape);
let nodeHeight = 0; const padding = state?.padding ?? 0;
let nodeWidth = 0; if (node.width && node.height) {
let nodePadding = 10; node.width += padding / 2 || 0;
if (node.height) { node.height += padding / 2 || 0;
nodeHeight = node.height;
} }
if (node.width) {
nodeWidth = node.width;
}
if (node.padding) {
nodePadding = node.padding;
}
node.height = nodeHeight + nodePadding / 2;
node.width = nodeWidth + nodePadding / 2;
node.intersect = function (point) { node.intersect = function (point) {
return intersect.rect(node, point); return intersect.rect(node, point);
}; };
return shapeSvg; return shapeSvg;
}; };

View File

@@ -0,0 +1,67 @@
import { log } from '../../../logger.js';
import {
labelHelper,
updateNodeBounds,
getNodeClasses,
createPathFromPoints,
generateCirclePoints,
} from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.d.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
export const halfRoundedRectangle = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const minWidth = 80,
minHeight = 50;
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
const w = Math.max(minWidth, bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0);
const h = Math.max(minHeight, bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0);
const radius = h / 2;
const { cssStyles } = node;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const points = [
{ x: -w / 2, y: -h / 2 },
{ x: w / 2 - radius, y: -h / 2 },
...generateCirclePoints(-w / 2 + radius, 0, radius, 50, 90, 270),
{ x: w / 2 - radius, y: h / 2 },
{ x: -w / 2, y: h / 2 },
];
const pathData = createPathFromPoints(points);
const shapeNode = rc.path(pathData, options);
const polygon = shapeSvg.insert(() => shapeNode, ':first-child');
polygon.attr('class', 'basic label-container');
if (cssStyles && node.look !== 'handDrawn') {
polygon.selectChildren('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
polygon.selectChildren('path').attr('style', nodeStyles);
}
// label.attr(
// 'transform',
// `translate(${-w / 2 + (node.padding ?? 0) - (bbox.x - (bbox.left ?? 0))}, ${-h / 2 + (node.padding ?? 0) - (bbox.y - (bbox.top ?? 0))})`
// );
updateNodeBounds(node, polygon);
node.intersect = function (point) {
log.info('Pill intersect', node, { radius, point });
const pos = intersect.polygon(node, points, point);
return pos;
};
return shapeSvg;
};

View File

@@ -97,9 +97,11 @@ export const userNodeOverrides = (node: Node, options: any) => {
fill: stylesMap.get('fill') || mainBkg, fill: stylesMap.get('fill') || mainBkg,
fillStyle: 'hachure', // solid fill fillStyle: 'hachure', // solid fill
fillWeight: 4, fillWeight: 4,
hachureGap: 5.2,
stroke: stylesMap.get('stroke') || nodeBorder, stroke: stylesMap.get('stroke') || nodeBorder,
seed: handDrawnSeed, seed: handDrawnSeed,
strokeWidth: 1.3, strokeWidth: stylesMap.get('stroke-width')?.replace('px', '') || 1.3,
fillLineDash: [0, 0],
}, },
options options
); );

View File

@@ -46,7 +46,6 @@ export const hexagon = async (parent: SVGAElement, node: Node): Promise<SVGAElem
const { cssStyles } = node; const { cssStyles } = node;
if (node.look === 'handDrawn') { if (node.look === 'handDrawn') {
// @ts-ignore - rough is not typed
const rc = rough.svg(shapeSvg); const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {}); const options = userNodeOverrides(node, {});
const pathData = createHexagonPathD(0, 0, w, h, m); const pathData = createHexagonPathD(0, 0, w, h, m);

View File

@@ -0,0 +1,60 @@
import { log } from '../../../logger.js';
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.d.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
export const hourglass = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.label = '';
node.labelStyle = labelStyles;
const { shapeSvg } = await labelHelper(parent, node, getNodeClasses(node));
const w = Math.max(30, node?.width ?? 0);
const h = Math.max(30, node?.height ?? 0);
const { cssStyles } = node;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const points = [
{ x: 0, y: 0 },
{ x: w, y: 0 },
{ x: 0, y: h },
{ x: w, y: h },
];
const pathData = createPathFromPoints(points);
const shapeNode = rc.path(pathData, options);
const polygon = shapeSvg.insert(() => shapeNode, ':first-child');
polygon.attr('class', 'basic label-container');
if (cssStyles && node.look !== 'handDrawn') {
polygon.selectChildren('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
polygon.selectChildren('path').attr('style', nodeStyles);
}
polygon.attr('transform', `translate(${-w / 2}, ${-h / 2})`);
updateNodeBounds(node, polygon);
// label.attr('transform', `translate(${-bbox.width / 2}, ${(h/2)})`); // To transform text below hourglass shape
node.intersect = function (point) {
log.info('Pill intersect', node, { points });
const pos = intersect.polygon(node, points, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,137 @@
import rough from 'roughjs';
import type { SVG } from '../../../diagram-api/types.js';
import { log } from '../../../logger.js';
import { getIconSVG } from '../../icons.js';
import type { Node, ShapeRenderOptions } from '../../types.d.ts';
import intersect from '../intersect/index.js';
import { compileStyles, styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import { labelHelper, updateNodeBounds } from './util.js';
export const icon = async (
parent: SVG,
node: Node,
{ config: { themeVariables, flowchart } }: ShapeRenderOptions
) => {
const { labelStyles } = styles2String(node);
node.labelStyle = labelStyles;
const assetHeight = node.assetHeight ?? 48;
const assetWidth = node.assetWidth ?? 48;
const iconSize = Math.max(assetHeight, assetWidth);
const defaultWidth = flowchart?.wrappingWidth;
node.width = Math.max(iconSize, defaultWidth ?? 0);
const { shapeSvg, bbox, label } = await labelHelper(parent, node, 'icon-shape default');
const topLabel = node.pos === 't';
const height = iconSize;
const width = iconSize;
const { nodeBorder } = themeVariables;
const { stylesMap } = compileStyles(node);
const x = -width / 2;
const y = -height / 2;
const labelPadding = node.label ? 8 : 0;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, { stroke: 'none', fill: 'none' });
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const iconNode = rc.rectangle(x, y, width, height, options);
const outerWidth = Math.max(width, bbox.width);
const outerHeight = height + bbox.height + labelPadding;
const outerNode = rc.rectangle(-outerWidth / 2, -outerHeight / 2, outerWidth, outerHeight, {
...options,
fill: 'transparent',
stroke: 'none',
});
const iconShape = shapeSvg.insert(() => iconNode, ':first-child');
const outerShape = shapeSvg.insert(() => outerNode);
if (node.icon) {
const iconElem = shapeSvg.append('g');
iconElem.html(
`<g>${await getIconSVG(node.icon, {
height: iconSize,
width: iconSize,
fallbackPrefix: '',
})}</g>`
);
const iconBBox = iconElem.node().getBBox();
const iconWidth = iconBBox.width;
const iconHeight = iconBBox.height;
const iconX = iconBBox.x;
const iconY = iconBBox.y;
iconElem.attr(
'transform',
`translate(${-iconWidth / 2 - iconX},${
topLabel
? bbox.height / 2 + labelPadding / 2 - iconHeight / 2 - iconY
: -bbox.height / 2 - labelPadding / 2 - iconHeight / 2 - iconY
})`
);
iconElem.selectAll('path').attr('fill', stylesMap.get('stroke') || nodeBorder);
}
label.attr(
'transform',
`translate(${-bbox.width / 2 - (bbox.x - (bbox.left ?? 0))},${
topLabel ? -outerHeight / 2 : outerHeight / 2 - bbox.height
})`
);
iconShape.attr(
'transform',
`translate(${0},${
topLabel ? bbox.height / 2 + labelPadding / 2 : -bbox.height / 2 - labelPadding / 2
})`
);
updateNodeBounds(node, outerShape);
node.intersect = function (point) {
log.info('iconSquare intersect', node, point);
if (!node.label) {
return intersect.rect(node, point);
}
const dx = node.x ?? 0;
const dy = node.y ?? 0;
const nodeHeight = node.height ?? 0;
let points = [];
if (topLabel) {
points = [
{ x: dx - bbox.width / 2, y: dy - nodeHeight / 2 },
{ x: dx + bbox.width / 2, y: dy - nodeHeight / 2 },
{ x: dx + bbox.width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding },
{ x: dx + width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding },
{ x: dx + width / 2, y: dy + nodeHeight / 2 },
{ x: dx - width / 2, y: dy + nodeHeight / 2 },
{ x: dx - width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding },
{ x: dx - bbox.width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding },
];
} else {
points = [
{ x: dx - width / 2, y: dy - nodeHeight / 2 },
{ x: dx + width / 2, y: dy - nodeHeight / 2 },
{ x: dx + width / 2, y: dy - nodeHeight / 2 + height },
{ x: dx + bbox.width / 2, y: dy - nodeHeight / 2 + height },
{ x: dx + bbox.width / 2 / 2, y: dy + nodeHeight / 2 },
{ x: dx - bbox.width / 2, y: dy + nodeHeight / 2 },
{ x: dx - bbox.width / 2, y: dy - nodeHeight / 2 + height },
{ x: dx - width / 2, y: dy - nodeHeight / 2 + height },
];
}
const pos = intersect.polygon(node, points, point);
return pos;
};
return shapeSvg;
};

Some files were not shown because too many files have changed in this diff Show More