mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-04 16:16:41 +02:00
Compare commits
77 Commits
applitools
...
demo/useca
Author | SHA1 | Date | |
---|---|---|---|
![]() |
89b29898d2 | ||
![]() |
2972bf25bf | ||
![]() |
6b1a7a9e1a | ||
![]() |
33bc4a0b4e | ||
![]() |
c6f25167a2 | ||
![]() |
82800a2c84 | ||
![]() |
27e700debd | ||
![]() |
01e47333d5 | ||
![]() |
d47ba7c2d1 | ||
![]() |
b1c4eb3f5c | ||
![]() |
310fcd2292 | ||
![]() |
04b6fc1280 | ||
![]() |
f46a151075 | ||
![]() |
b7e9d02b7c | ||
![]() |
0ef3130510 | ||
![]() |
862d40cc3a | ||
![]() |
00f5700320 | ||
![]() |
e32dc8513f | ||
![]() |
50127f3ffe | ||
![]() |
29bb0e3dca | ||
![]() |
1221de4c2d | ||
![]() |
c41e08cb7a | ||
![]() |
4760ed8893 | ||
![]() |
31ecf31c2e | ||
![]() |
b52766653c | ||
![]() |
6d9fad01a9 | ||
![]() |
8322a63598 | ||
![]() |
075e1b5e1f | ||
![]() |
3c9bd7be29 | ||
![]() |
6995248443 | ||
![]() |
93467a6fce | ||
![]() |
95d48e3497 | ||
![]() |
29886b8dd4 | ||
![]() |
e438e035bc | ||
![]() |
2bc5b6d2fa | ||
![]() |
e0b45c2d2b | ||
![]() |
d4c76968e9 | ||
![]() |
2cfebef122 | ||
![]() |
c0e2d4a23b | ||
![]() |
7bdcf93412 | ||
![]() |
d86e46b705 | ||
![]() |
71e09bcaef | ||
![]() |
cba659d097 | ||
![]() |
f7a0844a31 | ||
![]() |
2817383714 | ||
![]() |
80c6faf4d5 | ||
![]() |
9f6ee53382 | ||
![]() |
3248bf3da4 | ||
![]() |
e7a7ff8a2a | ||
![]() |
68fc68c239 | ||
![]() |
769b362005 | ||
![]() |
e4d3aa4610 | ||
![]() |
716548548a | ||
![]() |
4bece53a3c | ||
![]() |
297be4a868 | ||
![]() |
fb6ace73b5 | ||
![]() |
bf362673fc | ||
![]() |
d042b21b12 | ||
![]() |
677ff82d13 | ||
![]() |
981829a426 | ||
![]() |
327a5aa9fd | ||
![]() |
848f69a75c | ||
![]() |
99dbeba407 | ||
![]() |
d525acc05b | ||
![]() |
4915545429 | ||
![]() |
334fe87bc6 | ||
![]() |
283e7810d2 | ||
![]() |
237d01d510 | ||
![]() |
afeb761296 | ||
![]() |
3abcfbb8d2 | ||
![]() |
ee82694645 | ||
![]() |
012530e98e | ||
![]() |
a4a27611dd | ||
![]() |
5055ade44e | ||
![]() |
b61bec8faf | ||
![]() |
76d073b027 | ||
![]() |
cc476d59d1 |
5
.changeset/clean-wolves-turn.md
Normal file
5
.changeset/clean-wolves-turn.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Render newlines as spaces in class diagrams
|
5
.changeset/crazy-loops-matter.md
Normal file
5
.changeset/crazy-loops-matter.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Handle arrows correctly when auto number is enabled
|
5
.changeset/hungry-baths-glow.md
Normal file
5
.changeset/hungry-baths-glow.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': minor
|
||||
---
|
||||
|
||||
feat: Added support for new participant types (`actor`, `boundary`, `control`, `entity`, `database`, `collections`, `queue`) in `sequenceDiagram`.
|
95
.github/workflows/applitools-tests.yml
vendored
95
.github/workflows/applitools-tests.yml
vendored
@@ -1,95 +0,0 @@
|
||||
name: Applitools E2E (Develop Branch) - Fixed
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- applitools_workflow
|
||||
|
||||
workflow_dispatch:
|
||||
# Manual triggering only - to limit Applitools usage
|
||||
inputs:
|
||||
parent_branch:
|
||||
required: true
|
||||
type: string
|
||||
default: master
|
||||
description: 'Parent branch to use for PRs'
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
# on PRs from forks, this secret will always be empty, for security reasons
|
||||
USE_APPLI: ${{ secrets.APPLITOOLS_API_KEY && 'true' || '' }}
|
||||
|
||||
jobs:
|
||||
applitools-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
container:
|
||||
image: cypress/browsers:node-22.18.0-chrome-139.0.7258.127-1-ff-141.0.3-edge-139.0.3405.86-1
|
||||
options: --user root --shm-size=2gb
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Check Applitools API Key
|
||||
run: |
|
||||
if [ -z "${{ secrets.APPLITOOLS_API_KEY }}" ]; then
|
||||
echo "::error::APPLITOOLS_API_KEY secret is not set. Please add it to your repository secrets."
|
||||
exit 1
|
||||
else
|
||||
echo "✅ Applitools API Key is present"
|
||||
fi
|
||||
|
||||
- name: Setup Node.js and pnpm
|
||||
run: |
|
||||
# Install pnpm globally
|
||||
npm install -g pnpm@10.4.1
|
||||
|
||||
# Verify installations
|
||||
node --version
|
||||
pnpm --version
|
||||
|
||||
- name: Setup Applitools Environment
|
||||
run: |
|
||||
# Set Applitools environment variables for proper CI integration
|
||||
echo "APPLITOOLS_BATCH_ID=${{ github.run_id }}-${{ github.run_attempt }}" >> $GITHUB_ENV
|
||||
echo "APPLITOOLS_BATCH_NAME=GitHub Actions - ${{ github.workflow }}" >> $GITHUB_ENV
|
||||
echo "APPLITOOLS_SERVER_URL=https://eyes.applitools.com" >> $GITHUB_ENV
|
||||
# Force disable local Eyes server
|
||||
echo "APPLITOOLS_DISABLE_LOCAL_EYES_SERVER=true" >> $GITHUB_ENV
|
||||
|
||||
- name: Verify Cypress Installation
|
||||
run: |
|
||||
npx cypress verify
|
||||
npx cypress info
|
||||
|
||||
- name: Run Cypress with Applitools (single spec)
|
||||
uses: cypress-io/github-action@v6
|
||||
with:
|
||||
command: pnpm cypress
|
||||
start: pnpm dev
|
||||
wait-on: http://localhost:9000
|
||||
wait-on-timeout: 180
|
||||
browser: chrome
|
||||
headless: true
|
||||
env:
|
||||
# Ensure these are visible to Cypress + Applitools
|
||||
APPLITOOLS_API_KEY: ${{ secrets.APPLITOOLS_API_KEY }}
|
||||
APPLITOOLS_BATCH_ID: ${{ env.APPLITOOLS_BATCH_ID }}
|
||||
APPLITOOLS_BATCH_NAME: ${{ env.APPLITOOLS_BATCH_NAME }}
|
||||
APPLITOOLS_SERVER_URL: ${{ env.APPLITOOLS_SERVER_URL }}
|
||||
APPLITOOLS_DISABLE_LOCAL_EYES_SERVER: true
|
||||
USE_APPLI: true
|
||||
# Force Applitools to use cloud service instead of local server
|
||||
APPLITOOLS_DONT_CLOSE_BATCHES: false
|
||||
APPLITOOLS_SAVE_DEBUG_SCREENSHOTS: false
|
||||
# Disable any local server connections
|
||||
APPLITOOLS_PROXY_URL: ''
|
||||
# GitHub Actions CI environment
|
||||
CI: true
|
||||
# Disable Chrome sandbox for container
|
||||
ELECTRON_EXTRA_LAUNCH_ARGS: '--disable-dev-shm-usage'
|
3
.github/workflows/e2e-applitools.yml
vendored
3
.github/workflows/e2e-applitools.yml
vendored
@@ -23,9 +23,6 @@ env:
|
||||
jobs:
|
||||
e2e-applitools:
|
||||
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:
|
||||
- if: ${{ ! env.USE_APPLI }}
|
||||
name: Warn if not using Applitools
|
||||
|
2
.github/workflows/e2e-timings.yml
vendored
2
.github/workflows/e2e-timings.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Commit and create pull request
|
||||
uses: peter-evans/create-pull-request@cb4d3bfce175d44325c6b7697f81e0afe8a79bdf
|
||||
uses: peter-evans/create-pull-request@18e469570b1cf0dfc11d60ec121099f8ff3e617a
|
||||
with:
|
||||
add-paths: |
|
||||
cypress/timings.json
|
||||
|
@@ -1,38 +0,0 @@
|
||||
export default {
|
||||
// Explicitly set the server URL to use Applitools cloud service
|
||||
serverUrl: 'https://eyes.applitools.com',
|
||||
|
||||
// API key from environment variable
|
||||
apiKey: process.env.APPLITOOLS_API_KEY,
|
||||
|
||||
// Batch configuration
|
||||
batch: {
|
||||
name: 'Cypress Tests',
|
||||
id: process.env.APPLITOOLS_BATCH_ID,
|
||||
},
|
||||
|
||||
// Browser configuration for CI
|
||||
browser: [{ name: 'chrome', width: 1440, height: 1024 }],
|
||||
|
||||
// Test concurrency (reduce for stability in CI)
|
||||
testConcurrency: 1,
|
||||
|
||||
// Viewport size
|
||||
viewportSize: { width: 1440, height: 1024 },
|
||||
|
||||
// Force full page screenshots
|
||||
forceFullPageScreenshot: true,
|
||||
|
||||
// Don't fail tests on visual differences (optional)
|
||||
exitcode: false,
|
||||
|
||||
// Additional settings for CI stability
|
||||
matchTimeout: 2000,
|
||||
|
||||
// Disable local Eyes server
|
||||
// cspell:ignore dont
|
||||
dontCloseBatches: false,
|
||||
|
||||
// Save debug screenshots on failure
|
||||
saveDebugScreenshots: process.env.CI ? false : true,
|
||||
} as const;
|
@@ -1,90 +1,42 @@
|
||||
// cypress.config.ts
|
||||
import { defineConfig } from 'cypress';
|
||||
import eyesPlugin from '@applitools/eyes-cypress';
|
||||
import { registerArgosTask } from '@argos-ci/cypress/task';
|
||||
import coverage from '@cypress/code-coverage/task.js';
|
||||
import { defineConfig } from 'cypress';
|
||||
import { addMatchImageSnapshotPlugin } from 'cypress-image-snapshot/plugin.js';
|
||||
import cypressSplit from 'cypress-split';
|
||||
import eyesPlugin from '@applitools/eyes-cypress';
|
||||
|
||||
// --- Base Cypress config ---
|
||||
const baseConfig = defineConfig({
|
||||
projectId: 'n2sma2',
|
||||
viewportWidth: 1440,
|
||||
viewportHeight: 1024,
|
||||
e2e: {
|
||||
specPattern: 'cypress/integration/**/*.{js,ts}',
|
||||
setupNodeEvents(on, config) {
|
||||
// Code coverage
|
||||
coverage(on, config);
|
||||
export default eyesPlugin(
|
||||
defineConfig({
|
||||
projectId: 'n2sma2',
|
||||
viewportWidth: 1440,
|
||||
viewportHeight: 1024,
|
||||
e2e: {
|
||||
specPattern: 'cypress/integration/**/*.{js,ts}',
|
||||
setupNodeEvents(on, config) {
|
||||
coverage(on, config);
|
||||
cypressSplit(on, config);
|
||||
on('before:browser:launch', (browser, launchOptions) => {
|
||||
if (browser.name === 'chrome' && browser.isHeadless) {
|
||||
launchOptions.args.push('--window-size=1440,1024', '--force-device-scale-factor=1');
|
||||
}
|
||||
return launchOptions;
|
||||
});
|
||||
// copy any needed variables from process.env to config.env
|
||||
config.env.useAppli = process.env.USE_APPLI ? true : false;
|
||||
config.env.useArgos = process.env.RUN_VISUAL_TEST === 'true';
|
||||
|
||||
// Test splitting
|
||||
cypressSplit(on, config);
|
||||
|
||||
// Browser tweaks for CI
|
||||
on('before:browser:launch', (browser, launchOptions) => {
|
||||
if (browser.name === 'chrome' && browser.isHeadless) {
|
||||
launchOptions.args.push(
|
||||
'--window-size=1440,1024',
|
||||
'--force-device-scale-factor=1',
|
||||
'--no-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-gpu',
|
||||
'--disable-web-security'
|
||||
);
|
||||
if (config.env.useArgos) {
|
||||
registerArgosTask(on, config, {
|
||||
// Enable upload to Argos only when it runs on CI.
|
||||
uploadToArgos: !!process.env.CI,
|
||||
});
|
||||
} else {
|
||||
addMatchImageSnapshotPlugin(on, config);
|
||||
}
|
||||
return launchOptions;
|
||||
});
|
||||
|
||||
// Env flags
|
||||
config.env.useAppli = process.env.USE_APPLI === 'true';
|
||||
config.env.useArgos = process.env.RUN_VISUAL_TEST === 'true';
|
||||
|
||||
if (config.env.useArgos) {
|
||||
registerArgosTask(on, config, { uploadToArgos: !!process.env.CI });
|
||||
} else {
|
||||
addMatchImageSnapshotPlugin(on, config);
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
},
|
||||
video: false,
|
||||
defaultCommandTimeout: 10000,
|
||||
requestTimeout: 10000,
|
||||
responseTimeout: 10000,
|
||||
pageLoadTimeout: 30000,
|
||||
});
|
||||
|
||||
// --- Conditional Applitools wrapper ---
|
||||
function withApplitools(config: Cypress.ConfigOptions): Cypress.ConfigOptions {
|
||||
const shouldLoadApplitools = !!process.env.APPLITOOLS_API_KEY && process.env.USE_APPLI === 'true';
|
||||
|
||||
if (shouldLoadApplitools) {
|
||||
return eyesPlugin(config, {
|
||||
serverUrl: 'https://eyes.applitools.com',
|
||||
batch: {
|
||||
name:
|
||||
process.env.APPLITOOLS_BATCH_NAME ||
|
||||
`GitHub Actions - ${process.env.GITHUB_WORKFLOW || 'Cypress Tests'}`,
|
||||
id:
|
||||
process.env.APPLITOOLS_BATCH_ID ||
|
||||
`${process.env.GITHUB_RUN_ID}-${process.env.GITHUB_RUN_ATTEMPT}`,
|
||||
// do not forget to return the changed config object!
|
||||
return config;
|
||||
},
|
||||
testConcurrency: 1,
|
||||
browser: { name: 'chrome', width: 1440, height: 1024 },
|
||||
viewportSize: { width: 1440, height: 1024 },
|
||||
matchTimeout: 2000,
|
||||
forceFullPageScreenshot: true,
|
||||
// cspell:ignore dont
|
||||
dontCloseBatches: false,
|
||||
saveDebugScreenshots: false,
|
||||
saveDiffs: false,
|
||||
concurrency: 1,
|
||||
});
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
// --- Export final config ---
|
||||
export default withApplitools(baseConfig);
|
||||
},
|
||||
video: false,
|
||||
})
|
||||
);
|
||||
|
@@ -524,5 +524,18 @@ describe('Class diagram', () => {
|
||||
`,
|
||||
{}
|
||||
);
|
||||
it('should handle an empty class body with empty braces', () => {
|
||||
imgSnapshotTest(
|
||||
` classDiagram
|
||||
class FooBase~T~ {}
|
||||
class Bar {
|
||||
+Zip
|
||||
+Zap()
|
||||
}
|
||||
FooBase <|-- Ba
|
||||
`,
|
||||
{ flowchart: { defaultRenderer: 'elk' } }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
659
cypress/integration/rendering/sequencediagram-v2.spec.js
Normal file
659
cypress/integration/rendering/sequencediagram-v2.spec.js
Normal file
@@ -0,0 +1,659 @@
|
||||
import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts';
|
||||
|
||||
const looks = ['classic'];
|
||||
const participantTypes = [
|
||||
{ type: 'participant', display: 'participant' },
|
||||
{ type: 'actor', display: 'actor' },
|
||||
{ type: 'boundary', display: 'boundary' },
|
||||
{ type: 'control', display: 'control' },
|
||||
{ type: 'entity', display: 'entity' },
|
||||
{ type: 'database', display: 'database' },
|
||||
{ type: 'collections', display: 'collections' },
|
||||
{ type: 'queue', display: 'queue' },
|
||||
];
|
||||
|
||||
const restrictedTypes = ['boundary', 'control', 'entity', 'database', 'collections', 'queue'];
|
||||
|
||||
const interactionTypes = ['->>', '-->>', '->', '-->', '-x', '--x', '->>+', '-->>+'];
|
||||
|
||||
const notePositions = ['left of', 'right of', 'over'];
|
||||
|
||||
function getParticipantLine(name, type, alias) {
|
||||
if (restrictedTypes.includes(type)) {
|
||||
return ` participant ${name}@{ "type" : "${type}" }\n`;
|
||||
} else if (alias) {
|
||||
return ` participant ${name}@{ "type" : "${type}" } \n`;
|
||||
} else {
|
||||
return ` participant ${name}@{ "type" : "${type}" }\n`;
|
||||
}
|
||||
}
|
||||
|
||||
looks.forEach((look) => {
|
||||
describe(`Sequence Diagram Tests - ${look} look`, () => {
|
||||
it('should render all participant types', () => {
|
||||
let diagramCode = `sequenceDiagram\n`;
|
||||
participantTypes.forEach((pt, index) => {
|
||||
const name = `${pt.display}${index}`;
|
||||
diagramCode += getParticipantLine(name, pt.type);
|
||||
});
|
||||
for (let i = 0; i < participantTypes.length - 1; i++) {
|
||||
diagramCode += ` ${participantTypes[i].display}${i} ->> ${participantTypes[i + 1].display}${i + 1}: Message ${i}\n`;
|
||||
}
|
||||
imgSnapshotTest(diagramCode, { look, sequence: { diagramMarginX: 50, diagramMarginY: 10 } });
|
||||
});
|
||||
|
||||
it('should render all interaction types', () => {
|
||||
let diagramCode = `sequenceDiagram\n`;
|
||||
diagramCode += getParticipantLine('A', 'actor');
|
||||
diagramCode += getParticipantLine('B', 'boundary');
|
||||
interactionTypes.forEach((interaction, index) => {
|
||||
diagramCode += ` A ${interaction} B: ${interaction} message ${index}\n`;
|
||||
});
|
||||
imgSnapshotTest(diagramCode, { look });
|
||||
});
|
||||
|
||||
it('should render participant creation and destruction', () => {
|
||||
let diagramCode = `sequenceDiagram\n`;
|
||||
participantTypes.forEach((pt, index) => {
|
||||
const name = `${pt.display}${index}`;
|
||||
diagramCode += getParticipantLine('A', pt.type);
|
||||
diagramCode += getParticipantLine('B', pt.type);
|
||||
diagramCode += ` create participant ${name}@{ "type" : "${pt.type}" }\n`;
|
||||
diagramCode += ` A ->> ${name}: Hello ${pt.display}\n`;
|
||||
if (index % 2 === 0) {
|
||||
diagramCode += ` destroy ${name}\n`;
|
||||
}
|
||||
});
|
||||
imgSnapshotTest(diagramCode, { look });
|
||||
});
|
||||
|
||||
it('should render notes in all positions', () => {
|
||||
let diagramCode = `sequenceDiagram\n`;
|
||||
diagramCode += getParticipantLine('A', 'actor');
|
||||
diagramCode += getParticipantLine('B', 'boundary');
|
||||
notePositions.forEach((position, index) => {
|
||||
diagramCode += ` Note ${position} A: Note ${position} ${index}\n`;
|
||||
});
|
||||
diagramCode += ` A ->> B: Message with notes\n`;
|
||||
imgSnapshotTest(diagramCode, { look });
|
||||
});
|
||||
|
||||
it('should render parallel interactions', () => {
|
||||
let diagramCode = `sequenceDiagram\n`;
|
||||
participantTypes.slice(0, 4).forEach((pt, index) => {
|
||||
diagramCode += getParticipantLine(`${pt.display}${index}`, pt.type);
|
||||
});
|
||||
diagramCode += ` par Parallel actions\n`;
|
||||
for (let i = 0; i < 3; i += 2) {
|
||||
diagramCode += ` ${participantTypes[i].display}${i} ->> ${participantTypes[i + 1].display}${i + 1}: Message ${i}\n`;
|
||||
if (i < participantTypes.length - 2) {
|
||||
diagramCode += ` and\n`;
|
||||
}
|
||||
}
|
||||
diagramCode += ` end\n`;
|
||||
imgSnapshotTest(diagramCode, { look });
|
||||
});
|
||||
|
||||
it('should render alternative flows', () => {
|
||||
let diagramCode = `sequenceDiagram\n`;
|
||||
diagramCode += getParticipantLine('A', 'actor');
|
||||
diagramCode += getParticipantLine('B', 'boundary');
|
||||
diagramCode += ` alt Successful case\n`;
|
||||
diagramCode += ` A ->> B: Request\n`;
|
||||
diagramCode += ` B -->> A: Success\n`;
|
||||
diagramCode += ` else Failure case\n`;
|
||||
diagramCode += ` A ->> B: Request\n`;
|
||||
diagramCode += ` B --x A: Failure\n`;
|
||||
diagramCode += ` end\n`;
|
||||
imgSnapshotTest(diagramCode, { look });
|
||||
});
|
||||
|
||||
it('should render loops', () => {
|
||||
let diagramCode = `sequenceDiagram\n`;
|
||||
participantTypes.slice(0, 3).forEach((pt, index) => {
|
||||
diagramCode += getParticipantLine(`${pt.display}${index}`, pt.type);
|
||||
});
|
||||
diagramCode += ` loop For each participant\n`;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
diagramCode += ` ${participantTypes[0].display}0 ->> ${participantTypes[1].display}1: Message ${i}\n`;
|
||||
}
|
||||
diagramCode += ` end\n`;
|
||||
imgSnapshotTest(diagramCode, { look });
|
||||
});
|
||||
|
||||
it('should render boxes around groups', () => {
|
||||
let diagramCode = `sequenceDiagram\n`;
|
||||
diagramCode += ` box Group 1\n`;
|
||||
participantTypes.slice(0, 3).forEach((pt, index) => {
|
||||
diagramCode += ` ${getParticipantLine(`${pt.display}${index}`, pt.type)}`;
|
||||
});
|
||||
diagramCode += ` end\n`;
|
||||
diagramCode += ` box rgb(200,220,255) Group 2\n`;
|
||||
participantTypes.slice(3, 6).forEach((pt, index) => {
|
||||
diagramCode += ` ${getParticipantLine(`${pt.display}${index}`, pt.type)}`;
|
||||
});
|
||||
diagramCode += ` end\n`;
|
||||
diagramCode += ` ${participantTypes[0].display}0 ->> ${participantTypes[3].display}0: Cross-group message\n`;
|
||||
imgSnapshotTest(diagramCode, { look });
|
||||
});
|
||||
|
||||
it('should render with different font settings', () => {
|
||||
let diagramCode = `sequenceDiagram\n`;
|
||||
participantTypes.slice(0, 3).forEach((pt, index) => {
|
||||
diagramCode += getParticipantLine(`${pt.display}${index}`, pt.type);
|
||||
});
|
||||
diagramCode += ` ${participantTypes[0].display}0 ->> ${participantTypes[1].display}1: Regular message\n`;
|
||||
diagramCode += ` Note right of ${participantTypes[1].display}1: Regular note\n`;
|
||||
imgSnapshotTest(diagramCode, {
|
||||
look,
|
||||
sequence: {
|
||||
actorFontFamily: 'courier',
|
||||
actorFontSize: 14,
|
||||
messageFontFamily: 'Arial',
|
||||
messageFontSize: 12,
|
||||
noteFontFamily: 'times',
|
||||
noteFontSize: 16,
|
||||
noteAlign: 'left',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Additional tests for specific combinations
|
||||
describe('Sequence Diagram Special Cases', () => {
|
||||
it('should render complex sequence with all features', () => {
|
||||
const diagramCode = `
|
||||
sequenceDiagram
|
||||
box rgb(200,220,255) Authentication
|
||||
actor User
|
||||
participant LoginUI@{ "type": "boundary" }
|
||||
participant AuthService@{ "type": "control" }
|
||||
participant UserDB@{ "type": "database" }
|
||||
end
|
||||
|
||||
box rgb(200,255,220) Order Processing
|
||||
participant Order@{ "type": "entity" }
|
||||
participant OrderQueue@{ "type": "queue" }
|
||||
participant AuditLogs@{ "type": "collections" }
|
||||
end
|
||||
|
||||
User ->> LoginUI: Enter credentials
|
||||
LoginUI ->> AuthService: Validate
|
||||
AuthService ->> UserDB: Query user
|
||||
UserDB -->> AuthService: User data
|
||||
alt Valid credentials
|
||||
AuthService -->> LoginUI: Success
|
||||
LoginUI -->> User: Welcome
|
||||
|
||||
par Place order
|
||||
User ->> Order: New order
|
||||
Order ->> OrderQueue: Process
|
||||
and
|
||||
Order ->> AuditLogs: Record
|
||||
end
|
||||
|
||||
loop Until confirmed
|
||||
OrderQueue ->> Order: Update status
|
||||
Order -->> User: Notification
|
||||
end
|
||||
else Invalid credentials
|
||||
AuthService --x LoginUI: Failure
|
||||
LoginUI --x User: Retry
|
||||
end
|
||||
`;
|
||||
imgSnapshotTest(diagramCode, {});
|
||||
});
|
||||
|
||||
it('should render with wrapped messages and notes', () => {
|
||||
const diagramCode = `
|
||||
sequenceDiagram
|
||||
participant A
|
||||
participant B
|
||||
|
||||
A ->> B: This is a very long message that should wrap properly in the diagram rendering
|
||||
Note over A,B: This is a very long note that should also wrap properly when rendered in the diagram
|
||||
|
||||
par Wrapped parallel
|
||||
A ->> B: Parallel message 1<br>with explicit line break
|
||||
and
|
||||
B ->> A: Parallel message 2<br>with explicit line break
|
||||
end
|
||||
|
||||
loop Wrapped loop
|
||||
Note right of B: This is a long note<br>in a loop
|
||||
A ->> B: Message in loop
|
||||
end
|
||||
`;
|
||||
imgSnapshotTest(diagramCode, { sequence: { wrap: true } });
|
||||
});
|
||||
describe('Sequence Diagram Rendering with Different Participant Types', () => {
|
||||
it('should render a sequence diagram with various participant types', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
participant User@{ "type": "actor" }
|
||||
participant AuthService@{ "type": "control" }
|
||||
participant UI@{ "type": "boundary" }
|
||||
participant OrderController@{ "type": "control" }
|
||||
participant Product@{ "type": "entity" }
|
||||
participant MongoDB@{ "type": "database" }
|
||||
participant Products@{ "type": "collections" }
|
||||
participant OrderQueue@{ "type": "queue" }
|
||||
User ->> UI: Login request
|
||||
UI ->> AuthService: Validate credentials
|
||||
AuthService -->> UI: Authentication token
|
||||
UI ->> OrderController: Place order
|
||||
OrderController ->> Product: Check availability
|
||||
Product -->> OrderController: Available
|
||||
OrderController ->> MongoDB: Save order
|
||||
MongoDB -->> OrderController: Order saved
|
||||
OrderController ->> OrderQueue: Process payment
|
||||
OrderQueue -->> User: Order confirmation
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render participant creation and destruction with different types', () => {
|
||||
imgSnapshotTest(`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "boundary" }
|
||||
Alice->>Bob: Hello Bob, how are you ?
|
||||
Bob->>Alice: Fine, thank you. And you?
|
||||
create participant Carl@{ "type" : "control" }
|
||||
Alice->>Carl: Hi Carl!
|
||||
create actor D as Donald
|
||||
Carl->>D: Hi!
|
||||
destroy Carl
|
||||
Alice-xCarl: We are too many
|
||||
destroy Bob
|
||||
Bob->>Alice: I agree
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle complex interactions between different participant types', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
box rgb(200,220,255) Authentication
|
||||
participant User@{ "type": "actor" }
|
||||
participant LoginUI@{ "type": "boundary" }
|
||||
participant AuthService@{ "type": "control" }
|
||||
participant UserDB@{ "type": "database" }
|
||||
end
|
||||
|
||||
box rgb(200,255,220) Order Processing
|
||||
participant Order@{ "type": "entity" }
|
||||
participant OrderQueue@{ "type": "queue" }
|
||||
participant AuditLogs@{ "type": "collections" }
|
||||
end
|
||||
|
||||
User ->> LoginUI: Enter credentials
|
||||
LoginUI ->> AuthService: Validate
|
||||
AuthService ->> UserDB: Query user
|
||||
UserDB -->> AuthService: User data
|
||||
|
||||
alt Valid credentials
|
||||
AuthService -->> LoginUI: Success
|
||||
LoginUI -->> User: Welcome
|
||||
|
||||
par Place order
|
||||
User ->> Order: New order
|
||||
Order ->> OrderQueue: Process
|
||||
and
|
||||
Order ->> AuditLogs: Record
|
||||
end
|
||||
|
||||
loop Until confirmed
|
||||
OrderQueue ->> Order: Update status
|
||||
Order -->> User: Notification
|
||||
end
|
||||
else Invalid credentials
|
||||
AuthService --x LoginUI: Failure
|
||||
LoginUI --x User: Retry
|
||||
end
|
||||
`,
|
||||
{ sequence: { useMaxWidth: false } }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render parallel processes with different participant types', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
participant Customer@{ "type": "actor" }
|
||||
participant Frontend@{ "type": "participant" }
|
||||
participant PaymentService@{ "type": "boundary" }
|
||||
participant InventoryManager@{ "type": "control" }
|
||||
participant Order@{ "type": "entity" }
|
||||
participant OrdersDB@{ "type": "database" }
|
||||
participant NotificationQueue@{ "type": "queue" }
|
||||
|
||||
Customer ->> Frontend: Place order
|
||||
Frontend ->> Order: Create order
|
||||
par Parallel Processing
|
||||
Order ->> PaymentService: Process payment
|
||||
and
|
||||
Order ->> InventoryManager: Reserve items
|
||||
end
|
||||
PaymentService -->> Order: Payment confirmed
|
||||
InventoryManager -->> Order: Items reserved
|
||||
Order ->> OrdersDB: Save finalized order
|
||||
OrdersDB -->> Order: Order saved
|
||||
Order ->> NotificationQueue: Send confirmation
|
||||
NotificationQueue -->> Customer: Order confirmation
|
||||
`
|
||||
);
|
||||
});
|
||||
});
|
||||
it('should render different participant types with notes and loops', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
actor Admin
|
||||
participant Dashboard
|
||||
participant AuthService@{ "type" : "boundary" }
|
||||
participant UserManager@{ "type" : "control" }
|
||||
participant UserProfile@{ "type" : "entity" }
|
||||
participant UserDB@{ "type" : "database" }
|
||||
participant Logs@{ "type" : "database" }
|
||||
|
||||
Admin ->> Dashboard: Open user management
|
||||
loop Authentication check
|
||||
Dashboard ->> AuthService: Verify admin rights
|
||||
AuthService ->> Dashboard: Access granted
|
||||
end
|
||||
Dashboard ->> UserManager: List users
|
||||
UserManager ->> UserDB: Query users
|
||||
UserDB ->> UserManager: Return user data
|
||||
Note right of UserDB: Encrypted data<br/>requires decryption
|
||||
UserManager ->> UserProfile: Format profiles
|
||||
UserProfile ->> UserManager: Formatted data
|
||||
UserManager ->> Dashboard: Display users
|
||||
Dashboard ->> Logs: Record access
|
||||
Logs ->> Admin: Audit trail
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render different participant types with alternative flows', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
actor Client
|
||||
participant MobileApp
|
||||
participant CloudService@{ "type" : "boundary" }
|
||||
participant DataProcessor@{ "type" : "control" }
|
||||
participant Transaction@{ "type" : "entity" }
|
||||
participant TransactionsDB@{ "type" : "database" }
|
||||
participant EventBus@{ "type" : "queue" }
|
||||
|
||||
Client ->> MobileApp: Initiate transaction
|
||||
MobileApp ->> CloudService: Authenticate
|
||||
alt Authentication successful
|
||||
CloudService -->> MobileApp: Auth token
|
||||
MobileApp ->> DataProcessor: Process data
|
||||
DataProcessor ->> Transaction: Create transaction
|
||||
Transaction ->> TransactionsDB: Save record
|
||||
TransactionsDB -->> Transaction: Confirmation
|
||||
Transaction ->> EventBus: Publish event
|
||||
EventBus -->> Client: Notification
|
||||
else Authentication failed
|
||||
CloudService -->> MobileApp: Error
|
||||
MobileApp -->> Client: Show error
|
||||
end
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render different participant types with wrapping text', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
participant B@{ "type" : "boundary" }
|
||||
participant C@{ "type" : "control" }
|
||||
participant E@{ "type" : "entity" }
|
||||
participant DB@{ "type" : "database" }
|
||||
participant COL@{ "type" : "collections" }
|
||||
participant Q@{ "type" : "queue" }
|
||||
|
||||
FE ->> B: Another long message<br/>with explicit<br/>line breaks
|
||||
B -->> FE: Response message that is also quite long and needs to wrap
|
||||
FE ->> C: Process data
|
||||
C ->> E: Validate
|
||||
E -->> C: Validation result
|
||||
C ->> DB: Save
|
||||
DB -->> C: Save result
|
||||
C ->> COL: Log
|
||||
COL -->> Q: Forward
|
||||
Q -->> LongNameUser: Final response with confirmation of all actions taken
|
||||
`,
|
||||
{ sequence: { wrap: true } }
|
||||
);
|
||||
});
|
||||
|
||||
describe('Sequence Diagram - New Participant Types with Long Notes and Messages', () => {
|
||||
it('should render long notes left of boundary', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "boundary" }
|
||||
actor Bob
|
||||
Alice->>Bob: Hola
|
||||
Note left of Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
|
||||
Bob->>Alice: I'm short though
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render wrapped long notes left of control', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "control" }
|
||||
actor Bob
|
||||
Alice->>Bob: Hola
|
||||
Note left of Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
|
||||
Bob->>Alice: I'm short though
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render long notes right of entity', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "entity" }
|
||||
actor Bob
|
||||
Alice->>Bob: Hola
|
||||
Note right of Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
|
||||
Bob->>Alice: I'm short though
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render wrapped long notes right of database', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "database" }
|
||||
actor Bob
|
||||
Alice->>Bob: Hola
|
||||
Note right of Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
|
||||
Bob->>Alice: I'm short though
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render long notes over collections', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "collections" }
|
||||
actor Bob
|
||||
Alice->>Bob: Hola
|
||||
Note over Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
|
||||
Bob->>Alice: I'm short though
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render wrapped long notes over queue', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "queue" }
|
||||
actor Bob
|
||||
Alice->>Bob: Hola
|
||||
Note over Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
|
||||
Bob->>Alice: I'm short though
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render notes over actor and boundary', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
actor Alice
|
||||
participant Charlie@{ "type" : "boundary" }
|
||||
note over Alice: Some note
|
||||
note over Charlie: Other note
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render long messages from database to collections', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "database" }
|
||||
participant Bob@{ "type" : "collections" }
|
||||
Alice->>Bob: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
|
||||
Bob->>Alice: I'm short though
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render wrapped long messages from control to entity', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "control" }
|
||||
participant Bob@{ "type" : "entity" }
|
||||
Alice->>Bob:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
|
||||
Bob->>Alice: I'm short though
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render long messages from queue to boundary', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "queue" }
|
||||
participant Bob@{ "type" : "boundary" }
|
||||
Alice->>Bob: I'm short
|
||||
Bob->>Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render wrapped long messages from actor to database', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
actor Alice
|
||||
participant Bob@{ "type" : "database" }
|
||||
Alice->>Bob: I'm short
|
||||
Bob->>Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('svg size', () => {
|
||||
it('should render a sequence diagram when useMaxWidth is true (default)', () => {
|
||||
renderGraph(
|
||||
`
|
||||
sequenceDiagram
|
||||
actor Alice
|
||||
participant Bob@{ "type" : "boundary" }
|
||||
participant John@{ "type" : "control" }
|
||||
Alice ->> Bob: Hello Bob, how are you?
|
||||
Bob-->>John: How about you John?
|
||||
Bob--x Alice: I am good thanks!
|
||||
Bob-x John: I am good thanks!
|
||||
Note right of John: Bob thinks a long<br/>long time, so long<br/>that the text does<br/>not fit on a row.
|
||||
Bob-->Alice: Checking with John...
|
||||
alt either this
|
||||
Alice->>John: Yes
|
||||
else or this
|
||||
Alice->>John: No
|
||||
else or this will happen
|
||||
Alice->John: Maybe
|
||||
end
|
||||
par this happens in parallel
|
||||
Alice -->> Bob: Parallel message 1
|
||||
and
|
||||
Alice -->> John: Parallel message 2
|
||||
end
|
||||
`,
|
||||
{ sequence: { useMaxWidth: true } }
|
||||
);
|
||||
cy.get('svg').should((svg) => {
|
||||
expect(svg).to.have.attr('width', '100%');
|
||||
const style = svg.attr('style');
|
||||
expect(style).to.match(/^max-width: [\d.]+px;$/);
|
||||
const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join(''));
|
||||
expect(maxWidthValue).to.be.within(820 * 0.95, 820 * 1.05);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render a sequence diagram when useMaxWidth is false', () => {
|
||||
renderGraph(
|
||||
`
|
||||
sequenceDiagram
|
||||
actor Alice
|
||||
participant Bob@{ "type" : "boundary" }
|
||||
participant John@{ "type" : "control" }
|
||||
Alice ->> Bob: Hello Bob, how are you?
|
||||
Bob-->>John: How about you John?
|
||||
Bob--x Alice: I am good thanks!
|
||||
Bob-x John: I am good thanks!
|
||||
Note right of John: Bob thinks a long<br/>long time, so long<br/>that the text does<br/>not fit on a row.
|
||||
Bob-->Alice: Checking with John...
|
||||
alt either this
|
||||
Alice->>John: Yes
|
||||
else or this
|
||||
Alice->>John: No
|
||||
else or this will happen
|
||||
Alice->John: Maybe
|
||||
end
|
||||
par this happens in parallel
|
||||
Alice -->> Bob: Parallel message 1
|
||||
and
|
||||
Alice -->> John: Parallel message 2
|
||||
end
|
||||
`,
|
||||
{ sequence: { useMaxWidth: false } }
|
||||
);
|
||||
cy.get('svg').should((svg) => {
|
||||
const width = parseFloat(svg.attr('width'));
|
||||
expect(width).to.be.within(820 * 0.95, 820 * 1.05);
|
||||
expect(svg).to.not.have.attr('style');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -893,6 +893,17 @@ describe('Sequence diagram', () => {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle bidirectional arrows with autonumber', () => {
|
||||
imgSnapshotTest(`
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant A
|
||||
participant B
|
||||
A<<->>B: This is a bidirectional message
|
||||
A->B: This is a normal message`);
|
||||
});
|
||||
|
||||
it('should support actor links and properties when not mirrored EXPERIMENTAL: USE WITH CAUTION', () => {
|
||||
//Be aware that the syntax for "properties" is likely to be changed.
|
||||
imgSnapshotTest(
|
||||
|
@@ -2,219 +2,223 @@
|
||||
"durations": [
|
||||
{
|
||||
"spec": "cypress/integration/other/configuration.spec.js",
|
||||
"duration": 6297
|
||||
"duration": 6162
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/external-diagrams.spec.js",
|
||||
"duration": 2187
|
||||
"duration": 2148
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/ghsa.spec.js",
|
||||
"duration": 3509
|
||||
"duration": 3585
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/iife.spec.js",
|
||||
"duration": 2218
|
||||
"duration": 2099
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/interaction.spec.js",
|
||||
"duration": 12104
|
||||
"duration": 12119
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/rerender.spec.js",
|
||||
"duration": 2151
|
||||
"duration": 2063
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/xss.spec.js",
|
||||
"duration": 33064
|
||||
"duration": 31921
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/appli.spec.js",
|
||||
"duration": 3488
|
||||
"duration": 3385
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/architecture.spec.ts",
|
||||
"duration": 106
|
||||
"duration": 108
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/block.spec.js",
|
||||
"duration": 18317
|
||||
"duration": 18063
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/c4.spec.js",
|
||||
"duration": 5592
|
||||
"duration": 5519
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/classDiagram-elk-v3.spec.js",
|
||||
"duration": 39358
|
||||
"duration": 40040
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/classDiagram-handDrawn-v3.spec.js",
|
||||
"duration": 37160
|
||||
"duration": 38665
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/classDiagram-v2.spec.js",
|
||||
"duration": 23660
|
||||
"duration": 22836
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/classDiagram-v3.spec.js",
|
||||
"duration": 36866
|
||||
"duration": 37096
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/classDiagram.spec.js",
|
||||
"duration": 17334
|
||||
"duration": 16452
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/conf-and-directives.spec.js",
|
||||
"duration": 9871
|
||||
"duration": 10387
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/current.spec.js",
|
||||
"duration": 2833
|
||||
"duration": 2803
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/erDiagram-unified.spec.js",
|
||||
"duration": 85321
|
||||
"duration": 86891
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/erDiagram.spec.js",
|
||||
"duration": 15673
|
||||
"duration": 15206
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/errorDiagram.spec.js",
|
||||
"duration": 3724
|
||||
"duration": 3540
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart-elk.spec.js",
|
||||
"duration": 41178
|
||||
"duration": 41975
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart-handDrawn.spec.js",
|
||||
"duration": 29966
|
||||
"duration": 30909
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart-icon.spec.js",
|
||||
"duration": 7689
|
||||
"duration": 7881
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart-shape-alias.spec.ts",
|
||||
"duration": 24709
|
||||
"duration": 24294
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart-v2.spec.js",
|
||||
"duration": 45565
|
||||
"duration": 47652
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart.spec.js",
|
||||
"duration": 31144
|
||||
"duration": 32049
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/gantt.spec.js",
|
||||
"duration": 20808
|
||||
"duration": 20248
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/gitGraph.spec.js",
|
||||
"duration": 49985
|
||||
"duration": 51202
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/iconShape.spec.ts",
|
||||
"duration": 273272
|
||||
"duration": 283546
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/imageShape.spec.ts",
|
||||
"duration": 55880
|
||||
"duration": 57257
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/info.spec.ts",
|
||||
"duration": 3271
|
||||
"duration": 3352
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/journey.spec.js",
|
||||
"duration": 7293
|
||||
"duration": 7423
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/kanban.spec.ts",
|
||||
"duration": 7861
|
||||
"duration": 7804
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/katex.spec.js",
|
||||
"duration": 3922
|
||||
"duration": 3847
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/marker_unique_id.spec.js",
|
||||
"duration": 2726
|
||||
"duration": 2637
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/mindmap.spec.ts",
|
||||
"duration": 11670
|
||||
"duration": 11658
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/newShapes.spec.ts",
|
||||
"duration": 146020
|
||||
"duration": 149500
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/oldShapes.spec.ts",
|
||||
"duration": 114244
|
||||
"duration": 115427
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/packet.spec.ts",
|
||||
"duration": 5036
|
||||
"duration": 4801
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/pie.spec.ts",
|
||||
"duration": 6545
|
||||
"duration": 6786
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/quadrantChart.spec.js",
|
||||
"duration": 9097
|
||||
"duration": 9422
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/radar.spec.js",
|
||||
"duration": 5676
|
||||
"duration": 5652
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/requirement.spec.js",
|
||||
"duration": 2795
|
||||
"duration": 2787
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/requirementDiagram-unified.spec.js",
|
||||
"duration": 51660
|
||||
"duration": 53631
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/sankey.spec.ts",
|
||||
"duration": 6957
|
||||
"duration": 7075
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/sequencediagram-v2.spec.js",
|
||||
"duration": 20446
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/sequencediagram.spec.js",
|
||||
"duration": 36026
|
||||
"duration": 37326
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/stateDiagram-v2.spec.js",
|
||||
"duration": 29551
|
||||
"duration": 29208
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/stateDiagram.spec.js",
|
||||
"duration": 17364
|
||||
"duration": 16328
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/theme.spec.js",
|
||||
"duration": 30209
|
||||
"duration": 30541
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/timeline.spec.ts",
|
||||
"duration": 8699
|
||||
"duration": 8611
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/treemap.spec.ts",
|
||||
"duration": 12168
|
||||
"duration": 11878
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/xyChart.spec.js",
|
||||
"duration": 21453
|
||||
"duration": 20400
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/zenuml.spec.js",
|
||||
"duration": 3577
|
||||
"duration": 3528
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@
|
||||
|
||||
# Frequently Asked Questions
|
||||
|
||||
1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/556#issuecomment-363182217)
|
||||
1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/1433#issuecomment-1991554712)
|
||||
2. [How to specify custom CSS file?](https://github.com/mermaidjs/mermaid.cli/pull/24#issuecomment-373402785)
|
||||
3. [How to fix tooltip misplacement issue?](https://github.com/mermaid-js/mermaid/issues/542#issuecomment-3343564621)
|
||||
4. [How to specify gantt diagram xAxis format?](https://github.com/mermaid-js/mermaid/issues/269#issuecomment-373229136)
|
||||
|
@@ -19,6 +19,7 @@
|
||||
- [addDirective](functions/addDirective.md)
|
||||
- [getConfig](functions/getConfig.md)
|
||||
- [getSiteConfig](functions/getSiteConfig.md)
|
||||
- [getUserDefinedConfig](functions/getUserDefinedConfig.md)
|
||||
- [reset](functions/reset.md)
|
||||
- [sanitize](functions/sanitize.md)
|
||||
- [saveConfigFromInitialize](functions/saveConfigFromInitialize.md)
|
||||
|
19
docs/config/setup/config/functions/getUserDefinedConfig.md
Normal file
19
docs/config/setup/config/functions/getUserDefinedConfig.md
Normal file
@@ -0,0 +1,19 @@
|
||||
> **Warning**
|
||||
>
|
||||
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||
>
|
||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/setup/config/functions/getUserDefinedConfig.md](../../../../../packages/mermaid/src/docs/config/setup/config/functions/getUserDefinedConfig.md).
|
||||
|
||||
[**mermaid**](../../README.md)
|
||||
|
||||
---
|
||||
|
||||
# Function: getUserDefinedConfig()
|
||||
|
||||
> **getUserDefinedConfig**(): [`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)
|
||||
|
||||
Defined in: [packages/mermaid/src/config.ts:252](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L252)
|
||||
|
||||
## Returns
|
||||
|
||||
[`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)
|
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: ParseOptions
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:72](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L72)
|
||||
Defined in: [packages/mermaid/src/types.ts:84](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L84)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:72](https://github.com/mermaid-js/mer
|
||||
|
||||
> `optional` **suppressErrors**: `boolean`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:77](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L77)
|
||||
Defined in: [packages/mermaid/src/types.ts:89](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L89)
|
||||
|
||||
If `true`, parse will return `false` instead of throwing error when the diagram is invalid.
|
||||
The `parseError` function will not be called.
|
||||
|
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: ParseResult
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:80](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L80)
|
||||
Defined in: [packages/mermaid/src/types.ts:92](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L92)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:80](https://github.com/mermaid-js/mer
|
||||
|
||||
> **config**: [`MermaidConfig`](MermaidConfig.md)
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L88)
|
||||
Defined in: [packages/mermaid/src/types.ts:100](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L100)
|
||||
|
||||
The config passed as YAML frontmatter or directives
|
||||
|
||||
@@ -28,6 +28,6 @@ The config passed as YAML frontmatter or directives
|
||||
|
||||
> **diagramType**: `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:84](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L84)
|
||||
Defined in: [packages/mermaid/src/types.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L96)
|
||||
|
||||
The diagram type, e.g. 'flowchart', 'sequence', etc.
|
||||
|
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: RenderResult
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:98](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L98)
|
||||
Defined in: [packages/mermaid/src/types.ts:110](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L110)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:98](https://github.com/mermaid-js/mer
|
||||
|
||||
> `optional` **bindFunctions**: (`element`) => `void`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:116](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L116)
|
||||
Defined in: [packages/mermaid/src/types.ts:128](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L128)
|
||||
|
||||
Bind function to be called after the svg has been inserted into the DOM.
|
||||
This is necessary for adding event listeners to the elements in the svg.
|
||||
@@ -45,7 +45,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present.
|
||||
|
||||
> **diagramType**: `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:106](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L106)
|
||||
Defined in: [packages/mermaid/src/types.ts:118](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L118)
|
||||
|
||||
The diagram type, e.g. 'flowchart', 'sequence', etc.
|
||||
|
||||
@@ -55,6 +55,6 @@ The diagram type, e.g. 'flowchart', 'sequence', etc.
|
||||
|
||||
> **svg**: `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:102](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L102)
|
||||
Defined in: [packages/mermaid/src/types.ts:114](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L114)
|
||||
|
||||
The svg code for the rendered graph.
|
||||
|
@@ -983,11 +983,23 @@ flowchart TD
|
||||
- `b`
|
||||
- **w**: The width of the image. If not defined, this will default to the natural width of the image.
|
||||
- **h**: The height of the image. If not defined, this will default to the natural height of the image.
|
||||
- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the height (`h`) accordingly to the width (`w`). If not defined, this will default to `off` Possible values are:
|
||||
- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the width (`w`) accordingly to the height (`h`). If not defined, this will default to `off` Possible values are:
|
||||
- `on`
|
||||
- `off`
|
||||
|
||||
These new shapes provide additional flexibility and visual appeal to your flowcharts, making them more informative and engaging.
|
||||
If you want to resize an image, but keep the same aspect ratio, set `h`, and set `constraint: on` to constrain the aspect ratio. E.g.
|
||||
|
||||
```mermaid-example
|
||||
flowchart TD
|
||||
%% My image with a constrained aspect ratio
|
||||
A@{ img: "https://mermaid.js.org/favicon.svg", label: "My example image label", pos: "t", h: 60, constraint: "on" }
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
%% My image with a constrained aspect ratio
|
||||
A@{ img: "https://mermaid.js.org/favicon.svg", label: "My example image label", pos: "t", h: 60, constraint: "on" }
|
||||
```
|
||||
|
||||
## Links between nodes
|
||||
|
||||
|
@@ -74,6 +74,126 @@ sequenceDiagram
|
||||
Bob->>Alice: Hi Alice
|
||||
```
|
||||
|
||||
### Boundary
|
||||
|
||||
If you want to use the boundary symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "boundary" }
|
||||
participant Bob
|
||||
Alice->>Bob: Request from boundary
|
||||
Bob->>Alice: Response to boundary
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "boundary" }
|
||||
participant Bob
|
||||
Alice->>Bob: Request from boundary
|
||||
Bob->>Alice: Response to boundary
|
||||
```
|
||||
|
||||
### Control
|
||||
|
||||
If you want to use the control symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "control" }
|
||||
participant Bob
|
||||
Alice->>Bob: Control request
|
||||
Bob->>Alice: Control response
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "control" }
|
||||
participant Bob
|
||||
Alice->>Bob: Control request
|
||||
Bob->>Alice: Control response
|
||||
```
|
||||
|
||||
### Entity
|
||||
|
||||
If you want to use the entity symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "entity" }
|
||||
participant Bob
|
||||
Alice->>Bob: Entity request
|
||||
Bob->>Alice: Entity response
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "entity" }
|
||||
participant Bob
|
||||
Alice->>Bob: Entity request
|
||||
Bob->>Alice: Entity response
|
||||
```
|
||||
|
||||
### Database
|
||||
|
||||
If you want to use the database symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "database" }
|
||||
participant Bob
|
||||
Alice->>Bob: DB query
|
||||
Bob->>Alice: DB result
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "database" }
|
||||
participant Bob
|
||||
Alice->>Bob: DB query
|
||||
Bob->>Alice: DB result
|
||||
```
|
||||
|
||||
### Collections
|
||||
|
||||
If you want to use the collections symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "collections" }
|
||||
participant Bob
|
||||
Alice->>Bob: Collections request
|
||||
Bob->>Alice: Collections response
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "collections" }
|
||||
participant Bob
|
||||
Alice->>Bob: Collections request
|
||||
Bob->>Alice: Collections response
|
||||
```
|
||||
|
||||
### Queue
|
||||
|
||||
If you want to use the queue symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "queue" }
|
||||
participant Bob
|
||||
Alice->>Bob: Queue message
|
||||
Bob->>Alice: Queue response
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "queue" }
|
||||
participant Bob
|
||||
Alice->>Bob: Queue message
|
||||
Bob->>Alice: Queue response
|
||||
```
|
||||
|
||||
### Aliases
|
||||
|
||||
The actor can have a convenient identifier and a descriptive label.
|
||||
|
@@ -31,7 +31,7 @@
|
||||
"lint:fix": "eslint --cache --cache-strategy content --fix . && prettier --write . && tsx scripts/fixCSpell.ts",
|
||||
"lint:jison": "tsx ./scripts/jison/lint.mts",
|
||||
"contributors": "tsx scripts/updateContributors.ts",
|
||||
"cypress": "cypress run --spec ./cypress/integration/rendering/appli.spec.js",
|
||||
"cypress": "cypress run",
|
||||
"cypress:open": "cypress open",
|
||||
"e2e": "start-server-and-test dev http://localhost:9000/ cypress",
|
||||
"e2e:coverage": "start-server-and-test dev:coverage http://localhost:9000/ cypress",
|
||||
|
@@ -68,7 +68,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^7.0.4",
|
||||
"@iconify/utils": "^2.1.33",
|
||||
"@iconify/utils": "^3.0.1",
|
||||
"@mermaid-js/parser": "workspace:^",
|
||||
"@types/d3": "^7.4.3",
|
||||
"cytoscape": "^3.29.3",
|
||||
|
@@ -78,3 +78,187 @@ describe('when working with site config', () => {
|
||||
expect(config_4.altFontFamily).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserDefinedConfig', () => {
|
||||
beforeEach(() => {
|
||||
configApi.reset();
|
||||
});
|
||||
|
||||
it('should return empty object when no user config is defined', () => {
|
||||
const userConfig = configApi.getUserDefinedConfig();
|
||||
expect(userConfig).toEqual({});
|
||||
});
|
||||
|
||||
it('should return config from initialize only', () => {
|
||||
const initConfig: MermaidConfig = { theme: 'dark', fontFamily: 'Arial' };
|
||||
configApi.saveConfigFromInitialize(initConfig);
|
||||
|
||||
const userConfig = configApi.getUserDefinedConfig();
|
||||
expect(userConfig).toEqual(initConfig);
|
||||
});
|
||||
|
||||
it('should return config from directives only', () => {
|
||||
const directive1: MermaidConfig = { layout: 'elk', fontSize: 14 };
|
||||
const directive2: MermaidConfig = { theme: 'forest' };
|
||||
|
||||
configApi.addDirective(directive1);
|
||||
configApi.addDirective(directive2);
|
||||
|
||||
expect(configApi.getUserDefinedConfig()).toMatchInlineSnapshot(`
|
||||
{
|
||||
"fontFamily": "Arial",
|
||||
"fontSize": 14,
|
||||
"layout": "elk",
|
||||
"theme": "forest",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should combine initialize config and directives', () => {
|
||||
const initConfig: MermaidConfig = { theme: 'dark', fontFamily: 'Arial', layout: 'dagre' };
|
||||
const directive1: MermaidConfig = { layout: 'elk', fontSize: 14 };
|
||||
const directive2: MermaidConfig = { theme: 'forest' };
|
||||
|
||||
configApi.saveConfigFromInitialize(initConfig);
|
||||
configApi.addDirective(directive1);
|
||||
configApi.addDirective(directive2);
|
||||
|
||||
const userConfig = configApi.getUserDefinedConfig();
|
||||
expect(userConfig).toMatchInlineSnapshot(`
|
||||
{
|
||||
"fontFamily": "Arial",
|
||||
"fontSize": 14,
|
||||
"layout": "elk",
|
||||
"theme": "forest",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle nested config objects properly', () => {
|
||||
const initConfig: MermaidConfig = {
|
||||
flowchart: { nodeSpacing: 50, rankSpacing: 100 },
|
||||
theme: 'default',
|
||||
};
|
||||
const directive: MermaidConfig = {
|
||||
flowchart: { nodeSpacing: 75, curve: 'basis' },
|
||||
mindmap: { padding: 20 },
|
||||
};
|
||||
|
||||
configApi.saveConfigFromInitialize(initConfig);
|
||||
configApi.addDirective(directive);
|
||||
|
||||
const userConfig = configApi.getUserDefinedConfig();
|
||||
expect(userConfig).toMatchInlineSnapshot(`
|
||||
{
|
||||
"flowchart": {
|
||||
"curve": "basis",
|
||||
"nodeSpacing": 75,
|
||||
"rankSpacing": 100,
|
||||
},
|
||||
"mindmap": {
|
||||
"padding": 20,
|
||||
},
|
||||
"theme": "default",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle complex nested overrides', () => {
|
||||
const initConfig: MermaidConfig = {
|
||||
flowchart: {
|
||||
nodeSpacing: 50,
|
||||
rankSpacing: 100,
|
||||
curve: 'linear',
|
||||
},
|
||||
theme: 'default',
|
||||
};
|
||||
const directive1: MermaidConfig = {
|
||||
flowchart: {
|
||||
nodeSpacing: 75,
|
||||
},
|
||||
fontSize: 12,
|
||||
};
|
||||
const directive2: MermaidConfig = {
|
||||
flowchart: {
|
||||
curve: 'basis',
|
||||
nodeSpacing: 100,
|
||||
},
|
||||
mindmap: {
|
||||
padding: 15,
|
||||
},
|
||||
};
|
||||
|
||||
configApi.saveConfigFromInitialize(initConfig);
|
||||
configApi.addDirective(directive1);
|
||||
configApi.addDirective(directive2);
|
||||
|
||||
const userConfig = configApi.getUserDefinedConfig();
|
||||
expect(userConfig).toMatchInlineSnapshot(`
|
||||
{
|
||||
"flowchart": {
|
||||
"curve": "basis",
|
||||
"nodeSpacing": 100,
|
||||
"rankSpacing": 100,
|
||||
},
|
||||
"fontSize": 12,
|
||||
"mindmap": {
|
||||
"padding": 15,
|
||||
},
|
||||
"theme": "default",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should return independent copies (not references)', () => {
|
||||
const initConfig: MermaidConfig = { theme: 'dark', flowchart: { nodeSpacing: 50 } };
|
||||
configApi.saveConfigFromInitialize(initConfig);
|
||||
|
||||
const userConfig1 = configApi.getUserDefinedConfig();
|
||||
const userConfig2 = configApi.getUserDefinedConfig();
|
||||
|
||||
userConfig1.theme = 'neutral';
|
||||
userConfig1.flowchart!.nodeSpacing = 999;
|
||||
|
||||
expect(userConfig2).toMatchInlineSnapshot(`
|
||||
{
|
||||
"flowchart": {
|
||||
"nodeSpacing": 50,
|
||||
},
|
||||
"theme": "dark",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle edge cases with undefined values', () => {
|
||||
const initConfig: MermaidConfig = { theme: 'dark', layout: undefined };
|
||||
const directive: MermaidConfig = { fontSize: 14, fontFamily: undefined };
|
||||
|
||||
configApi.saveConfigFromInitialize(initConfig);
|
||||
configApi.addDirective(directive);
|
||||
|
||||
expect(configApi.getUserDefinedConfig()).toMatchInlineSnapshot(`
|
||||
{
|
||||
"fontSize": 14,
|
||||
"layout": undefined,
|
||||
"theme": "dark",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should retain config from initialize after reset', () => {
|
||||
const initConfig: MermaidConfig = { theme: 'dark' };
|
||||
const directive: MermaidConfig = { layout: 'elk' };
|
||||
|
||||
configApi.saveConfigFromInitialize(initConfig);
|
||||
configApi.addDirective(directive);
|
||||
|
||||
expect(configApi.getUserDefinedConfig()).toMatchInlineSnapshot(`
|
||||
{
|
||||
"layout": "elk",
|
||||
"theme": "dark",
|
||||
}
|
||||
`);
|
||||
|
||||
configApi.reset();
|
||||
});
|
||||
});
|
||||
|
@@ -248,3 +248,17 @@ const checkConfig = (config: MermaidConfig) => {
|
||||
issueWarning('LAZY_LOAD_DEPRECATED');
|
||||
}
|
||||
};
|
||||
|
||||
export const getUserDefinedConfig = (): MermaidConfig => {
|
||||
let userConfig: MermaidConfig = {};
|
||||
|
||||
if (configFromInitialize) {
|
||||
userConfig = assignWithDepth(userConfig, configFromInitialize);
|
||||
}
|
||||
|
||||
for (const d of directives) {
|
||||
userConfig = assignWithDepth(userConfig, d);
|
||||
}
|
||||
|
||||
return userConfig;
|
||||
};
|
||||
|
@@ -28,6 +28,7 @@ import architecture from '../diagrams/architecture/architectureDetector.js';
|
||||
import { registerLazyLoadedDiagrams } from './detectType.js';
|
||||
import { registerDiagram } from './diagramAPI.js';
|
||||
import { treemap } from '../diagrams/treemap/detector.js';
|
||||
import usecase from '../diagrams/useCase/useCaseDetector.js';
|
||||
import '../type.d.ts';
|
||||
|
||||
let hasLoadedDiagrams = false;
|
||||
@@ -101,6 +102,7 @@ export const addDiagrams = () => {
|
||||
xychart,
|
||||
block,
|
||||
radar,
|
||||
treemap
|
||||
treemap,
|
||||
usecase
|
||||
);
|
||||
};
|
||||
|
@@ -14,7 +14,7 @@
|
||||
|
||||
// Special states for recognizing aliases
|
||||
// A special state for grabbing text up to the first comment/newline
|
||||
%x ID ALIAS LINE
|
||||
%x ID ALIAS LINE CONFIG CONFIG_DATA
|
||||
|
||||
%x acc_title
|
||||
%x acc_descr
|
||||
@@ -28,6 +28,11 @@
|
||||
\%%(?!\{)[^\n]* /* skip comments */
|
||||
[^\}]\%\%[^\n]* /* skip comments */
|
||||
[0-9]+(?=[ \n]+) return 'NUM';
|
||||
<ID>\@\{ { this.begin('CONFIG'); return 'CONFIG_START'; }
|
||||
<CONFIG>[^\}]+ { return 'CONFIG_CONTENT'; }
|
||||
<CONFIG>\} { this.popState(); this.popState(); return 'CONFIG_END'; }
|
||||
<ID>[^\<->\->:\n,;@\s]+(?=\@\{) { yytext = yytext.trim(); return 'ACTOR'; }
|
||||
<ID>[^\<->\->:\n,;@]+?([\-]*[^\<->\->:\n,;@]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
|
||||
"box" { this.begin('LINE'); return 'box'; }
|
||||
"participant" { this.begin('ID'); return 'participant'; }
|
||||
"actor" { this.begin('ID'); return 'participant_actor'; }
|
||||
@@ -231,6 +236,8 @@ participant_statement
|
||||
| 'participant_actor' actor 'AS' restOfLine 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
|
||||
| 'participant_actor' actor 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant'; $$=$2;}
|
||||
| 'destroy' actor 'NEWLINE' {$2.type='destroyParticipant'; $$=$2;}
|
||||
| 'participant' actor_with_config 'NEWLINE' {$2.draw='participant'; $2.type='addParticipant'; $$=$2;}
|
||||
|
||||
;
|
||||
|
||||
note_statement
|
||||
@@ -301,6 +308,23 @@ signal
|
||||
{ $$ = [$1,$3,{type: 'addMessage', from:$1.actor, to:$3.actor, signalType:$2, msg:$4}]}
|
||||
;
|
||||
|
||||
actor_with_config
|
||||
: ACTOR config_object
|
||||
{
|
||||
$$ = {
|
||||
type: 'addParticipant',
|
||||
actor: $1,
|
||||
config: $2
|
||||
};
|
||||
}
|
||||
;
|
||||
|
||||
config_object
|
||||
: CONFIG_START CONFIG_CONTENT CONFIG_END
|
||||
{
|
||||
$$ = $2.trim();
|
||||
}
|
||||
;
|
||||
// actor
|
||||
// : actor_participant
|
||||
// | actor_actor
|
||||
@@ -313,7 +337,7 @@ signaltype
|
||||
: SOLID_OPEN_ARROW { $$ = yy.LINETYPE.SOLID_OPEN; }
|
||||
| DOTTED_OPEN_ARROW { $$ = yy.LINETYPE.DOTTED_OPEN; }
|
||||
| SOLID_ARROW { $$ = yy.LINETYPE.SOLID; }
|
||||
| BIDIRECTIONAL_SOLID_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_SOLID; }
|
||||
| BIDIRECTIONAL_SOLID_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_SOLID; }
|
||||
| DOTTED_ARROW { $$ = yy.LINETYPE.DOTTED; }
|
||||
| BIDIRECTIONAL_DOTTED_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_DOTTED; }
|
||||
| SOLID_CROSS { $$ = yy.LINETYPE.SOLID_CROSS; }
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import * as yaml from 'js-yaml';
|
||||
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { ImperativeState } from '../../utils/imperativeState.js';
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
setDiagramTitle,
|
||||
} from '../common/commonDb.js';
|
||||
import type { Actor, AddMessageParams, Box, Message, Note } from './types.js';
|
||||
import type { ParticipantMetaData } from '../../types.js';
|
||||
|
||||
interface SequenceState {
|
||||
prevActor?: string;
|
||||
@@ -75,6 +77,17 @@ const PLACEMENT = {
|
||||
OVER: 2,
|
||||
} as const;
|
||||
|
||||
export const PARTICIPANT_TYPE = {
|
||||
ACTOR: 'actor',
|
||||
BOUNDARY: 'boundary',
|
||||
COLLECTIONS: 'collections',
|
||||
CONTROL: 'control',
|
||||
DATABASE: 'database',
|
||||
ENTITY: 'entity',
|
||||
PARTICIPANT: 'participant',
|
||||
QUEUE: 'queue',
|
||||
} as const;
|
||||
|
||||
export class SequenceDB implements DiagramDB {
|
||||
private readonly state = new ImperativeState<SequenceState>(() => ({
|
||||
prevActor: undefined,
|
||||
@@ -119,9 +132,22 @@ export class SequenceDB implements DiagramDB {
|
||||
id: string,
|
||||
name: string,
|
||||
description: { text: string; wrap?: boolean | null; type: string },
|
||||
type: string
|
||||
type: string,
|
||||
metadata?: any
|
||||
) {
|
||||
let assignedBox = this.state.records.currentBox;
|
||||
let doc;
|
||||
if (metadata !== undefined) {
|
||||
let yamlData;
|
||||
// detect if shapeData contains a newline character
|
||||
if (!metadata.includes('\n')) {
|
||||
yamlData = '{\n' + metadata + '\n}';
|
||||
} else {
|
||||
yamlData = metadata + '\n';
|
||||
}
|
||||
doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA }) as ParticipantMetaData;
|
||||
}
|
||||
type = doc?.type ?? type;
|
||||
const old = this.state.records.actors.get(id);
|
||||
if (old) {
|
||||
// If already set and trying to set to a new one throw error
|
||||
@@ -518,7 +544,7 @@ export class SequenceDB implements DiagramDB {
|
||||
});
|
||||
break;
|
||||
case 'addParticipant':
|
||||
this.addActor(param.actor, param.actor, param.description, param.draw);
|
||||
this.addActor(param.actor, param.actor, param.description, param.draw, param.config);
|
||||
break;
|
||||
case 'createParticipant':
|
||||
if (this.state.records.actors.has(param.actor)) {
|
||||
@@ -527,7 +553,7 @@ export class SequenceDB implements DiagramDB {
|
||||
);
|
||||
}
|
||||
this.state.records.lastCreated = param.actor;
|
||||
this.addActor(param.actor, param.actor, param.description, param.draw);
|
||||
this.addActor(param.actor, param.actor, param.description, param.draw, param.config);
|
||||
this.state.records.createdActors.set(param.actor, this.state.records.messages.length);
|
||||
break;
|
||||
case 'destroyParticipant':
|
||||
|
@@ -2058,4 +2058,272 @@ Bob->>Alice:Got it!
|
||||
expect(messages[0].from).toBe('Alice');
|
||||
expect(messages[0].to).toBe('Bob');
|
||||
});
|
||||
describe('when parsing extended participant syntax', () => {
|
||||
it('should parse participants with different quote styles and whitespace', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "database" }
|
||||
participant Bob@{ "type" : "database" }
|
||||
participant Carl@{ type: "database" }
|
||||
participant David@{ "type" : 'database' }
|
||||
participant Eve@{ type: 'database' }
|
||||
participant Favela@{ "type" : "database" }
|
||||
Bob->>+Alice: Hi Alice
|
||||
Alice->>+Bob: Hi Bob
|
||||
`);
|
||||
|
||||
const actors = diagram.db.getActors();
|
||||
|
||||
expect(actors.get('Alice').type).toBe('database');
|
||||
expect(actors.get('Alice').description).toBe('Alice');
|
||||
|
||||
expect(actors.get('Bob').type).toBe('database');
|
||||
expect(actors.get('Bob').description).toBe('Bob');
|
||||
|
||||
expect(actors.get('Carl').type).toBe('database');
|
||||
expect(actors.get('Carl').description).toBe('Carl');
|
||||
|
||||
expect(actors.get('David').type).toBe('database');
|
||||
expect(actors.get('David').description).toBe('David');
|
||||
|
||||
expect(actors.get('Eve').type).toBe('database');
|
||||
expect(actors.get('Eve').description).toBe('Eve');
|
||||
|
||||
expect(actors.get('Favela').type).toBe('database');
|
||||
expect(actors.get('Favela').description).toBe('Favela');
|
||||
|
||||
// Verify messages were parsed correctly
|
||||
const messages = diagram.db.getMessages();
|
||||
expect(messages.length).toBe(4); // 2 messages + 2 activation messages
|
||||
expect(messages[0].from).toBe('Bob');
|
||||
expect(messages[0].to).toBe('Alice');
|
||||
expect(messages[0].message).toBe('Hi Alice');
|
||||
expect(messages[2].from).toBe('Alice'); // Second message (index 2 due to activation)
|
||||
expect(messages[2].to).toBe('Bob');
|
||||
expect(messages[2].message).toBe('Hi Bob');
|
||||
});
|
||||
|
||||
it('should parse mixed participant types with extended syntax', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant lead
|
||||
participant dsa@{ "type" : "queue" }
|
||||
API->>+Database: getUserb
|
||||
Database-->>-API: userb
|
||||
dsa --> Database: hello
|
||||
`);
|
||||
|
||||
// Verify actors were created
|
||||
const actors = diagram.db.getActors();
|
||||
|
||||
expect(actors.get('lead').type).toBe('participant');
|
||||
expect(actors.get('lead').description).toBe('lead');
|
||||
|
||||
// Participant with extended syntax
|
||||
expect(actors.get('dsa').type).toBe('queue');
|
||||
expect(actors.get('dsa').description).toBe('dsa');
|
||||
|
||||
// Implicitly created actors (from messages)
|
||||
expect(actors.get('API').type).toBe('participant');
|
||||
expect(actors.get('API').description).toBe('API');
|
||||
|
||||
expect(actors.get('Database').type).toBe('participant');
|
||||
expect(actors.get('Database').description).toBe('Database');
|
||||
|
||||
// Verify messages were parsed correctly
|
||||
const messages = diagram.db.getMessages();
|
||||
expect(messages.length).toBe(5); // 3 messages + 2 activation messages
|
||||
|
||||
// First message with activation
|
||||
expect(messages[0].from).toBe('API');
|
||||
expect(messages[0].to).toBe('Database');
|
||||
expect(messages[0].message).toBe('getUserb');
|
||||
expect(messages[0].activate).toBe(true);
|
||||
|
||||
// Second message with deactivation
|
||||
expect(messages[2].from).toBe('Database');
|
||||
expect(messages[2].to).toBe('API');
|
||||
expect(messages[2].message).toBe('userb');
|
||||
|
||||
// Third message
|
||||
expect(messages[4].from).toBe('dsa');
|
||||
expect(messages[4].to).toBe('Database');
|
||||
expect(messages[4].message).toBe('hello');
|
||||
});
|
||||
|
||||
it('should fail for malformed JSON in participant definition', async () => {
|
||||
const invalidDiagram = `
|
||||
sequenceDiagram
|
||||
participant D@{ "type: "entity" }
|
||||
participant E@{ "type": "dat
|
||||
abase }
|
||||
`;
|
||||
|
||||
let error = false;
|
||||
try {
|
||||
await mermaidAPI.parse(invalidDiagram);
|
||||
} catch (e) {
|
||||
error = true;
|
||||
}
|
||||
expect(error).toBe(true);
|
||||
});
|
||||
|
||||
it('should fail for missing colon separator', async () => {
|
||||
const invalidDiagram = `
|
||||
sequenceDiagram
|
||||
participant C@{ "type" "control" }
|
||||
C ->> C: action
|
||||
`;
|
||||
|
||||
let error = false;
|
||||
try {
|
||||
await mermaidAPI.parse(invalidDiagram);
|
||||
} catch (e) {
|
||||
error = true;
|
||||
}
|
||||
expect(error).toBe(true);
|
||||
});
|
||||
|
||||
it('should fail for missing closing brace', async () => {
|
||||
const invalidDiagram = `
|
||||
sequenceDiagram
|
||||
participant E@{ "type": "entity"
|
||||
E ->> E: process
|
||||
`;
|
||||
|
||||
let error = false;
|
||||
try {
|
||||
await mermaidAPI.parse(invalidDiagram);
|
||||
} catch (e) {
|
||||
error = true;
|
||||
}
|
||||
expect(error).toBe(true);
|
||||
});
|
||||
});
|
||||
describe('participant type parsing', () => {
|
||||
it('should parse boundary participant', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant boundary@{ "type" : "boundary" }
|
||||
boundary->boundary: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('boundary').type).toBe('boundary');
|
||||
expect(actors.get('boundary').description).toBe('boundary');
|
||||
});
|
||||
|
||||
it('should parse control participant', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant C@{ "type" : "control" }
|
||||
C->C: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('C').type).toBe('control');
|
||||
expect(actors.get('C').description).toBe('C');
|
||||
});
|
||||
|
||||
it('should parse entity participant', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant E@{ "type" : "entity" }
|
||||
E->E: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('E').type).toBe('entity');
|
||||
expect(actors.get('E').description).toBe('E');
|
||||
});
|
||||
|
||||
it('should parse database participant', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant D@{ "type" : "database" }
|
||||
D->D: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('D').type).toBe('database');
|
||||
expect(actors.get('D').description).toBe('D');
|
||||
});
|
||||
|
||||
it('should parse collections participant', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant L@{ "type" : "collections" }
|
||||
L->L: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('L').type).toBe('collections');
|
||||
expect(actors.get('L').description).toBe('L');
|
||||
});
|
||||
|
||||
it('should parse queue participant', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant Q@{ "type" : "queue" }
|
||||
Q->Q: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('Q').type).toBe('queue');
|
||||
expect(actors.get('Q').description).toBe('Q');
|
||||
});
|
||||
});
|
||||
|
||||
describe('participant type parsing', () => {
|
||||
it('should parse actor participant', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant A@{ "type" : "queue" }
|
||||
A->A: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('A').type).toBe('queue');
|
||||
expect(actors.get('A').description).toBe('A');
|
||||
});
|
||||
|
||||
it('should parse participant participant', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant P@{ "type" : "database" }
|
||||
P->P: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('P').type).toBe('database');
|
||||
expect(actors.get('P').description).toBe('P');
|
||||
});
|
||||
|
||||
it('should parse boundary using actor keyword', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "collections" }
|
||||
participant Bob@{ "type" : "control" }
|
||||
Alice->>Bob: Hello Bob, how are you?
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('Alice').type).toBe('collections');
|
||||
expect(actors.get('Bob').type).toBe('control');
|
||||
expect(actors.get('Bob').description).toBe('Bob');
|
||||
});
|
||||
|
||||
it('should parse control using participant keyword', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant C@{ "type" : "control" }
|
||||
C->C: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('C').type).toBe('control');
|
||||
expect(actors.get('C').description).toBe('C');
|
||||
});
|
||||
|
||||
it('should parse entity using actor keyword', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant E@{ "type" : "entity" }
|
||||
E->E: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('E').type).toBe('entity');
|
||||
expect(actors.get('E').description).toBe('E');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -10,6 +10,7 @@ import assignWithDepth from '../../assignWithDepth.js';
|
||||
import utils from '../../utils.js';
|
||||
import { configureSvgSize } from '../../setupGraphViewbox.js';
|
||||
import type { Diagram } from '../../Diagram.js';
|
||||
import { PARTICIPANT_TYPE } from './sequenceDb.js';
|
||||
|
||||
let conf = {};
|
||||
|
||||
@@ -476,7 +477,29 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO
|
||||
|
||||
// add node number
|
||||
if (sequenceVisible || conf.showSequenceNumbers) {
|
||||
line.attr('marker-start', 'url(' + url + '#sequencenumber)');
|
||||
const isBidirectional =
|
||||
type === diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID ||
|
||||
type === diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED;
|
||||
|
||||
if (isBidirectional) {
|
||||
const SEQUENCE_NUMBER_RADIUS = 6;
|
||||
|
||||
if (startx < stopx) {
|
||||
line.attr('x1', startx + 2 * SEQUENCE_NUMBER_RADIUS);
|
||||
} else {
|
||||
line.attr('x1', startx + SEQUENCE_NUMBER_RADIUS);
|
||||
}
|
||||
}
|
||||
|
||||
diagram
|
||||
.append('line')
|
||||
.attr('x1', startx)
|
||||
.attr('y1', lineStartY)
|
||||
.attr('x2', startx)
|
||||
.attr('y2', lineStartY)
|
||||
.attr('stroke-width', 0)
|
||||
.attr('marker-start', 'url(' + url + '#sequencenumber)');
|
||||
|
||||
diagram
|
||||
.append('text')
|
||||
.attr('x', startx)
|
||||
@@ -724,11 +747,19 @@ function adjustCreatedDestroyedData(
|
||||
msgModel.startx = msgModel.startx - adjustment;
|
||||
}
|
||||
}
|
||||
const actorArray = [
|
||||
PARTICIPANT_TYPE.ACTOR,
|
||||
PARTICIPANT_TYPE.CONTROL,
|
||||
PARTICIPANT_TYPE.ENTITY,
|
||||
PARTICIPANT_TYPE.DATABASE,
|
||||
];
|
||||
|
||||
// if it is a create message
|
||||
if (createdActors.get(msg.to) == index) {
|
||||
const actor = actors.get(msg.to);
|
||||
const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 + 3 : actor.width / 2 + 3;
|
||||
const adjustment = actorArray.includes(actor.type)
|
||||
? ACTOR_TYPE_WIDTH / 2 + 3
|
||||
: actor.width / 2 + 3;
|
||||
receiverAdjustment(actor, adjustment);
|
||||
actor.starty = lineStartY - actor.height / 2;
|
||||
bounds.bumpVerticalPos(actor.height / 2);
|
||||
@@ -737,7 +768,7 @@ function adjustCreatedDestroyedData(
|
||||
else if (destroyedActors.get(msg.from) == index) {
|
||||
const actor = actors.get(msg.from);
|
||||
if (conf.mirrorActors) {
|
||||
const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 : actor.width / 2;
|
||||
const adjustment = actorArray.includes(actor.type) ? ACTOR_TYPE_WIDTH / 2 : actor.width / 2;
|
||||
senderAdjustment(actor, adjustment);
|
||||
}
|
||||
actor.stopy = lineStartY - actor.height / 2;
|
||||
@@ -747,7 +778,9 @@ function adjustCreatedDestroyedData(
|
||||
else if (destroyedActors.get(msg.to) == index) {
|
||||
const actor = actors.get(msg.to);
|
||||
if (conf.mirrorActors) {
|
||||
const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 + 3 : actor.width / 2 + 3;
|
||||
const adjustment = actorArray.includes(actor.type)
|
||||
? ACTOR_TYPE_WIDTH / 2 + 3
|
||||
: actor.width / 2 + 3;
|
||||
receiverAdjustment(actor, adjustment);
|
||||
}
|
||||
actor.stopy = lineStartY - actor.height / 2;
|
||||
@@ -1065,10 +1098,11 @@ export const draw = async function (_text: string, id: string, _version: string,
|
||||
for (const box of bounds.models.boxes) {
|
||||
box.height = bounds.getVerticalPos() - box.y;
|
||||
bounds.insert(box.x, box.y, box.x + box.width, box.height);
|
||||
box.startx = box.x;
|
||||
box.starty = box.y;
|
||||
box.stopx = box.startx + box.width;
|
||||
box.stopy = box.starty + box.height;
|
||||
const boxPadding = conf.boxMargin * 2;
|
||||
box.startx = box.x - boxPadding;
|
||||
box.starty = box.y - boxPadding * 0.25;
|
||||
box.stopx = box.startx + box.width + 2 * boxPadding;
|
||||
box.stopy = box.starty + box.height + boxPadding * 0.75;
|
||||
box.stroke = 'rgb(0,0,0, 0.5)';
|
||||
svgDraw.drawBox(diagram, box, conf);
|
||||
}
|
||||
@@ -1333,6 +1367,9 @@ async function calculateActorMargins(
|
||||
return (total += actors.get(aKey).width + (actors.get(aKey).margin || 0));
|
||||
}, 0);
|
||||
|
||||
const standardBoxPadding = conf.boxMargin * 8;
|
||||
totalWidth += standardBoxPadding;
|
||||
|
||||
totalWidth -= 2 * conf.boxTextMargin;
|
||||
if (box.wrap) {
|
||||
box.name = utils.wrapLabel(box.name, totalWidth - 2 * conf.wrapPadding, textFont);
|
||||
|
@@ -12,6 +12,11 @@ const getStyles = (options) =>
|
||||
.actor-line {
|
||||
stroke: ${options.actorLineColor};
|
||||
}
|
||||
|
||||
.innerArc {
|
||||
stroke-width: 1.5;
|
||||
stroke-dasharray: none;
|
||||
}
|
||||
|
||||
.messageLine0 {
|
||||
stroke-width: 1.5;
|
||||
@@ -115,6 +120,7 @@ const getStyles = (options) =>
|
||||
fill: ${options.actorBkg};
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
export default getStyles;
|
||||
|
@@ -415,6 +415,600 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) {
|
||||
return height;
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws an actor in the diagram with the attached line
|
||||
*
|
||||
* @param {any} elem - The diagram we'll draw to.
|
||||
* @param {any} actor - The actor to draw.
|
||||
* @param {any} conf - DrawText implementation discriminator object
|
||||
* @param {boolean} isFooter - If the actor is the footer one
|
||||
*/
|
||||
const drawActorTypeCollections = function (elem, actor, conf, isFooter) {
|
||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
||||
const center = actor.x + actor.width / 2;
|
||||
const centerY = actorY + actor.height;
|
||||
|
||||
const boxplusLineGroup = elem.append('g').lower();
|
||||
var g = boxplusLineGroup;
|
||||
|
||||
if (!isFooter) {
|
||||
actorCnt++;
|
||||
if (Object.keys(actor.links || {}).length && !conf.forceMenus) {
|
||||
g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer');
|
||||
}
|
||||
g.append('line')
|
||||
.attr('id', 'actor' + actorCnt)
|
||||
.attr('x1', center)
|
||||
.attr('y1', centerY)
|
||||
.attr('x2', center)
|
||||
.attr('y2', 2000)
|
||||
.attr('class', 'actor-line 200')
|
||||
.attr('stroke-width', '0.5px')
|
||||
.attr('stroke', '#999')
|
||||
.attr('name', actor.name);
|
||||
|
||||
g = boxplusLineGroup.append('g');
|
||||
actor.actorCnt = actorCnt;
|
||||
|
||||
if (actor.links != null) {
|
||||
g.attr('id', 'root-' + actorCnt);
|
||||
}
|
||||
}
|
||||
|
||||
const rect = svgDrawCommon.getNoteRect();
|
||||
var cssclass = 'actor';
|
||||
if (actor.properties?.class) {
|
||||
cssclass = actor.properties.class;
|
||||
} else {
|
||||
rect.fill = '#eaeaea';
|
||||
}
|
||||
if (isFooter) {
|
||||
cssclass += ` ${BOTTOM_ACTOR_CLASS}`;
|
||||
} else {
|
||||
cssclass += ` ${TOP_ACTOR_CLASS}`;
|
||||
}
|
||||
rect.x = actor.x;
|
||||
rect.y = actorY;
|
||||
rect.width = actor.width;
|
||||
rect.height = actor.height;
|
||||
rect.class = cssclass;
|
||||
rect.name = actor.name;
|
||||
|
||||
// DRAW STACKED RECTANGLES
|
||||
const offset = 6;
|
||||
const shadowRect = {
|
||||
...rect,
|
||||
x: rect.x + (isFooter ? -offset : -offset),
|
||||
y: rect.y + (isFooter ? +offset : +offset),
|
||||
class: 'actor',
|
||||
};
|
||||
const rectElem = drawRect(g, rect); // draw main rectangle on top
|
||||
drawRect(g, shadowRect);
|
||||
actor.rectData = rect;
|
||||
|
||||
if (actor.properties?.icon) {
|
||||
const iconSrc = actor.properties.icon.trim();
|
||||
if (iconSrc.charAt(0) === '@') {
|
||||
svgDrawCommon.drawEmbeddedImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc.substr(1));
|
||||
} else {
|
||||
svgDrawCommon.drawImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc);
|
||||
}
|
||||
}
|
||||
|
||||
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
|
||||
actor.description,
|
||||
g,
|
||||
rect.x - offset,
|
||||
rect.y + offset,
|
||||
rect.width,
|
||||
rect.height,
|
||||
{ class: `actor ${ACTOR_BOX_CLASS}` },
|
||||
conf
|
||||
);
|
||||
|
||||
let height = actor.height;
|
||||
if (rectElem.node) {
|
||||
const bounds = rectElem.node().getBBox();
|
||||
actor.height = bounds.height;
|
||||
height = bounds.height;
|
||||
}
|
||||
|
||||
return height;
|
||||
};
|
||||
|
||||
const drawActorTypeQueue = function (elem, actor, conf, isFooter) {
|
||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
||||
const center = actor.x + actor.width / 2;
|
||||
const centerY = actorY + actor.height;
|
||||
|
||||
const boxplusLineGroup = elem.append('g').lower();
|
||||
let g = boxplusLineGroup;
|
||||
|
||||
if (!isFooter) {
|
||||
actorCnt++;
|
||||
if (Object.keys(actor.links || {}).length && !conf.forceMenus) {
|
||||
g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer');
|
||||
}
|
||||
g.append('line')
|
||||
.attr('id', 'actor' + actorCnt)
|
||||
.attr('x1', center)
|
||||
.attr('y1', centerY)
|
||||
.attr('x2', center)
|
||||
.attr('y2', 2000)
|
||||
.attr('class', 'actor-line 200')
|
||||
.attr('stroke-width', '0.5px')
|
||||
.attr('stroke', '#999')
|
||||
.attr('name', actor.name);
|
||||
|
||||
g = boxplusLineGroup.append('g');
|
||||
actor.actorCnt = actorCnt;
|
||||
|
||||
if (actor.links != null) {
|
||||
g.attr('id', 'root-' + actorCnt);
|
||||
}
|
||||
}
|
||||
|
||||
const rect = svgDrawCommon.getNoteRect();
|
||||
let cssclass = 'actor';
|
||||
if (actor.properties?.class) {
|
||||
cssclass = actor.properties.class;
|
||||
} else {
|
||||
rect.fill = '#eaeaea';
|
||||
}
|
||||
|
||||
if (isFooter) {
|
||||
cssclass += ` ${BOTTOM_ACTOR_CLASS}`;
|
||||
} else {
|
||||
cssclass += ` ${TOP_ACTOR_CLASS}`;
|
||||
}
|
||||
|
||||
rect.x = actor.x;
|
||||
rect.y = actorY;
|
||||
rect.width = actor.width;
|
||||
rect.height = actor.height;
|
||||
rect.class = cssclass;
|
||||
rect.name = actor.name;
|
||||
|
||||
// Cylinder dimensions
|
||||
const ry = rect.height / 2;
|
||||
const rx = ry / (2.5 + rect.height / 50);
|
||||
|
||||
// Cylinder base group
|
||||
const cylinderGroup = g.append('g');
|
||||
const cylinderArc = g.append('g');
|
||||
|
||||
// Main cylinder body
|
||||
cylinderGroup
|
||||
.append('path')
|
||||
.attr(
|
||||
'd',
|
||||
`M ${rect.x},${rect.y + ry}
|
||||
a ${rx},${ry} 0 0 0 0,${rect.height}
|
||||
h ${rect.width - 2 * rx}
|
||||
a ${rx},${ry} 0 0 0 0,-${rect.height}
|
||||
Z
|
||||
`
|
||||
)
|
||||
.attr('class', cssclass);
|
||||
cylinderArc
|
||||
.append('path')
|
||||
.attr(
|
||||
'd',
|
||||
`M ${rect.x},${rect.y + ry}
|
||||
a ${rx},${ry} 0 0 0 0,${rect.height}`
|
||||
)
|
||||
.attr('stroke', '#666')
|
||||
.attr('stroke-width', '1px')
|
||||
.attr('class', cssclass);
|
||||
|
||||
cylinderGroup.attr('transform', `translate(${rx}, ${-(rect.height / 2)})`);
|
||||
cylinderArc.attr('transform', `translate(${rect.width - rx}, ${-rect.height / 2})`);
|
||||
|
||||
actor.rectData = rect;
|
||||
|
||||
if (actor.properties?.icon) {
|
||||
const iconSrc = actor.properties.icon.trim();
|
||||
const iconX = rect.x + rect.width - 20;
|
||||
const iconY = rect.y + 10;
|
||||
if (iconSrc.charAt(0) === '@') {
|
||||
svgDrawCommon.drawEmbeddedImage(g, iconX, iconY, iconSrc.substr(1));
|
||||
} else {
|
||||
svgDrawCommon.drawImage(g, iconX, iconY, iconSrc);
|
||||
}
|
||||
}
|
||||
|
||||
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
|
||||
actor.description,
|
||||
g,
|
||||
rect.x,
|
||||
rect.y,
|
||||
rect.width,
|
||||
rect.height,
|
||||
{ class: `actor ${ACTOR_BOX_CLASS}` },
|
||||
conf
|
||||
);
|
||||
|
||||
let height = actor.height;
|
||||
const lastPath = cylinderGroup.select('path:last-child');
|
||||
if (lastPath.node()) {
|
||||
const bounds = lastPath.node().getBBox();
|
||||
actor.height = bounds.height;
|
||||
height = bounds.height;
|
||||
}
|
||||
|
||||
return height;
|
||||
};
|
||||
|
||||
const drawActorTypeControl = function (elem, actor, conf, isFooter) {
|
||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
||||
const center = actor.x + actor.width / 2;
|
||||
const centerY = actorY + 75;
|
||||
|
||||
const line = elem.append('g').lower();
|
||||
|
||||
if (!isFooter) {
|
||||
actorCnt++;
|
||||
line
|
||||
.append('line')
|
||||
.attr('id', 'actor' + actorCnt)
|
||||
.attr('x1', center)
|
||||
.attr('y1', centerY)
|
||||
.attr('x2', center)
|
||||
.attr('y2', 2000)
|
||||
.attr('class', 'actor-line 200')
|
||||
.attr('stroke-width', '0.5px')
|
||||
.attr('stroke', '#999')
|
||||
.attr('name', actor.name);
|
||||
|
||||
actor.actorCnt = actorCnt;
|
||||
}
|
||||
const actElem = elem.append('g');
|
||||
let cssClass = ACTOR_MAN_FIGURE_CLASS;
|
||||
if (isFooter) {
|
||||
cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
|
||||
} else {
|
||||
cssClass += ` ${TOP_ACTOR_CLASS}`;
|
||||
}
|
||||
actElem.attr('class', cssClass);
|
||||
actElem.attr('name', actor.name);
|
||||
|
||||
const rect = svgDrawCommon.getNoteRect();
|
||||
rect.x = actor.x;
|
||||
rect.y = actorY;
|
||||
rect.fill = '#eaeaea';
|
||||
rect.width = actor.width;
|
||||
rect.height = actor.height;
|
||||
rect.class = 'actor';
|
||||
|
||||
const cx = actor.x + actor.width / 2;
|
||||
const cy = actorY + 30;
|
||||
const r = 18;
|
||||
|
||||
actElem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', 'filled-head-control')
|
||||
.attr('refX', 11)
|
||||
.attr('refY', 5.8)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 28)
|
||||
.attr('orient', '172.5')
|
||||
.append('path')
|
||||
.attr('d', 'M 14.4 5.6 L 7.2 10.4 L 8.8 5.6 L 7.2 0.8 Z');
|
||||
|
||||
// Draw the base circle
|
||||
actElem
|
||||
.append('circle')
|
||||
.attr('cx', cx)
|
||||
.attr('cy', cy)
|
||||
.attr('r', r)
|
||||
.attr('fill', '#eaeaf7')
|
||||
.attr('stroke', '#666')
|
||||
.attr('stroke-width', 1.2);
|
||||
|
||||
// Draw looping arrow as arc path
|
||||
actElem
|
||||
.append('line')
|
||||
.attr('marker-end', 'url(#filled-head-control)')
|
||||
.attr('transform', `translate(${cx}, ${cy - r})`);
|
||||
|
||||
const bounds = actElem.node().getBBox();
|
||||
actor.height = bounds.height + 2 * (conf?.sequence?.labelBoxHeight ?? 0);
|
||||
|
||||
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
|
||||
actor.description,
|
||||
actElem,
|
||||
rect.x,
|
||||
rect.y + r + (isFooter ? 5 : 10),
|
||||
rect.width,
|
||||
rect.height,
|
||||
{ class: `actor ${ACTOR_MAN_FIGURE_CLASS}` },
|
||||
conf
|
||||
);
|
||||
|
||||
return actor.height;
|
||||
};
|
||||
|
||||
const drawActorTypeEntity = function (elem, actor, conf, isFooter) {
|
||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
||||
const center = actor.x + actor.width / 2;
|
||||
const centerY = actorY + 75;
|
||||
|
||||
const line = elem.append('g').lower();
|
||||
|
||||
const actElem = elem.append('g');
|
||||
let cssClass = ACTOR_MAN_FIGURE_CLASS;
|
||||
if (isFooter) {
|
||||
cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
|
||||
} else {
|
||||
cssClass += ` ${TOP_ACTOR_CLASS}`;
|
||||
}
|
||||
actElem.attr('class', cssClass);
|
||||
actElem.attr('name', actor.name);
|
||||
|
||||
const rect = svgDrawCommon.getNoteRect();
|
||||
rect.x = actor.x;
|
||||
rect.y = actorY;
|
||||
rect.fill = '#eaeaea';
|
||||
rect.width = actor.width;
|
||||
rect.height = actor.height;
|
||||
rect.class = 'actor';
|
||||
|
||||
const cx = actor.x + actor.width / 2;
|
||||
const cy = actorY + (!isFooter ? 25 : 10);
|
||||
const r = 18;
|
||||
|
||||
actElem
|
||||
.append('circle')
|
||||
.attr('cx', cx)
|
||||
.attr('cy', cy)
|
||||
.attr('r', r)
|
||||
.attr('width', actor.width)
|
||||
.attr('height', actor.height);
|
||||
|
||||
actElem
|
||||
.append('line')
|
||||
.attr('x1', cx - r)
|
||||
.attr('x2', cx + r)
|
||||
.attr('y1', cy + r)
|
||||
.attr('y2', cy + r)
|
||||
.attr('stroke', '#333')
|
||||
.attr('stroke-width', 2);
|
||||
|
||||
const bounds = actElem.node().getBBox();
|
||||
actor.height = bounds.height + (conf?.sequence?.labelBoxHeight ?? 0);
|
||||
|
||||
if (!isFooter) {
|
||||
actorCnt++;
|
||||
line
|
||||
.append('line')
|
||||
.attr('id', 'actor' + actorCnt)
|
||||
.attr('x1', center)
|
||||
.attr('y1', centerY)
|
||||
.attr('x2', center)
|
||||
.attr('y2', 2000)
|
||||
.attr('class', 'actor-line 200')
|
||||
.attr('stroke-width', '0.5px')
|
||||
.attr('stroke', '#999')
|
||||
.attr('name', actor.name);
|
||||
|
||||
actor.actorCnt = actorCnt;
|
||||
}
|
||||
|
||||
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
|
||||
actor.description,
|
||||
actElem,
|
||||
rect.x,
|
||||
rect.y + (!isFooter ? (cy + r - actorY) / 2 : (cy - actorY + r - 5) / 2),
|
||||
rect.width,
|
||||
rect.height,
|
||||
{ class: `actor ${ACTOR_MAN_FIGURE_CLASS}` },
|
||||
conf
|
||||
);
|
||||
|
||||
if (!isFooter) {
|
||||
actElem.attr('transform', `translate(${0}, ${r / 2})`);
|
||||
} else {
|
||||
actElem.attr('transform', `translate(${0}, ${r / 2})`);
|
||||
}
|
||||
|
||||
return actor.height;
|
||||
};
|
||||
|
||||
const drawActorTypeDatabase = function (elem, actor, conf, isFooter) {
|
||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
||||
const center = actor.x + actor.width / 2;
|
||||
const centerY = actorY + actor.height + 2 * conf.boxTextMargin;
|
||||
|
||||
const boxplusLineGroup = elem.append('g').lower();
|
||||
let g = boxplusLineGroup;
|
||||
|
||||
if (!isFooter) {
|
||||
actorCnt++;
|
||||
if (Object.keys(actor.links || {}).length && !conf.forceMenus) {
|
||||
g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer');
|
||||
}
|
||||
g.append('line')
|
||||
.attr('id', 'actor' + actorCnt)
|
||||
.attr('x1', center)
|
||||
.attr('y1', centerY)
|
||||
.attr('x2', center)
|
||||
.attr('y2', 2000)
|
||||
.attr('class', 'actor-line 200')
|
||||
.attr('stroke-width', '0.5px')
|
||||
.attr('stroke', '#999')
|
||||
.attr('name', actor.name);
|
||||
|
||||
g = boxplusLineGroup.append('g');
|
||||
actor.actorCnt = actorCnt;
|
||||
|
||||
if (actor.links != null) {
|
||||
g.attr('id', 'root-' + actorCnt);
|
||||
}
|
||||
}
|
||||
|
||||
const rect = svgDrawCommon.getNoteRect();
|
||||
|
||||
let cssclass = 'actor';
|
||||
if (actor.properties?.class) {
|
||||
cssclass = actor.properties.class;
|
||||
} else {
|
||||
rect.fill = '#eaeaea';
|
||||
}
|
||||
|
||||
if (isFooter) {
|
||||
cssclass += ` ${BOTTOM_ACTOR_CLASS}`;
|
||||
} else {
|
||||
cssclass += ` ${TOP_ACTOR_CLASS}`;
|
||||
}
|
||||
|
||||
rect.x = actor.x;
|
||||
rect.y = actorY;
|
||||
rect.width = actor.width;
|
||||
rect.height = actor.height;
|
||||
rect.class = cssclass;
|
||||
rect.name = actor.name;
|
||||
|
||||
// Cylinder dimensions
|
||||
rect.x = actor.x;
|
||||
rect.y = actorY;
|
||||
const w = rect.width / 4;
|
||||
const h = rect.width / 4;
|
||||
const rx = w / 2;
|
||||
const ry = rx / (2.5 + w / 50);
|
||||
|
||||
// Cylinder base group
|
||||
const cylinderGroup = g.append('g');
|
||||
|
||||
const d = `
|
||||
M ${rect.x},${rect.y + ry}
|
||||
a ${rx},${ry} 0 0 0 ${w},0
|
||||
a ${rx},${ry} 0 0 0 -${w},0
|
||||
l 0,${h - 2 * ry}
|
||||
a ${rx},${ry} 0 0 0 ${w},0
|
||||
l 0,-${h - 2 * ry}
|
||||
`;
|
||||
// Draw the main cylinder body
|
||||
cylinderGroup
|
||||
.append('path')
|
||||
.attr('d', d)
|
||||
.attr('fill', '#eaeaea')
|
||||
.attr('stroke', '#000')
|
||||
.attr('stroke-width', 1)
|
||||
.attr('class', cssclass);
|
||||
|
||||
if (!isFooter) {
|
||||
cylinderGroup.attr('transform', `translate(${w * 1.5}, ${(rect.height + ry) / 4})`);
|
||||
} else {
|
||||
cylinderGroup.attr('transform', `translate(${w * 1.5}, ${rect.height / 4 - 2 * ry})`);
|
||||
}
|
||||
actor.rectData = rect;
|
||||
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
|
||||
actor.description,
|
||||
g,
|
||||
rect.x,
|
||||
rect.y + (!isFooter ? (rect.height + ry) / 2 : (rect.height + h) / 4),
|
||||
rect.width,
|
||||
rect.height,
|
||||
{ class: `actor ${ACTOR_BOX_CLASS}` },
|
||||
conf
|
||||
);
|
||||
|
||||
const lastPath = cylinderGroup.select('path:last-child');
|
||||
if (lastPath.node()) {
|
||||
const bounds = lastPath.node().getBBox();
|
||||
actor.height = bounds.height + (conf.sequence.labelBoxHeight ?? 0);
|
||||
}
|
||||
|
||||
return actor.height;
|
||||
};
|
||||
|
||||
const drawActorTypeBoundary = function (elem, actor, conf, isFooter) {
|
||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
||||
const center = actor.x + actor.width / 2;
|
||||
const centerY = actorY + 80;
|
||||
const radius = 30;
|
||||
const line = elem.append('g').lower();
|
||||
|
||||
if (!isFooter) {
|
||||
actorCnt++;
|
||||
line
|
||||
.append('line')
|
||||
.attr('id', 'actor' + actorCnt)
|
||||
.attr('x1', center)
|
||||
.attr('y1', centerY)
|
||||
.attr('x2', center)
|
||||
.attr('y2', 2000)
|
||||
.attr('class', 'actor-line 200')
|
||||
.attr('stroke-width', '0.5px')
|
||||
.attr('stroke', '#999')
|
||||
.attr('name', actor.name);
|
||||
|
||||
actor.actorCnt = actorCnt;
|
||||
}
|
||||
const actElem = elem.append('g');
|
||||
let cssClass = ACTOR_MAN_FIGURE_CLASS;
|
||||
if (isFooter) {
|
||||
cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
|
||||
} else {
|
||||
cssClass += ` ${TOP_ACTOR_CLASS}`;
|
||||
}
|
||||
actElem.attr('class', cssClass);
|
||||
actElem.attr('name', actor.name);
|
||||
|
||||
const rect = svgDrawCommon.getNoteRect();
|
||||
rect.x = actor.x;
|
||||
rect.y = actorY;
|
||||
rect.fill = '#eaeaea';
|
||||
rect.width = actor.width;
|
||||
rect.height = actor.height;
|
||||
rect.class = 'actor';
|
||||
|
||||
actElem
|
||||
.append('line')
|
||||
.attr('id', 'actor-man-torso' + actorCnt)
|
||||
.attr('x1', actor.x + actor.width / 2 - radius * 2.5)
|
||||
.attr('y1', actorY + 10)
|
||||
.attr('x2', actor.x + actor.width / 2 - 15)
|
||||
.attr('y2', actorY + 10);
|
||||
|
||||
actElem
|
||||
.append('line')
|
||||
.attr('id', 'actor-man-arms' + actorCnt)
|
||||
.attr('x1', actor.x + actor.width / 2 - radius * 2.5)
|
||||
.attr('y1', actorY + 0) // starting Y
|
||||
.attr('x2', actor.x + actor.width / 2 - radius * 2.5)
|
||||
.attr('y2', actorY + 20); // ending Y (26px long, adjust as needed)
|
||||
|
||||
actElem
|
||||
.append('circle')
|
||||
.attr('cx', actor.x + actor.width / 2)
|
||||
.attr('cy', actorY + 10)
|
||||
.attr('r', radius);
|
||||
|
||||
const bounds = actElem.node().getBBox();
|
||||
actor.height = bounds.height + (conf.sequence.labelBoxHeight ?? 0);
|
||||
|
||||
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
|
||||
actor.description,
|
||||
actElem,
|
||||
rect.x,
|
||||
rect.y + (!isFooter ? radius / 2 + 3 : radius / 2 - 4),
|
||||
rect.width,
|
||||
rect.height,
|
||||
{ class: `actor ${ACTOR_MAN_FIGURE_CLASS}` },
|
||||
conf
|
||||
);
|
||||
|
||||
if (!isFooter) {
|
||||
actElem.attr('transform', `translate(0,${radius / 2 + 7})`);
|
||||
} else {
|
||||
actElem.attr('transform', `translate(0,${radius / 2 + 7})`);
|
||||
}
|
||||
|
||||
return actor.height;
|
||||
};
|
||||
|
||||
const drawActorTypeActor = function (elem, actor, conf, isFooter) {
|
||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
||||
const center = actor.x + actor.width / 2;
|
||||
@@ -516,6 +1110,18 @@ export const drawActor = async function (elem, actor, conf, isFooter) {
|
||||
return await drawActorTypeActor(elem, actor, conf, isFooter);
|
||||
case 'participant':
|
||||
return await drawActorTypeParticipant(elem, actor, conf, isFooter);
|
||||
case 'boundary':
|
||||
return await drawActorTypeBoundary(elem, actor, conf, isFooter);
|
||||
case 'control':
|
||||
return await drawActorTypeControl(elem, actor, conf, isFooter);
|
||||
case 'entity':
|
||||
return await drawActorTypeEntity(elem, actor, conf, isFooter);
|
||||
case 'database':
|
||||
return await drawActorTypeDatabase(elem, actor, conf, isFooter);
|
||||
case 'collections':
|
||||
return await drawActorTypeCollections(elem, actor, conf, isFooter);
|
||||
case 'queue':
|
||||
return await drawActorTypeQueue(elem, actor, conf, isFooter);
|
||||
}
|
||||
};
|
||||
|
||||
|
124
packages/mermaid/src/diagrams/useCase/styles.js
Normal file
124
packages/mermaid/src/diagrams/useCase/styles.js
Normal file
@@ -0,0 +1,124 @@
|
||||
const getStyles = (options) =>
|
||||
`
|
||||
.usecase-diagram {
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: ${options.fontSize};
|
||||
}
|
||||
|
||||
/* Actor styles */
|
||||
.usecase-actor-man {
|
||||
stroke: ${options.actorBorder};
|
||||
fill: ${options.actorBkg};
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.usecase-actor-man circle {
|
||||
fill: ${options.useCaseActorBkg};
|
||||
stroke: ${options.useCaseActorBorder};
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.usecase-actor-man line {
|
||||
stroke: ${options.useCaseActorBorder};
|
||||
stroke-width: 2px;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.usecase-actor-man text {
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
fill: ${options.useCaseActorTextColor};
|
||||
text-anchor: middle;
|
||||
dominant-baseline: central;
|
||||
}
|
||||
|
||||
/* Use case styles */
|
||||
.usecase-usecase {
|
||||
fill: ${options.useCaseUseCaseBkg};
|
||||
stroke: ${options.useCaseUseCaseBorder};
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.usecase-usecase text {
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 12px;
|
||||
fill: ${options.useCaseUseCaseTextColor};
|
||||
text-anchor: middle;
|
||||
dominant-baseline: central;
|
||||
}
|
||||
|
||||
/* System boundary styles */
|
||||
.usecase-system-boundary {
|
||||
fill: ${options.useCaseSystemBoundaryBkg};
|
||||
stroke: ${options.useCaseSystemBoundaryBorder};
|
||||
stroke-width: 2px;
|
||||
stroke-dasharray: 5,5;
|
||||
}
|
||||
|
||||
.usecase-system-boundary text {
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
fill: ${options.useCaseSystemBoundaryTextColor};
|
||||
text-anchor: middle;
|
||||
dominant-baseline: central;
|
||||
}
|
||||
|
||||
/* Arrow and relationship styles */
|
||||
.usecase-arrow {
|
||||
stroke: ${'red'};
|
||||
stroke-width: 2px;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.usecase-arrow-label {
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 12px;
|
||||
fill: ${options.useCaseArrowTextColor};
|
||||
text-anchor: middle;
|
||||
dominant-baseline: central;
|
||||
}
|
||||
|
||||
/* Node styles for standalone nodes */
|
||||
.usecase-node {
|
||||
fill: ${options.useCaseUseCaseBkg};
|
||||
stroke: ${options.useCaseUseCaseBorder};
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.usecase-node text {
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 12px;
|
||||
fill: ${options.useCaseUseCaseTextColor};
|
||||
text-anchor: middle;
|
||||
dominant-baseline: central;
|
||||
}
|
||||
|
||||
/* Hover effects */
|
||||
.usecase-actor-man:hover circle {
|
||||
fill: ${options.useCaseActorBkg};
|
||||
stroke: ${options.useCaseArrowColor};
|
||||
}
|
||||
|
||||
.usecase-actor-man:hover line {
|
||||
stroke: ${options.useCaseArrowColor};
|
||||
}
|
||||
|
||||
.usecase-actor-man:hover text {
|
||||
fill: ${options.useCaseArrowColor};
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.usecase-usecase:hover {
|
||||
fill: ${options.useCaseSystemBoundaryBkg};
|
||||
stroke: ${options.useCaseArrowColor};
|
||||
}
|
||||
|
||||
.usecase-usecase:hover text {
|
||||
fill: ${options.useCaseArrowColor};
|
||||
font-weight: bold;
|
||||
}
|
||||
`;
|
||||
|
||||
export default getStyles;
|
586
packages/mermaid/src/diagrams/useCase/useCaseDb.ts
Normal file
586
packages/mermaid/src/diagrams/useCase/useCaseDb.ts
Normal file
@@ -0,0 +1,586 @@
|
||||
// Simple actor type for useCase diagrams
|
||||
interface Actor {
|
||||
type: 'actor';
|
||||
name: string;
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
// Simple use case type
|
||||
interface UseCase {
|
||||
type: 'useCase';
|
||||
name: string;
|
||||
}
|
||||
|
||||
// System boundary type
|
||||
interface SystemBoundary {
|
||||
type: 'systemBoundary';
|
||||
name: string;
|
||||
useCases: UseCase[];
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
// System boundary metadata type
|
||||
interface SystemBoundaryMetadata {
|
||||
type: 'systemBoundaryMetadata';
|
||||
name: string; // boundary name
|
||||
metadata: Record<string, string>;
|
||||
}
|
||||
|
||||
// Actor-UseCase relationship type
|
||||
interface ActorUseCaseRelationship {
|
||||
type: 'actorUseCaseRelationship';
|
||||
from: string; // actor name
|
||||
to: string; // use case name
|
||||
arrow: string; // '-->' or '->'
|
||||
label?: string; // edge label (optional)
|
||||
}
|
||||
|
||||
// Node type
|
||||
interface Node {
|
||||
type: 'node';
|
||||
id: string; // node ID (e.g., 'a', 'b', 'c')
|
||||
label: string; // node label (e.g., 'Go through code')
|
||||
}
|
||||
|
||||
// Actor-Node relationship type
|
||||
interface ActorNodeRelationship {
|
||||
type: 'actorNodeRelationship';
|
||||
from: string; // actor name
|
||||
to: string; // node ID
|
||||
arrow: string; // '-->' or '->'
|
||||
label?: string; // edge label (optional)
|
||||
}
|
||||
|
||||
// Inline Actor-Node relationship type
|
||||
interface InlineActorNodeRelationship {
|
||||
type: 'inlineActorNodeRelationship';
|
||||
actor: string; // actor name
|
||||
node: Node; // node definition
|
||||
arrow: string; // '-->' or '->'
|
||||
label?: string; // edge label (optional)
|
||||
}
|
||||
|
||||
export class UseCaseDB {
|
||||
private actors: Actor[] = [];
|
||||
private systemBoundaries: SystemBoundary[] = [];
|
||||
private systemBoundaryMetadata: SystemBoundaryMetadata[] = [];
|
||||
private useCases: UseCase[] = [];
|
||||
private relationships: ActorUseCaseRelationship[] = [];
|
||||
private nodes: Node[] = [];
|
||||
private nodeRelationships: ActorNodeRelationship[] = [];
|
||||
private inlineRelationships: InlineActorNodeRelationship[] = [];
|
||||
|
||||
constructor() {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.actors = [];
|
||||
this.systemBoundaries = [];
|
||||
this.systemBoundaryMetadata = [];
|
||||
this.useCases = [];
|
||||
this.relationships = [];
|
||||
this.nodes = [];
|
||||
this.nodeRelationships = [];
|
||||
this.inlineRelationships = [];
|
||||
}
|
||||
|
||||
addActor(actor: Actor): void {
|
||||
this.actors.push(actor);
|
||||
}
|
||||
|
||||
addSystemBoundary(boundary: SystemBoundary): void {
|
||||
this.systemBoundaries.push(boundary);
|
||||
}
|
||||
|
||||
addSystemBoundaryMetadata(metadata: SystemBoundaryMetadata): void {
|
||||
this.systemBoundaryMetadata.push(metadata);
|
||||
// Apply metadata to existing system boundary
|
||||
const boundary = this.systemBoundaries.find(b => b.name === metadata.name);
|
||||
if (boundary) {
|
||||
boundary.metadata = metadata.metadata;
|
||||
}
|
||||
}
|
||||
|
||||
addUseCase(useCase: UseCase): void {
|
||||
this.useCases.push(useCase);
|
||||
}
|
||||
|
||||
addRelationship(relationship: ActorUseCaseRelationship): void {
|
||||
this.relationships.push(relationship);
|
||||
}
|
||||
|
||||
addNode(node: Node): void {
|
||||
this.nodes.push(node);
|
||||
}
|
||||
|
||||
addNodeRelationship(relationship: ActorNodeRelationship): void {
|
||||
this.nodeRelationships.push(relationship);
|
||||
}
|
||||
|
||||
addInlineRelationship(relationship: InlineActorNodeRelationship): void {
|
||||
this.inlineRelationships.push(relationship);
|
||||
// Also add the node and actor separately
|
||||
this.addNode(relationship.node);
|
||||
// Add actor if not already exists
|
||||
const actorExists = this.actors.some(actor => actor.name === relationship.actor);
|
||||
if (!actorExists) {
|
||||
this.addActor({
|
||||
type: 'actor',
|
||||
name: relationship.actor
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getActors(): Actor[] {
|
||||
return this.actors;
|
||||
}
|
||||
|
||||
getSystemBoundaries(): SystemBoundary[] {
|
||||
return this.systemBoundaries;
|
||||
}
|
||||
|
||||
getSystemBoundaryMetadata(): SystemBoundaryMetadata[] {
|
||||
return this.systemBoundaryMetadata;
|
||||
}
|
||||
|
||||
getUseCases(): UseCase[] {
|
||||
return this.useCases;
|
||||
}
|
||||
|
||||
getRelationships(): ActorUseCaseRelationship[] {
|
||||
return this.relationships;
|
||||
}
|
||||
|
||||
getNodes(): Node[] {
|
||||
return this.nodes;
|
||||
}
|
||||
|
||||
getNodeRelationships(): ActorNodeRelationship[] {
|
||||
return this.nodeRelationships;
|
||||
}
|
||||
|
||||
getInlineRelationships(): InlineActorNodeRelationship[] {
|
||||
return this.inlineRelationships;
|
||||
}
|
||||
|
||||
parse(text: string): void {
|
||||
this.clear();
|
||||
|
||||
// For now, use the simple parser with enhanced metadata support
|
||||
// TODO: Integrate ANTLR parser in the future
|
||||
|
||||
// Simple parser for usecase diagrams (fallback)
|
||||
const lines = text.split('\n').map(line => line.trim()).filter(line => line && !line.startsWith('%'));
|
||||
|
||||
let foundUsecase = false;
|
||||
let inSystemBoundary = false;
|
||||
let currentBoundary: SystemBoundary | null = null;
|
||||
let inMetadataBlock = false;
|
||||
let currentMetadataName = '';
|
||||
let currentMetadataContent = '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (line === 'usecase') {
|
||||
foundUsecase = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!foundUsecase) {
|
||||
continue
|
||||
};
|
||||
|
||||
if (line.startsWith('actor ')) {
|
||||
const actorPart = line.substring(6).trim();
|
||||
if (actorPart) {
|
||||
// Check if this is an inline actor-node relationship
|
||||
if (this.isInlineActorNodeRelationshipLine(actorPart)) {
|
||||
const relationship = this.parseInlineActorNodeRelationshipLine(actorPart);
|
||||
if (relationship) {
|
||||
this.addInlineRelationship(relationship);
|
||||
}
|
||||
} else {
|
||||
const actors = this.parseActorList(actorPart);
|
||||
actors.forEach((actor: Actor) => this.addActor(actor));
|
||||
}
|
||||
}
|
||||
} else if (line.startsWith('systemBoundary ')) {
|
||||
const boundaryPart = line.substring(15).trim();
|
||||
if (boundaryPart.endsWith(' {')) {
|
||||
// New curly brace syntax: systemBoundary Name {
|
||||
const boundaryName = boundaryPart.substring(0, boundaryPart.length - 2).trim();
|
||||
currentBoundary = {
|
||||
type: 'systemBoundary',
|
||||
name: boundaryName,
|
||||
useCases: []
|
||||
};
|
||||
inSystemBoundary = true;
|
||||
} else if (boundaryPart) {
|
||||
// Old syntax: systemBoundary Name (followed by 'end')
|
||||
currentBoundary = {
|
||||
type: 'systemBoundary',
|
||||
name: boundaryPart,
|
||||
useCases: []
|
||||
};
|
||||
inSystemBoundary = true;
|
||||
}
|
||||
} else if (line === 'end' || (line === '}' && !inMetadataBlock)) {
|
||||
if (inSystemBoundary && currentBoundary) {
|
||||
this.addSystemBoundary(currentBoundary);
|
||||
currentBoundary = null;
|
||||
inSystemBoundary = false;
|
||||
}
|
||||
} else if (inSystemBoundary && currentBoundary && line) {
|
||||
// This is a use case inside the system boundary
|
||||
const useCase: UseCase = {
|
||||
type: 'useCase',
|
||||
name: line
|
||||
};
|
||||
currentBoundary.useCases.push(useCase);
|
||||
} else if (line && !inSystemBoundary) {
|
||||
// Handle multi-line metadata blocks
|
||||
if (inMetadataBlock) {
|
||||
if (line.includes('}')) {
|
||||
// End of metadata block
|
||||
currentMetadataContent += line.replace('}', '').trim();
|
||||
const metadata = this.parseMetadataContent(currentMetadataName, currentMetadataContent);
|
||||
if (metadata) {
|
||||
this.addSystemBoundaryMetadata(metadata);
|
||||
}
|
||||
inMetadataBlock = false;
|
||||
currentMetadataName = '';
|
||||
currentMetadataContent = '';
|
||||
} else {
|
||||
// Continue collecting metadata content
|
||||
currentMetadataContent += line.trim() + ' ';
|
||||
}
|
||||
} else if (line.includes('@{')) {
|
||||
// Start of metadata block
|
||||
const match = line.match(/^(\w+)@\{(.*)$/);
|
||||
if (match) {
|
||||
currentMetadataName = match[1];
|
||||
const content = match[2].trim();
|
||||
if (content.includes('}')) {
|
||||
// Single line metadata
|
||||
const metadata = this.parseMetadataContent(currentMetadataName, content.replace('}', ''));
|
||||
if (metadata) {
|
||||
this.addSystemBoundaryMetadata(metadata);
|
||||
}
|
||||
} else {
|
||||
// Multi-line metadata
|
||||
inMetadataBlock = true;
|
||||
currentMetadataContent = content + ' ';
|
||||
}
|
||||
}
|
||||
} else if (this.isRelationshipLine(line)) {
|
||||
// Check if this is a relationship (actor --> usecase or actor --> node)
|
||||
const relationship = this.parseRelationshipLine(line);
|
||||
if (relationship) {
|
||||
if (relationship.type === 'actorUseCaseRelationship') {
|
||||
this.addRelationship(relationship);
|
||||
} else if (relationship.type === 'actorNodeRelationship') {
|
||||
this.addNodeRelationship(relationship);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This is a standalone use case
|
||||
const useCase: UseCase = {
|
||||
type: 'useCase',
|
||||
name: line
|
||||
};
|
||||
this.addUseCase(useCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private parseActorList(actorPart: string): Actor[] {
|
||||
// Smart split by comma that respects metadata braces
|
||||
const actorNames = this.smartSplitActors(actorPart);
|
||||
|
||||
return actorNames.map(actorName => this.parseActorWithMetadata(actorName));
|
||||
}
|
||||
|
||||
private smartSplitActors(input: string): string[] {
|
||||
const actors: string[] = [];
|
||||
let current = '';
|
||||
let braceDepth = 0;
|
||||
let inQuotes = false;
|
||||
let quoteChar = '';
|
||||
|
||||
for (const char of input) {
|
||||
|
||||
if (!inQuotes && (char === '"' || char === "'")) {
|
||||
inQuotes = true;
|
||||
quoteChar = char;
|
||||
current += char;
|
||||
} else if (inQuotes && char === quoteChar) {
|
||||
inQuotes = false;
|
||||
quoteChar = '';
|
||||
current += char;
|
||||
} else if (!inQuotes && char === '{') {
|
||||
braceDepth++;
|
||||
current += char;
|
||||
} else if (!inQuotes && char === '}') {
|
||||
braceDepth--;
|
||||
current += char;
|
||||
} else if (!inQuotes && char === ',' && braceDepth === 0) {
|
||||
// This is a real separator, not inside metadata
|
||||
if (current.trim()) {
|
||||
actors.push(current.trim());
|
||||
}
|
||||
current = '';
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last actor
|
||||
if (current.trim()) {
|
||||
actors.push(current.trim());
|
||||
}
|
||||
|
||||
return actors;
|
||||
}
|
||||
|
||||
private parseActorWithMetadata(actorPart: string): Actor {
|
||||
// Check if there's metadata (contains @{...})
|
||||
const metadataRegex = /^([^@]+)@{([^}]*)}$/;
|
||||
const metadataMatch = metadataRegex.exec(actorPart);
|
||||
|
||||
if (metadataMatch) {
|
||||
const name = metadataMatch[1].trim();
|
||||
const metadataStr = metadataMatch[2].trim();
|
||||
const metadata = this.parseMetadataString(metadataStr);
|
||||
|
||||
return {
|
||||
type: 'actor',
|
||||
name,
|
||||
metadata
|
||||
};
|
||||
} else {
|
||||
// No metadata, just return the name
|
||||
return {
|
||||
type: 'actor',
|
||||
name: actorPart
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private parseMetadataString(metadataStr: string): Record<string, string> {
|
||||
const metadata: Record<string, string> = {};
|
||||
|
||||
if (!metadataStr.trim()) {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
// Split by comma and parse key-value pairs
|
||||
const pairs = metadataStr.split(',');
|
||||
|
||||
for (const pair of pairs) {
|
||||
const colonIndex = pair.indexOf(':');
|
||||
if (colonIndex > 0) {
|
||||
const key = pair.substring(0, colonIndex).trim();
|
||||
let value = pair.substring(colonIndex + 1).trim();
|
||||
|
||||
// Remove quotes if present
|
||||
if ((value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
metadata[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private isRelationshipLine(line: string): boolean {
|
||||
return line.includes('-->') || line.includes('->');
|
||||
}
|
||||
|
||||
private parseRelationshipLine(line: string): ActorUseCaseRelationship | ActorNodeRelationship | null {
|
||||
let arrow = '';
|
||||
let label: string | undefined;
|
||||
let parts: string[] = [];
|
||||
|
||||
// Check for labeled arrows first (--label--> or --label->)
|
||||
const labeledArrowMatch = line.match(/^(.+?)\s*(--\w+--?>)\s*(.+)$/);
|
||||
if (labeledArrowMatch) {
|
||||
parts = [labeledArrowMatch[1].trim(), labeledArrowMatch[3].trim()];
|
||||
arrow = labeledArrowMatch[2];
|
||||
// Extract label from arrow
|
||||
const labelMatch = arrow.match(/^--(\w+)--?>$/);
|
||||
if (labelMatch) {
|
||||
label = labelMatch[1];
|
||||
}
|
||||
} else if (line.includes('-->')) {
|
||||
arrow = '-->';
|
||||
parts = line.split('-->').map(part => part.trim());
|
||||
} else if (line.includes('->')) {
|
||||
arrow = '->';
|
||||
parts = line.split('->').map(part => part.trim());
|
||||
}
|
||||
|
||||
if (parts.length === 2 && parts[0] && parts[1]) {
|
||||
// Check if target is a node definition (contains parentheses)
|
||||
if (this.isNodeDefinitionString(parts[1])) {
|
||||
const node = this.parseNodeDefinitionString(parts[1]);
|
||||
if (node) {
|
||||
this.addNode(node);
|
||||
return {
|
||||
type: 'actorNodeRelationship',
|
||||
from: parts[0],
|
||||
to: node.id,
|
||||
arrow,
|
||||
label
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
type: 'actorUseCaseRelationship',
|
||||
from: parts[0],
|
||||
to: parts[1],
|
||||
arrow,
|
||||
label
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private isInlineActorNodeRelationshipLine(line: string): boolean {
|
||||
// Check for pattern: ActorName --> nodeId(label) or ActorName --label--> nodeId(label)
|
||||
const hasArrow = line.includes('-->') || line.includes('->') || !!line.match(/--\w+-->/);
|
||||
const hasNodeDefinition = line.includes('(') && line.includes(')');
|
||||
return hasArrow && hasNodeDefinition;
|
||||
}
|
||||
|
||||
private parseInlineActorNodeRelationshipLine(line: string): InlineActorNodeRelationship | null {
|
||||
let arrow = '';
|
||||
let label: string | undefined;
|
||||
let parts: string[] = [];
|
||||
|
||||
// Check for labeled arrows first (--label--> or --label->)
|
||||
const labeledArrowMatch = line.match(/^(.+?)\s*(--\w+--?>)\s*(.+)$/);
|
||||
if (labeledArrowMatch) {
|
||||
parts = [labeledArrowMatch[1].trim(), labeledArrowMatch[3].trim()];
|
||||
arrow = labeledArrowMatch[2];
|
||||
// Extract label from arrow
|
||||
const labelMatch = arrow.match(/^--(\w+)--?>$/);
|
||||
if (labelMatch) {
|
||||
label = labelMatch[1];
|
||||
}
|
||||
} else if (line.includes('-->')) {
|
||||
arrow = '-->';
|
||||
parts = line.split('-->').map(part => part.trim());
|
||||
} else if (line.includes('->')) {
|
||||
arrow = '->';
|
||||
parts = line.split('->').map(part => part.trim());
|
||||
}
|
||||
|
||||
if (parts.length === 2 && parts[0] && parts[1]) {
|
||||
const node = this.parseNodeDefinitionString(parts[1]);
|
||||
if (node) {
|
||||
return {
|
||||
type: 'inlineActorNodeRelationship',
|
||||
actor: parts[0],
|
||||
node,
|
||||
arrow,
|
||||
label
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private isNodeDefinitionString(str: string): boolean {
|
||||
return str.includes('(') && str.includes(')');
|
||||
}
|
||||
|
||||
private parseNodeDefinitionString(str: string): Node | null {
|
||||
const match = str.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\((.+)\)$/);
|
||||
if (match) {
|
||||
return {
|
||||
type: 'node',
|
||||
id: match[1],
|
||||
label: match[2]
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private isSystemBoundaryMetadataLine(line: string): boolean {
|
||||
// Check for pattern: boundaryName@{...}
|
||||
return line.includes('@{') && line.includes('}');
|
||||
}
|
||||
|
||||
private parseSystemBoundaryMetadataLine(line: string): SystemBoundaryMetadata | null {
|
||||
// Parse pattern: boundaryName@{key: value, key2: value2}
|
||||
const match = line.match(/^(\w+)@\{(.+)\}$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = match[1];
|
||||
const metadataContent = match[2];
|
||||
const metadata: Record<string, string> = {};
|
||||
|
||||
// Parse key-value pairs
|
||||
const pairs = metadataContent.split(',').map(pair => pair.trim());
|
||||
for (const pair of pairs) {
|
||||
const colonIndex = pair.indexOf(':');
|
||||
if (colonIndex > 0) {
|
||||
const key = pair.substring(0, colonIndex).trim();
|
||||
let value = pair.substring(colonIndex + 1).trim();
|
||||
|
||||
// Remove quotes if present
|
||||
if ((value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
metadata[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'systemBoundaryMetadata',
|
||||
name,
|
||||
metadata
|
||||
};
|
||||
}
|
||||
|
||||
private parseMetadataContent(name: string, content: string): SystemBoundaryMetadata | null {
|
||||
const metadata: Record<string, string> = {};
|
||||
|
||||
// Parse key-value pairs from content
|
||||
const pairs = content.split(',').map(pair => pair.trim()).filter(pair => pair);
|
||||
for (const pair of pairs) {
|
||||
const colonIndex = pair.indexOf(':');
|
||||
if (colonIndex > 0) {
|
||||
const key = pair.substring(0, colonIndex).trim();
|
||||
let value = pair.substring(colonIndex + 1).trim();
|
||||
|
||||
// Remove quotes if present
|
||||
if ((value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
metadata[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'systemBoundaryMetadata',
|
||||
name,
|
||||
metadata
|
||||
};
|
||||
}
|
||||
}
|
24
packages/mermaid/src/diagrams/useCase/useCaseDetector.ts
Normal file
24
packages/mermaid/src/diagrams/useCase/useCaseDetector.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type {
|
||||
DiagramDetector,
|
||||
DiagramLoader,
|
||||
ExternalDiagramDefinition,
|
||||
} from '../../diagram-api/types.js';
|
||||
|
||||
const id = 'usecase';
|
||||
|
||||
const detector: DiagramDetector = (txt) => {
|
||||
return /^\s*usecase/.test(txt);
|
||||
};
|
||||
|
||||
const loader: DiagramLoader = async () => {
|
||||
const { diagram } = await import('./useCaseDiagram.js');
|
||||
return { id, diagram };
|
||||
};
|
||||
|
||||
const plugin: ExternalDiagramDefinition = {
|
||||
id,
|
||||
detector,
|
||||
loader,
|
||||
};
|
||||
|
||||
export default plugin;
|
1421
packages/mermaid/src/diagrams/useCase/useCaseDiagram.spec.js
Normal file
1421
packages/mermaid/src/diagrams/useCase/useCaseDiagram.spec.js
Normal file
File diff suppressed because it is too large
Load Diff
33
packages/mermaid/src/diagrams/useCase/useCaseDiagram.ts
Normal file
33
packages/mermaid/src/diagrams/useCase/useCaseDiagram.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
import { UseCaseDB } from './useCaseDb.js';
|
||||
import styles from './styles.js';
|
||||
import renderer from './useCaseRenderer.js';
|
||||
|
||||
// Shared database instance
|
||||
let db: UseCaseDB;
|
||||
|
||||
// Create a simple parser that integrates with our custom parser
|
||||
const parser = {
|
||||
parse: (text: string) => {
|
||||
// Use the shared database instance
|
||||
db.parse(text);
|
||||
},
|
||||
};
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
get db() {
|
||||
if (!db) {
|
||||
db = new UseCaseDB();
|
||||
}
|
||||
return db;
|
||||
},
|
||||
renderer,
|
||||
styles,
|
||||
init: (cnf) => {
|
||||
// Initialize configuration if needed
|
||||
if (!db) {
|
||||
db = new UseCaseDB();
|
||||
}
|
||||
},
|
||||
};
|
619
packages/mermaid/src/diagrams/useCase/useCaseRenderer.ts
Normal file
619
packages/mermaid/src/diagrams/useCase/useCaseRenderer.ts
Normal file
@@ -0,0 +1,619 @@
|
||||
import { select } from 'd3';
|
||||
import type { Diagram } from '../../Diagram.js';
|
||||
import type { UseCaseDB } from './useCaseDb.js';
|
||||
import { log } from '../../logger.js';
|
||||
|
||||
// Position interfaces
|
||||
interface NodePosition {
|
||||
name: string; // node ID (for relationship matching)
|
||||
label: string; // node label (for display)
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
// Constants for actor rendering
|
||||
const ACTOR_TYPE_WIDTH = 36; // 18 * 2 from sequence diagram
|
||||
const ACTOR_MAN_FIGURE_CLASS = 'usecase-actor-man';
|
||||
const ACTOR_SPACING = 120; // Horizontal spacing between actors
|
||||
const ACTOR_HEIGHT = 80; // Height of actor figure
|
||||
const MARGIN = 50; // Margin around the diagram
|
||||
|
||||
// Simple actor interface for positioning
|
||||
interface ActorPosition {
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
// System boundary interface for positioning
|
||||
interface SystemBoundaryPosition {
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
useCases: UseCasePosition[];
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
// Use case interface for positioning
|
||||
interface UseCasePosition {
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a stick figure actor similar to sequence diagrams but optimized for useCase
|
||||
*/
|
||||
const drawActorTypeActor = (elem: any, actor: ActorPosition, conf: any): number => {
|
||||
const center = actor.x + actor.width / 2;
|
||||
const actorY = actor.y;
|
||||
|
||||
// Create actor group
|
||||
const actElem = elem.append('g');
|
||||
actElem.attr('class', ACTOR_MAN_FIGURE_CLASS);
|
||||
actElem.attr('name', actor.name);
|
||||
|
||||
// Draw stick figure
|
||||
// Head (circle)
|
||||
actElem
|
||||
.append('circle')
|
||||
.attr('cx', center)
|
||||
.attr('cy', actorY + 15)
|
||||
.attr('r', 10);
|
||||
|
||||
// Body (torso line)
|
||||
actElem
|
||||
.append('line')
|
||||
.attr('x1', center)
|
||||
.attr('y1', actorY + 25)
|
||||
.attr('x2', center)
|
||||
.attr('y2', actorY + 50)
|
||||
.style('stroke', 'black');
|
||||
|
||||
// Arms (horizontal line)
|
||||
actElem
|
||||
.append('line')
|
||||
.attr('x1', center - ACTOR_TYPE_WIDTH / 2)
|
||||
.attr('y1', actorY + 35)
|
||||
.attr('x2', center + ACTOR_TYPE_WIDTH / 2)
|
||||
.style('stroke', 'black')
|
||||
.attr('y2', actorY + 35);
|
||||
|
||||
// Left leg
|
||||
actElem
|
||||
.append('line')
|
||||
.attr('x1', center)
|
||||
.attr('y1', actorY + 50)
|
||||
.attr('x2', center - ACTOR_TYPE_WIDTH / 2)
|
||||
.style('stroke', 'black')
|
||||
.attr('y2', actorY + 70);
|
||||
|
||||
// Right leg
|
||||
actElem
|
||||
.append('line')
|
||||
.attr('x1', center)
|
||||
.attr('y1', actorY + 50)
|
||||
.attr('x2', center + ACTOR_TYPE_WIDTH / 2)
|
||||
.attr('y2', actorY + 70)
|
||||
.style('stroke', 'black');
|
||||
|
||||
// Actor name text
|
||||
const textY = actorY + ACTOR_HEIGHT + 15;
|
||||
drawActorText(actor.name, actElem, actor.x, textY, actor.width, 20);
|
||||
|
||||
return ACTOR_HEIGHT; // Total height including text and metadata
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws text for actor name - simplified version of sequence diagram text drawing
|
||||
*/
|
||||
const drawActorText = (content: string, g: any, x: number, y: number, width: number, height: number): void => {
|
||||
g.append('text')
|
||||
.attr('x', x + width / 2)
|
||||
.attr('y', y + height / 2)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'central')
|
||||
.text(content);
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a system boundary box with use cases inside
|
||||
*/
|
||||
const drawSystemBoundary = (g: any, boundary: SystemBoundaryPosition, conf: any): void => {
|
||||
// Determine boundary type from metadata (default to 'rect')
|
||||
const boundaryType = boundary.metadata?.type || 'rect';
|
||||
|
||||
if (boundaryType === 'package') {
|
||||
// Draw package-style boundary with title box
|
||||
const titleHeight = 25;
|
||||
const titleWidth = Math.max(100, boundary.name.length * 8 + 20);
|
||||
|
||||
// Draw main boundary rectangle
|
||||
g.append('rect')
|
||||
.attr('x', boundary.x)
|
||||
.attr('y', boundary.y + titleHeight)
|
||||
.attr('width', boundary.width)
|
||||
.attr('height', boundary.height - titleHeight)
|
||||
.attr('class', 'usecase-system-boundary')
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', '#333')
|
||||
.attr('stroke-width', 2);
|
||||
|
||||
// Draw title box
|
||||
g.append('rect')
|
||||
.attr('x', boundary.x)
|
||||
.attr('y', boundary.y)
|
||||
.attr('width', titleWidth)
|
||||
.attr('height', titleHeight)
|
||||
.attr('class', 'usecase-system-boundary')
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', '#333')
|
||||
.attr('stroke-width', 2);
|
||||
|
||||
// Draw title text
|
||||
g.append('text')
|
||||
.attr('x', boundary.x + titleWidth / 2)
|
||||
.attr('y', boundary.y + titleHeight / 2)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.style('font-size', '14px')
|
||||
.style('font-weight', 'bold')
|
||||
.style('font-family', 'Arial, sans-serif')
|
||||
.style('fill', '#333')
|
||||
.text(boundary.name);
|
||||
} else {
|
||||
// Draw rect-style boundary (default)
|
||||
g.append('rect')
|
||||
.attr('x', boundary.x)
|
||||
.attr('y', boundary.y)
|
||||
.attr('width', boundary.width)
|
||||
.attr('height', boundary.height)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', '#333')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('stroke-dasharray', '5,5');
|
||||
|
||||
// Draw boundary title
|
||||
g.append('text')
|
||||
.attr('x', boundary.x + 10)
|
||||
.attr('y', boundary.y + 20)
|
||||
.style('font-size', '16px')
|
||||
.style('font-weight', 'bold')
|
||||
.style('font-family', 'Arial, sans-serif')
|
||||
.style('fill', '#333')
|
||||
.text(boundary.name);
|
||||
}
|
||||
|
||||
// Draw use cases inside the boundary
|
||||
boundary.useCases.forEach((useCase) => {
|
||||
// Draw use case oval
|
||||
g.append('ellipse')
|
||||
.attr('cx', useCase.x + useCase.width / 2)
|
||||
.attr('cy', useCase.y + useCase.height / 2)
|
||||
.attr('rx', useCase.width / 2)
|
||||
.attr('ry', useCase.height / 2)
|
||||
.attr('class', 'usecase-usecase')
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', '#333');
|
||||
|
||||
// Draw use case text
|
||||
g.append('text')
|
||||
.attr('x', useCase.x + useCase.width / 2)
|
||||
.attr('y', useCase.y + useCase.height / 2)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'central')
|
||||
.text(useCase.name);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a standalone node as an oval
|
||||
*/
|
||||
const drawNode = (g: any, nodePos: NodePosition): void => {
|
||||
const nodeGroup = g.append('g').attr('class', `node-${nodePos.name}`);
|
||||
|
||||
// Draw oval background
|
||||
nodeGroup.append('ellipse')
|
||||
.attr('cx', nodePos.x + nodePos.width / 2)
|
||||
.attr('cy', nodePos.y + nodePos.height / 2)
|
||||
.attr('rx', nodePos.width / 2)
|
||||
.attr('ry', nodePos.height / 2)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', '#333')
|
||||
.attr('class', 'usecase-node');
|
||||
|
||||
// Add node label
|
||||
nodeGroup.append('text')
|
||||
.attr('x', nodePos.x + nodePos.width / 2)
|
||||
.attr('y', nodePos.y + nodePos.height / 2)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.text(nodePos.label);
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws an arrow relationship between entities (actor-to-usecase or actor-to-actor)
|
||||
*/
|
||||
const drawRelationship = (g: any, relationship: any, actorPositions: ActorPosition[], boundaryPositions: SystemBoundaryPosition[], conf: any): void => {
|
||||
// Find the source entity (always an actor)
|
||||
const fromEntity = actorPositions.find(a => a.name === relationship.from);
|
||||
if (!fromEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the target entity (could be a use case or another actor)
|
||||
let toEntity: UseCasePosition | ActorPosition | undefined;
|
||||
let isTargetUseCase = false;
|
||||
|
||||
// First check if target is a use case in system boundaries
|
||||
for (const boundary of boundaryPositions) {
|
||||
toEntity = boundary.useCases.find(uc => uc.name === relationship.to);
|
||||
if (toEntity) {
|
||||
isTargetUseCase = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If not found in boundaries, check if target is another actor
|
||||
toEntity ??= actorPositions.find(a => a.name === relationship.to);
|
||||
|
||||
if (!toEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate connection points
|
||||
const fromCenterX = fromEntity.x + fromEntity.width / 2;
|
||||
const fromCenterY = fromEntity.y + fromEntity.height / 2;
|
||||
|
||||
// For use cases, connect to the edge (left side), for actors connect to center
|
||||
const toCenterX = isTargetUseCase ? toEntity.x : toEntity.x + toEntity.width / 2;
|
||||
const toCenterY = isTargetUseCase ? toEntity.y + toEntity.height / 2 : toEntity.y + toEntity.height / 2;
|
||||
|
||||
// Draw arrow line
|
||||
g.append('line')
|
||||
.attr('x1', fromCenterX)
|
||||
.attr('y1', fromCenterY)
|
||||
.attr('x2', toCenterX)
|
||||
.attr('y2', toCenterY)
|
||||
.attr('class', 'usecase-arrow')
|
||||
.attr('stroke', '#333')
|
||||
.attr('marker-end', 'url(#arrowhead)');
|
||||
|
||||
// Add edge label if present
|
||||
if (relationship.label) {
|
||||
const midX = (fromCenterX + toCenterX) / 2;
|
||||
const midY = (fromCenterY + toCenterY) / 2;
|
||||
|
||||
g.append('text')
|
||||
.attr('x', midX)
|
||||
.attr('y', midY - 5)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('class', 'usecase-arrow-label')
|
||||
.attr('stroke', '#333')
|
||||
.attr('font-weight', 200)
|
||||
.text(relationship.label);
|
||||
}
|
||||
|
||||
// Add arrowhead marker definition if not already added
|
||||
const defs = g.select('defs').empty() ? g.append('defs') : g.select('defs');
|
||||
|
||||
if (defs.select('#arrowhead').empty()) {
|
||||
defs.append('marker')
|
||||
.attr('id', 'arrowhead')
|
||||
.attr('viewBox', '0 0 10 10')
|
||||
.attr('refX', 9)
|
||||
.attr('refY', 3)
|
||||
.attr('markerWidth', 6)
|
||||
.attr('markerHeight', 6)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M0,0 L0,6 L9,3 z')
|
||||
.attr('fill', '#333');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws an arrow relationship between an actor and a standalone node
|
||||
*/
|
||||
const drawNodeRelationship = (g: any, relationship: any, actorPositions: ActorPosition[], nodePositions: NodePosition[], conf: any): void => {
|
||||
// Find the actor position
|
||||
const actor = actorPositions.find(a => a.name === relationship.from);
|
||||
if (!actor) {return};
|
||||
|
||||
// Find the node position
|
||||
const node = nodePositions.find(n => n.name === relationship.to);
|
||||
if (!node) {return};
|
||||
|
||||
// Calculate connection points
|
||||
const actorCenterX = actor.x + actor.width / 2;
|
||||
const actorCenterY = actor.y + actor.height / 2;
|
||||
|
||||
// For nodes (which are like use cases), connect to the edge (left side)
|
||||
const nodeCenterX = node.x;
|
||||
const nodeCenterY = node.y + node.height / 2;
|
||||
|
||||
// Draw arrow line
|
||||
g.append('line')
|
||||
.attr('x1', actorCenterX)
|
||||
.attr('y1', actorCenterY)
|
||||
.attr('x2', nodeCenterX)
|
||||
.attr('y2', nodeCenterY)
|
||||
.attr('stroke', '#333')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('marker-end', 'url(#arrowhead)');
|
||||
|
||||
// Add edge label if present
|
||||
if (relationship.label) {
|
||||
const midX = (actorCenterX + nodeCenterX) / 2;
|
||||
const midY = (actorCenterY + nodeCenterY) / 2;
|
||||
|
||||
g.append('text')
|
||||
.attr('x', midX)
|
||||
.attr('y', midY - 5)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('font-size', '12px')
|
||||
.attr('font-family', 'Arial, sans-serif')
|
||||
.attr('fill', '#333')
|
||||
.text(relationship.label);
|
||||
}
|
||||
|
||||
// Add arrowhead marker definition if not already added
|
||||
const defs = g.select('defs').empty() ? g.append('defs') : g.select('defs');
|
||||
|
||||
if (defs.select('#arrowhead').empty()) {
|
||||
defs.append('marker')
|
||||
.attr('id', 'arrowhead')
|
||||
.attr('viewBox', '0 0 10 10')
|
||||
.attr('refX', 9)
|
||||
.attr('refY', 3)
|
||||
.attr('markerWidth', 6)
|
||||
.attr('markerHeight', 6)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M0,0 L0,6 L9,3 z')
|
||||
.attr('fill', '#333');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws an arrow relationship from an inline actor-node definition
|
||||
*/
|
||||
const drawInlineRelationship = (g: any, relationship: any, actorPositions: ActorPosition[], nodePositions: NodePosition[], conf: any): void => {
|
||||
// Find the actor position
|
||||
const actor = actorPositions.find(a => a.name === relationship.actor);
|
||||
if (!actor) {return};
|
||||
|
||||
// Find the node position by node ID
|
||||
const node = nodePositions.find(n => n.name === relationship.node.id);
|
||||
if (!node) {return};
|
||||
|
||||
// Calculate connection points
|
||||
const actorCenterX = actor.x + actor.width / 2;
|
||||
const actorCenterY = actor.y + actor.height / 2;
|
||||
|
||||
// For nodes (which are like use cases), connect to the edge (left side)
|
||||
const nodeCenterX = node.x;
|
||||
const nodeCenterY = node.y + node.height / 2;
|
||||
|
||||
// Draw arrow line
|
||||
g.append('line')
|
||||
.attr('x1', actorCenterX)
|
||||
.attr('y1', actorCenterY)
|
||||
.attr('x2', nodeCenterX)
|
||||
.attr('y2', nodeCenterY)
|
||||
.attr('stroke', '#333')
|
||||
.attr('stroke-width', 1)
|
||||
.attr('marker-end', 'url(#arrowhead)');
|
||||
|
||||
// Add edge label if present
|
||||
if (relationship.label) {
|
||||
const midX = (actorCenterX + nodeCenterX) / 2;
|
||||
const midY = (actorCenterY + nodeCenterY) / 2;
|
||||
|
||||
g.append('text')
|
||||
.attr('x', midX)
|
||||
.attr('y', midY - 5)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('font-size', '12px')
|
||||
.attr('font-family', 'Arial, sans-serif')
|
||||
.attr('fill', '#333')
|
||||
.text(relationship.label);
|
||||
}
|
||||
|
||||
// Add arrowhead marker definition if not already added
|
||||
const defs = g.select('defs').empty() ? g.append('defs') : g.select('defs');
|
||||
|
||||
if (defs.select('#arrowhead').empty()) {
|
||||
defs.append('marker')
|
||||
.attr('id', 'arrowhead')
|
||||
.attr('viewBox', '0 0 10 10')
|
||||
.attr('refX', 9)
|
||||
.attr('refY', 3)
|
||||
.attr('markerWidth', 6)
|
||||
.attr('markerHeight', 6)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M0,0 L0,6 L9,3 z')
|
||||
.attr('fill', '#333');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Main draw function for useCase diagrams
|
||||
*/
|
||||
const draw = (text: string, id: string, version: string, diagram: Diagram): void => {
|
||||
const db = diagram.db as UseCaseDB;
|
||||
|
||||
log.debug('Drawing useCase diagram', id);
|
||||
|
||||
const actors = db.getActors();
|
||||
const systemBoundaries = db.getSystemBoundaries();
|
||||
const useCases = db.getUseCases();
|
||||
const relationships = db.getRelationships();
|
||||
const nodes = db.getNodes();
|
||||
const nodeRelationships = db.getNodeRelationships();
|
||||
const inlineRelationships = db.getInlineRelationships();
|
||||
|
||||
// Create SVG container - use the same approach as other diagrams
|
||||
const svg = select(`[id="${id}"]`);
|
||||
svg.selectAll('*').remove();
|
||||
|
||||
if (actors.length === 0 && systemBoundaries.length === 0 && useCases.length === 0 && relationships.length === 0 && nodes.length === 0 && nodeRelationships.length === 0 && inlineRelationships.length === 0) {
|
||||
// Empty diagram
|
||||
svg.attr('width', 200);
|
||||
svg.attr('height', 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate layout
|
||||
let currentX = MARGIN;
|
||||
let currentY = MARGIN;
|
||||
let maxHeight = 0;
|
||||
|
||||
// Position actors
|
||||
const actorPositions: ActorPosition[] = actors.map((actor, index) => ({
|
||||
name: actor.name,
|
||||
x: currentX + index * ACTOR_SPACING,
|
||||
y: currentY,
|
||||
width: ACTOR_TYPE_WIDTH + 20, // Extra width for text
|
||||
height: ACTOR_HEIGHT,
|
||||
metadata: actor.metadata
|
||||
}));
|
||||
|
||||
if (actors.length > 0) {
|
||||
currentX += actors.length * ACTOR_SPACING;
|
||||
maxHeight = Math.max(maxHeight, ACTOR_HEIGHT + 50);
|
||||
}
|
||||
|
||||
// Position system boundaries
|
||||
const boundaryPositions: SystemBoundaryPosition[] = systemBoundaries.map((boundary, index) => {
|
||||
const boundaryWidth = Math.max(200, boundary.useCases.length * 120);
|
||||
const boundaryHeight = 150;
|
||||
|
||||
const position: SystemBoundaryPosition = {
|
||||
name: boundary.name,
|
||||
x: currentX + index * (boundaryWidth + 50),
|
||||
y: currentY,
|
||||
width: boundaryWidth,
|
||||
height: boundaryHeight,
|
||||
metadata: boundary.metadata,
|
||||
useCases: boundary.useCases.map((useCase, ucIndex) => ({
|
||||
name: useCase.name,
|
||||
x: currentX + index * (boundaryWidth + 50) + 20 + ucIndex * 100,
|
||||
y: currentY + 40,
|
||||
width: 80,
|
||||
height: 40
|
||||
}))
|
||||
};
|
||||
|
||||
return position;
|
||||
});
|
||||
|
||||
if (systemBoundaries.length > 0) {
|
||||
const totalBoundaryWidth = systemBoundaries.reduce((sum, boundary, index) => {
|
||||
const boundaryWidth = Math.max(200, boundary.useCases.length * 120);
|
||||
return sum + boundaryWidth + (index > 0 ? 50 : 0);
|
||||
}, 0);
|
||||
currentX += totalBoundaryWidth;
|
||||
maxHeight = Math.max(maxHeight, 150);
|
||||
}
|
||||
|
||||
// Position standalone nodes
|
||||
|
||||
const nodePositions: NodePosition[] = [];
|
||||
if (nodes.length > 0) {
|
||||
currentX += 50; // Add some spacing
|
||||
nodes.forEach((node, index) => {
|
||||
const nodeWidth = Math.max(100, node.label.length * 8);
|
||||
const nodeHeight = 40;
|
||||
|
||||
nodePositions.push({
|
||||
name: node.id,
|
||||
label: node.label,
|
||||
x: currentX,
|
||||
y: MARGIN + 50,
|
||||
width: nodeWidth,
|
||||
height: nodeHeight
|
||||
});
|
||||
|
||||
currentX += nodeWidth + 50;
|
||||
});
|
||||
maxHeight = Math.max(maxHeight, 90);
|
||||
}
|
||||
|
||||
// Create main group
|
||||
const g = svg.append('g').attr('class', 'usecase-diagram');
|
||||
|
||||
// Default configuration
|
||||
const conf = {
|
||||
actorFontSize: '14px',
|
||||
actorFontFamily: 'Arial, sans-serif',
|
||||
actorFontWeight: 'normal'
|
||||
};
|
||||
|
||||
// Draw all actors
|
||||
actorPositions.forEach((actorPos) => {
|
||||
const height = drawActorTypeActor(g, actorPos, conf);
|
||||
maxHeight = Math.max(maxHeight, height);
|
||||
});
|
||||
|
||||
// Draw system boundaries
|
||||
boundaryPositions.forEach((boundaryPos) => {
|
||||
drawSystemBoundary(g, boundaryPos, conf);
|
||||
});
|
||||
|
||||
// Draw standalone nodes
|
||||
nodePositions.forEach((nodePos) => {
|
||||
drawNode(g, nodePos);
|
||||
});
|
||||
|
||||
// Draw relationships (arrows)
|
||||
relationships.forEach((relationship) => {
|
||||
drawRelationship(g, relationship, actorPositions, boundaryPositions, conf);
|
||||
});
|
||||
|
||||
// Draw node relationships (arrows to standalone nodes)
|
||||
nodeRelationships.forEach((relationship) => {
|
||||
drawNodeRelationship(g, relationship, actorPositions, nodePositions, conf);
|
||||
});
|
||||
|
||||
// Draw inline relationships (from inline actor-node definitions)
|
||||
inlineRelationships.forEach((relationship) => {
|
||||
drawInlineRelationship(g, relationship, actorPositions, nodePositions, conf);
|
||||
});
|
||||
|
||||
// Calculate total dimensions
|
||||
let totalWidth = MARGIN;
|
||||
if (actors.length > 0) {
|
||||
totalWidth = Math.max(totalWidth, actorPositions[actorPositions.length - 1].x + actorPositions[actorPositions.length - 1].width + MARGIN);
|
||||
}
|
||||
if (systemBoundaries.length > 0) {
|
||||
totalWidth = Math.max(totalWidth, boundaryPositions[boundaryPositions.length - 1].x + boundaryPositions[boundaryPositions.length - 1].width + MARGIN);
|
||||
}
|
||||
if (nodePositions.length > 0) {
|
||||
totalWidth = Math.max(totalWidth, nodePositions[nodePositions.length - 1].x + nodePositions[nodePositions.length - 1].width + MARGIN);
|
||||
}
|
||||
|
||||
const totalHeight = MARGIN + maxHeight + MARGIN;
|
||||
|
||||
// Set SVG dimensions
|
||||
svg.attr('width', totalWidth);
|
||||
svg.attr('height', totalHeight);
|
||||
svg.attr('viewBox', `0 0 ${totalWidth} ${totalHeight}`);
|
||||
};
|
||||
|
||||
export default {
|
||||
draw,
|
||||
};
|
@@ -31,7 +31,7 @@
|
||||
"fast-glob": "^3.3.3",
|
||||
"https-localhost": "^4.7.1",
|
||||
"pathe": "^2.0.3",
|
||||
"unocss": "^66.0.0",
|
||||
"unocss": "^66.4.2",
|
||||
"unplugin-vue-components": "^28.4.0",
|
||||
"vite": "^6.1.1",
|
||||
"vite-plugin-pwa": "^1.0.0",
|
||||
|
@@ -590,11 +590,17 @@ flowchart TD
|
||||
- `b`
|
||||
- **w**: The width of the image. If not defined, this will default to the natural width of the image.
|
||||
- **h**: The height of the image. If not defined, this will default to the natural height of the image.
|
||||
- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the height (`h`) accordingly to the width (`w`). If not defined, this will default to `off` Possible values are:
|
||||
- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the width (`w`) accordingly to the height (`h`). If not defined, this will default to `off` Possible values are:
|
||||
- `on`
|
||||
- `off`
|
||||
|
||||
These new shapes provide additional flexibility and visual appeal to your flowcharts, making them more informative and engaging.
|
||||
If you want to resize an image, but keep the same aspect ratio, set `h`, and set `constraint: on` to constrain the aspect ratio. E.g.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
%% My image with a constrained aspect ratio
|
||||
A@{ img: "https://mermaid.js.org/favicon.svg", label: "My example image label", pos: "t", h: 60, constraint: "on" }
|
||||
```
|
||||
|
||||
## Links between nodes
|
||||
|
||||
|
@@ -46,6 +46,78 @@ sequenceDiagram
|
||||
Bob->>Alice: Hi Alice
|
||||
```
|
||||
|
||||
### Boundary
|
||||
|
||||
If you want to use the boundary symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "boundary" }
|
||||
participant Bob
|
||||
Alice->>Bob: Request from boundary
|
||||
Bob->>Alice: Response to boundary
|
||||
```
|
||||
|
||||
### Control
|
||||
|
||||
If you want to use the control symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "control" }
|
||||
participant Bob
|
||||
Alice->>Bob: Control request
|
||||
Bob->>Alice: Control response
|
||||
```
|
||||
|
||||
### Entity
|
||||
|
||||
If you want to use the entity symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "entity" }
|
||||
participant Bob
|
||||
Alice->>Bob: Entity request
|
||||
Bob->>Alice: Entity response
|
||||
```
|
||||
|
||||
### Database
|
||||
|
||||
If you want to use the database symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "database" }
|
||||
participant Bob
|
||||
Alice->>Bob: DB query
|
||||
Bob->>Alice: DB result
|
||||
```
|
||||
|
||||
### Collections
|
||||
|
||||
If you want to use the collections symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "collections" }
|
||||
participant Bob
|
||||
Alice->>Bob: Collections request
|
||||
Bob->>Alice: Collections response
|
||||
```
|
||||
|
||||
### Queue
|
||||
|
||||
If you want to use the queue symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "queue" }
|
||||
participant Bob
|
||||
Alice->>Bob: Queue message
|
||||
Bob->>Alice: Queue response
|
||||
```
|
||||
|
||||
### Aliases
|
||||
|
||||
The actor can have a convenient identifier and a descriptive label.
|
||||
|
@@ -41,7 +41,6 @@ import { decodeEntities, encodeEntities } from './utils.js';
|
||||
import { toBase64 } from './utils/base64.js';
|
||||
import { StateDB } from './diagrams/state/stateDb.js';
|
||||
import { ensureNodeFromSelector, jsdomIt } from './tests/util.js';
|
||||
import { select } from 'd3';
|
||||
import { JSDOM } from 'jsdom';
|
||||
|
||||
/**
|
||||
@@ -50,7 +49,6 @@ import { JSDOM } from 'jsdom';
|
||||
*/
|
||||
|
||||
// -------------------------------------------------------------------------------------
|
||||
|
||||
describe('mermaidAPI', () => {
|
||||
describe('encodeEntities', () => {
|
||||
it('removes the ending ; from style [text1]:[optional word]#[text2]; with ', () => {
|
||||
@@ -913,4 +911,241 @@ graph TD;A--x|text including URL space|B;`)
|
||||
expect(sequenceDiagram1.db.getActors()).not.toEqual(sequenceDiagram2.db.getActors());
|
||||
});
|
||||
});
|
||||
|
||||
describe('mermaidAPI config precedence', () => {
|
||||
const id = 'mermaid-config-test';
|
||||
|
||||
beforeEach(() => {
|
||||
mermaidAPI.globalReset();
|
||||
});
|
||||
|
||||
jsdomIt('renders with YAML config taking precedence over initialize config', async () => {
|
||||
mermaid.initialize({
|
||||
theme: 'forest',
|
||||
fontFamily: 'Arial',
|
||||
themeVariables: { fontFamily: 'Arial', fontSize: '16px' },
|
||||
flowchart: { htmlLabels: false },
|
||||
});
|
||||
|
||||
const diagramText = `---
|
||||
config:
|
||||
theme: base
|
||||
fontFamily: Courier
|
||||
themeVariables:
|
||||
fontFamily: "Courier New"
|
||||
fontSize: "20px"
|
||||
flowchart:
|
||||
htmlLabels: true
|
||||
---
|
||||
flowchart TD
|
||||
A --> B
|
||||
`;
|
||||
|
||||
const { svg } = await mermaidAPI.render('yaml-over-init', diagramText);
|
||||
|
||||
const config = mermaidAPI.getConfig();
|
||||
expect(config.theme).toBe('base');
|
||||
expect(config.fontFamily).toBe('Courier');
|
||||
expect(config.themeVariables.fontFamily).toBe('Courier New');
|
||||
expect(config.themeVariables.fontSize).toBe('20px');
|
||||
expect(config.flowchart?.htmlLabels).toBe(true);
|
||||
|
||||
const svgNode = ensureNodeFromSelector('svg', new JSDOM(svg).window.document);
|
||||
expect(svgNode).not.toBeNull();
|
||||
});
|
||||
|
||||
jsdomIt(
|
||||
'renders with YAML themeVariables fully overriding initialize themeVariables',
|
||||
async () => {
|
||||
mermaid.initialize({
|
||||
themeVariables: { fontFamily: 'Arial', fontSize: '16px' },
|
||||
});
|
||||
|
||||
const diagramText = `---
|
||||
config:
|
||||
themeVariables:
|
||||
fontFamily: "Courier New"
|
||||
fontSize: "20px"
|
||||
---
|
||||
flowchart TD
|
||||
A --> B
|
||||
`;
|
||||
|
||||
const { svg } = await mermaidAPI.render(id, diagramText);
|
||||
const config = mermaidAPI.getConfig();
|
||||
|
||||
expect(config.themeVariables.fontFamily).toBe('Courier New');
|
||||
expect(config.themeVariables.fontSize).toBe('20px');
|
||||
expect(config.themeVariables.fontFamily).not.toBe('Arial');
|
||||
expect(config.themeVariables.fontSize).not.toBe('16px');
|
||||
|
||||
const svgNode = ensureNodeFromSelector('svg', new JSDOM(svg).window.document);
|
||||
expect(svgNode).not.toBeNull();
|
||||
}
|
||||
);
|
||||
|
||||
jsdomIt(
|
||||
'renders with YAML themeVariables overriding only provided keys and keeping others from initialize',
|
||||
async () => {
|
||||
mermaid.initialize({
|
||||
theme: 'forest',
|
||||
fontFamily: 'Arial',
|
||||
themeVariables: { fontFamily: 'Arial', fontSize: '16px', colorPrimary: '#ff0000' },
|
||||
});
|
||||
|
||||
const diagramText = `---
|
||||
config:
|
||||
themeVariables:
|
||||
fontFamily: "Courier New"
|
||||
---
|
||||
flowchart TD
|
||||
A --> B
|
||||
`;
|
||||
|
||||
const { svg } = await mermaidAPI.render(id, diagramText);
|
||||
|
||||
const config = mermaidAPI.getConfig();
|
||||
expect(config.themeVariables.fontFamily).toBe('Courier New');
|
||||
expect(config.themeVariables.fontSize).toBe('16px');
|
||||
expect(config.themeVariables.colorPrimary).toBe('#ff0000');
|
||||
|
||||
const svgNode = ensureNodeFromSelector('svg', new JSDOM(svg).window.document);
|
||||
expect(svgNode).not.toBeNull();
|
||||
}
|
||||
);
|
||||
|
||||
jsdomIt(
|
||||
'renders with YAML config (no themeVariables) and falls back to initialize themeVariables',
|
||||
async () => {
|
||||
mermaid.initialize({
|
||||
themeVariables: { fontFamily: 'Arial', fontSize: '16px' },
|
||||
});
|
||||
|
||||
const diagramText = `---
|
||||
config:
|
||||
theme: base
|
||||
---
|
||||
flowchart TD
|
||||
A --> B
|
||||
`;
|
||||
|
||||
const { svg } = await mermaidAPI.render(id, diagramText);
|
||||
|
||||
const config = mermaidAPI.getConfig();
|
||||
expect(config.themeVariables.fontFamily).toBe('Arial');
|
||||
expect(config.themeVariables.fontSize).toBe('16px');
|
||||
expect(config.theme).toBe('base');
|
||||
|
||||
const svgNode = ensureNodeFromSelector('svg', new JSDOM(svg).window.document);
|
||||
expect(svgNode).not.toBeNull();
|
||||
}
|
||||
);
|
||||
|
||||
jsdomIt(
|
||||
'renders with full YAML config block taking full precedence over initialize config',
|
||||
async () => {
|
||||
mermaid.initialize({
|
||||
theme: 'forest',
|
||||
fontFamily: 'Arial',
|
||||
themeVariables: { fontFamily: 'Arial', fontSize: '16px' },
|
||||
flowchart: { htmlLabels: false },
|
||||
});
|
||||
|
||||
const diagramText = `---
|
||||
config:
|
||||
theme: base
|
||||
fontFamily: Courier
|
||||
themeVariables:
|
||||
fontFamily: "Courier New"
|
||||
fontSize: "20px"
|
||||
flowchart:
|
||||
htmlLabels: true
|
||||
---
|
||||
flowchart TD
|
||||
A --> B
|
||||
`;
|
||||
|
||||
const { svg } = await mermaidAPI.render('yaml-over-init', diagramText);
|
||||
|
||||
const config = mermaidAPI.getConfig();
|
||||
expect(config.theme).toBe('base');
|
||||
expect(config.fontFamily).toBe('Courier');
|
||||
expect(config.themeVariables.fontFamily).toBe('Courier New');
|
||||
expect(config.themeVariables.fontSize).toBe('20px');
|
||||
expect(config.flowchart?.htmlLabels).toBe(true);
|
||||
|
||||
const svgNode = ensureNodeFromSelector('svg', new JSDOM(svg).window.document);
|
||||
expect(svgNode).not.toBeNull();
|
||||
}
|
||||
);
|
||||
|
||||
jsdomIt(
|
||||
'renders with YAML config (no themeVariables) and falls back to initialize themeVariables (duplicate scenario)',
|
||||
async () => {
|
||||
mermaid.initialize({
|
||||
themeVariables: { fontFamily: 'Arial', fontSize: '16px' },
|
||||
});
|
||||
|
||||
const diagramText = `---
|
||||
config:
|
||||
theme: base
|
||||
---
|
||||
flowchart TD
|
||||
A --> B
|
||||
`;
|
||||
|
||||
await mermaidAPI.render(id, diagramText);
|
||||
|
||||
const config = mermaidAPI.getConfig();
|
||||
expect(config.themeVariables.fontFamily).toBe('Arial');
|
||||
expect(config.themeVariables.fontSize).toBe('16px');
|
||||
expect(config.theme).toBe('base');
|
||||
}
|
||||
);
|
||||
|
||||
jsdomIt('renders with no YAML config so initialize config is fully applied', async () => {
|
||||
mermaid.initialize({
|
||||
theme: 'forest',
|
||||
fontFamily: 'Arial',
|
||||
themeVariables: { fontFamily: 'Arial', fontSize: '16px' },
|
||||
});
|
||||
|
||||
const diagramText = `
|
||||
flowchart TD
|
||||
A --> B
|
||||
`;
|
||||
|
||||
await mermaidAPI.render(id, diagramText);
|
||||
|
||||
const config = mermaidAPI.getConfig();
|
||||
expect(config.theme).toBe('forest');
|
||||
expect(config.fontFamily).toBe('Arial');
|
||||
expect(config.themeVariables.fontFamily).toBe('Arial');
|
||||
expect(config.themeVariables.fontSize).toBe('16px');
|
||||
});
|
||||
|
||||
jsdomIt(
|
||||
'renders with empty YAML config block and falls back to initialize config',
|
||||
async () => {
|
||||
mermaid.initialize({
|
||||
theme: 'dark',
|
||||
themeVariables: { fontFamily: 'Times', fontSize: '14px' },
|
||||
});
|
||||
|
||||
const diagramText = `---
|
||||
config: {}
|
||||
---
|
||||
flowchart TD
|
||||
A --> B
|
||||
`;
|
||||
|
||||
await mermaidAPI.render(id, diagramText);
|
||||
|
||||
const config = mermaidAPI.getConfig();
|
||||
expect(config.theme).toBe('dark');
|
||||
expect(config.themeVariables.fontFamily).toBe('Times');
|
||||
expect(config.themeVariables.fontSize).toBe('14px');
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@@ -13,6 +13,18 @@ export interface NodeMetaData {
|
||||
ticket?: string;
|
||||
}
|
||||
|
||||
export interface ParticipantMetaData {
|
||||
type?:
|
||||
| 'actor'
|
||||
| 'participant'
|
||||
| 'boundary'
|
||||
| 'control'
|
||||
| 'entity'
|
||||
| 'database'
|
||||
| 'collections'
|
||||
| 'queue';
|
||||
}
|
||||
|
||||
export interface EdgeMetaData {
|
||||
animation?: 'fast' | 'slow';
|
||||
animate?: boolean;
|
||||
|
@@ -19,7 +19,9 @@
|
||||
"scripts": {
|
||||
"clean": "rimraf dist src/language/generated",
|
||||
"langium:generate": "langium generate",
|
||||
"langium:watch": "langium generate --watch"
|
||||
"langium:watch": "langium generate --watch",
|
||||
"antlr:generate": "antlr4ts -visitor -listener -o src/language/useCase/generated src/language/useCase/Usecase.g4",
|
||||
"generate": "npm run langium:generate && npm run antlr:generate"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -33,6 +35,8 @@
|
||||
"ast"
|
||||
],
|
||||
"dependencies": {
|
||||
"antlr4ts": "0.5.0-alpha.4",
|
||||
"antlr4ts-cli": "0.5.0-alpha.4",
|
||||
"langium": "3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@@ -45,3 +45,4 @@ export * from './pie/index.js';
|
||||
export * from './architecture/index.js';
|
||||
export * from './radar/index.js';
|
||||
export * from './treemap/index.js';
|
||||
export * from './useCase/index.js';
|
||||
|
70
packages/parser/src/language/useCase/.antlr/Usecase.interp
Normal file
70
packages/parser/src/language/useCase/.antlr/Usecase.interp
Normal file
File diff suppressed because one or more lines are too long
29
packages/parser/src/language/useCase/.antlr/Usecase.tokens
Normal file
29
packages/parser/src/language/useCase/.antlr/Usecase.tokens
Normal file
@@ -0,0 +1,29 @@
|
||||
USECASE_START=1
|
||||
ACTOR=2
|
||||
SYSTEM_BOUNDARY=3
|
||||
END=4
|
||||
ARROW=5
|
||||
LABELED_ARROW=6
|
||||
AT=7
|
||||
LBRACE=8
|
||||
RBRACE=9
|
||||
LPAREN=10
|
||||
RPAREN=11
|
||||
COMMA=12
|
||||
COLON=13
|
||||
STRING=14
|
||||
IDENTIFIER=15
|
||||
NEWLINE=16
|
||||
WS=17
|
||||
COMMENT=18
|
||||
'usecase'=1
|
||||
'actor'=2
|
||||
'systemBoundary'=3
|
||||
'end'=4
|
||||
'@'=7
|
||||
'{'=8
|
||||
'}'=9
|
||||
'('=10
|
||||
')'=11
|
||||
','=12
|
||||
':'=13
|
@@ -0,0 +1,71 @@
|
||||
token literal names:
|
||||
null
|
||||
'usecase'
|
||||
'actor'
|
||||
'systemBoundary'
|
||||
'end'
|
||||
null
|
||||
null
|
||||
'@'
|
||||
'{'
|
||||
'}'
|
||||
'('
|
||||
')'
|
||||
','
|
||||
':'
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
|
||||
token symbolic names:
|
||||
null
|
||||
USECASE_START
|
||||
ACTOR
|
||||
SYSTEM_BOUNDARY
|
||||
END
|
||||
ARROW
|
||||
LABELED_ARROW
|
||||
AT
|
||||
LBRACE
|
||||
RBRACE
|
||||
LPAREN
|
||||
RPAREN
|
||||
COMMA
|
||||
COLON
|
||||
STRING
|
||||
IDENTIFIER
|
||||
NEWLINE
|
||||
WS
|
||||
COMMENT
|
||||
|
||||
rule names:
|
||||
USECASE_START
|
||||
ACTOR
|
||||
SYSTEM_BOUNDARY
|
||||
END
|
||||
ARROW
|
||||
LABELED_ARROW
|
||||
AT
|
||||
LBRACE
|
||||
RBRACE
|
||||
LPAREN
|
||||
RPAREN
|
||||
COMMA
|
||||
COLON
|
||||
STRING
|
||||
IDENTIFIER
|
||||
NEWLINE
|
||||
WS
|
||||
COMMENT
|
||||
|
||||
channel names:
|
||||
DEFAULT_TOKEN_CHANNEL
|
||||
HIDDEN
|
||||
|
||||
mode names:
|
||||
DEFAULT_MODE
|
||||
|
||||
atn:
|
||||
[4, 0, 18, 154, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 3, 4, 76, 8, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 93, 8, 5, 1, 6, 1, 6, 1, 7, 1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 12, 1, 12, 1, 13, 1, 13, 5, 13, 111, 8, 13, 10, 13, 12, 13, 114, 9, 13, 1, 13, 1, 13, 1, 13, 5, 13, 119, 8, 13, 10, 13, 12, 13, 122, 9, 13, 1, 13, 3, 13, 125, 8, 13, 1, 14, 1, 14, 5, 14, 129, 8, 14, 10, 14, 12, 14, 132, 9, 14, 1, 15, 4, 15, 135, 8, 15, 11, 15, 12, 15, 136, 1, 16, 4, 16, 140, 8, 16, 11, 16, 12, 16, 141, 1, 16, 1, 16, 1, 17, 1, 17, 5, 17, 148, 8, 17, 10, 17, 12, 17, 151, 9, 17, 1, 17, 1, 17, 0, 0, 18, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, 23, 12, 25, 13, 27, 14, 29, 15, 31, 16, 33, 17, 35, 18, 1, 0, 6, 3, 0, 10, 10, 13, 13, 34, 34, 3, 0, 10, 10, 13, 13, 39, 39, 3, 0, 65, 90, 95, 95, 97, 122, 4, 0, 48, 57, 65, 90, 95, 95, 97, 122, 2, 0, 10, 10, 13, 13, 2, 0, 9, 9, 32, 32, 162, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0, 0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 1, 37, 1, 0, 0, 0, 3, 45, 1, 0, 0, 0, 5, 51, 1, 0, 0, 0, 7, 66, 1, 0, 0, 0, 9, 75, 1, 0, 0, 0, 11, 92, 1, 0, 0, 0, 13, 94, 1, 0, 0, 0, 15, 96, 1, 0, 0, 0, 17, 98, 1, 0, 0, 0, 19, 100, 1, 0, 0, 0, 21, 102, 1, 0, 0, 0, 23, 104, 1, 0, 0, 0, 25, 106, 1, 0, 0, 0, 27, 124, 1, 0, 0, 0, 29, 126, 1, 0, 0, 0, 31, 134, 1, 0, 0, 0, 33, 139, 1, 0, 0, 0, 35, 145, 1, 0, 0, 0, 37, 38, 5, 117, 0, 0, 38, 39, 5, 115, 0, 0, 39, 40, 5, 101, 0, 0, 40, 41, 5, 99, 0, 0, 41, 42, 5, 97, 0, 0, 42, 43, 5, 115, 0, 0, 43, 44, 5, 101, 0, 0, 44, 2, 1, 0, 0, 0, 45, 46, 5, 97, 0, 0, 46, 47, 5, 99, 0, 0, 47, 48, 5, 116, 0, 0, 48, 49, 5, 111, 0, 0, 49, 50, 5, 114, 0, 0, 50, 4, 1, 0, 0, 0, 51, 52, 5, 115, 0, 0, 52, 53, 5, 121, 0, 0, 53, 54, 5, 115, 0, 0, 54, 55, 5, 116, 0, 0, 55, 56, 5, 101, 0, 0, 56, 57, 5, 109, 0, 0, 57, 58, 5, 66, 0, 0, 58, 59, 5, 111, 0, 0, 59, 60, 5, 117, 0, 0, 60, 61, 5, 110, 0, 0, 61, 62, 5, 100, 0, 0, 62, 63, 5, 97, 0, 0, 63, 64, 5, 114, 0, 0, 64, 65, 5, 121, 0, 0, 65, 6, 1, 0, 0, 0, 66, 67, 5, 101, 0, 0, 67, 68, 5, 110, 0, 0, 68, 69, 5, 100, 0, 0, 69, 8, 1, 0, 0, 0, 70, 71, 5, 45, 0, 0, 71, 72, 5, 45, 0, 0, 72, 76, 5, 62, 0, 0, 73, 74, 5, 45, 0, 0, 74, 76, 5, 62, 0, 0, 75, 70, 1, 0, 0, 0, 75, 73, 1, 0, 0, 0, 76, 10, 1, 0, 0, 0, 77, 78, 5, 45, 0, 0, 78, 79, 5, 45, 0, 0, 79, 80, 1, 0, 0, 0, 80, 81, 3, 29, 14, 0, 81, 82, 5, 45, 0, 0, 82, 83, 5, 45, 0, 0, 83, 84, 5, 62, 0, 0, 84, 93, 1, 0, 0, 0, 85, 86, 5, 45, 0, 0, 86, 87, 5, 45, 0, 0, 87, 88, 1, 0, 0, 0, 88, 89, 3, 29, 14, 0, 89, 90, 5, 45, 0, 0, 90, 91, 5, 62, 0, 0, 91, 93, 1, 0, 0, 0, 92, 77, 1, 0, 0, 0, 92, 85, 1, 0, 0, 0, 93, 12, 1, 0, 0, 0, 94, 95, 5, 64, 0, 0, 95, 14, 1, 0, 0, 0, 96, 97, 5, 123, 0, 0, 97, 16, 1, 0, 0, 0, 98, 99, 5, 125, 0, 0, 99, 18, 1, 0, 0, 0, 100, 101, 5, 40, 0, 0, 101, 20, 1, 0, 0, 0, 102, 103, 5, 41, 0, 0, 103, 22, 1, 0, 0, 0, 104, 105, 5, 44, 0, 0, 105, 24, 1, 0, 0, 0, 106, 107, 5, 58, 0, 0, 107, 26, 1, 0, 0, 0, 108, 112, 5, 34, 0, 0, 109, 111, 8, 0, 0, 0, 110, 109, 1, 0, 0, 0, 111, 114, 1, 0, 0, 0, 112, 110, 1, 0, 0, 0, 112, 113, 1, 0, 0, 0, 113, 115, 1, 0, 0, 0, 114, 112, 1, 0, 0, 0, 115, 125, 5, 34, 0, 0, 116, 120, 5, 39, 0, 0, 117, 119, 8, 1, 0, 0, 118, 117, 1, 0, 0, 0, 119, 122, 1, 0, 0, 0, 120, 118, 1, 0, 0, 0, 120, 121, 1, 0, 0, 0, 121, 123, 1, 0, 0, 0, 122, 120, 1, 0, 0, 0, 123, 125, 5, 39, 0, 0, 124, 108, 1, 0, 0, 0, 124, 116, 1, 0, 0, 0, 125, 28, 1, 0, 0, 0, 126, 130, 7, 2, 0, 0, 127, 129, 7, 3, 0, 0, 128, 127, 1, 0, 0, 0, 129, 132, 1, 0, 0, 0, 130, 128, 1, 0, 0, 0, 130, 131, 1, 0, 0, 0, 131, 30, 1, 0, 0, 0, 132, 130, 1, 0, 0, 0, 133, 135, 7, 4, 0, 0, 134, 133, 1, 0, 0, 0, 135, 136, 1, 0, 0, 0, 136, 134, 1, 0, 0, 0, 136, 137, 1, 0, 0, 0, 137, 32, 1, 0, 0, 0, 138, 140, 7, 5, 0, 0, 139, 138, 1, 0, 0, 0, 140, 141, 1, 0, 0, 0, 141, 139, 1, 0, 0, 0, 141, 142, 1, 0, 0, 0, 142, 143, 1, 0, 0, 0, 143, 144, 6, 16, 0, 0, 144, 34, 1, 0, 0, 0, 145, 149, 5, 37, 0, 0, 146, 148, 8, 4, 0, 0, 147, 146, 1, 0, 0, 0, 148, 151, 1, 0, 0, 0, 149, 147, 1, 0, 0, 0, 149, 150, 1, 0, 0, 0, 150, 152, 1, 0, 0, 0, 151, 149, 1, 0, 0, 0, 152, 153, 6, 17, 0, 0, 153, 36, 1, 0, 0, 0, 10, 0, 75, 92, 112, 120, 124, 130, 136, 141, 149, 1, 6, 0, 0]
|
213
packages/parser/src/language/useCase/.antlr/UsecaseLexer.java
Normal file
213
packages/parser/src/language/useCase/.antlr/UsecaseLexer.java
Normal file
@@ -0,0 +1,213 @@
|
||||
// Generated from /home/omkar-kadam/Public/mermaid/mermaid/packages/parser/src/language/useCase/Usecase.g4 by ANTLR 4.13.1
|
||||
import org.antlr.v4.runtime.Lexer;
|
||||
import org.antlr.v4.runtime.CharStream;
|
||||
import org.antlr.v4.runtime.Token;
|
||||
import org.antlr.v4.runtime.TokenStream;
|
||||
import org.antlr.v4.runtime.*;
|
||||
import org.antlr.v4.runtime.atn.*;
|
||||
import org.antlr.v4.runtime.dfa.DFA;
|
||||
import org.antlr.v4.runtime.misc.*;
|
||||
|
||||
@SuppressWarnings({"all", "warnings", "unchecked", "unused", "cast", "CheckReturnValue", "this-escape"})
|
||||
public class UsecaseLexer extends Lexer {
|
||||
static { RuntimeMetaData.checkVersion("4.13.1", RuntimeMetaData.VERSION); }
|
||||
|
||||
protected static final DFA[] _decisionToDFA;
|
||||
protected static final PredictionContextCache _sharedContextCache =
|
||||
new PredictionContextCache();
|
||||
public static final int
|
||||
USECASE_START=1, ACTOR=2, SYSTEM_BOUNDARY=3, END=4, ARROW=5, LABELED_ARROW=6,
|
||||
AT=7, LBRACE=8, RBRACE=9, LPAREN=10, RPAREN=11, COMMA=12, COLON=13, STRING=14,
|
||||
IDENTIFIER=15, NEWLINE=16, WS=17, COMMENT=18;
|
||||
public static String[] channelNames = {
|
||||
"DEFAULT_TOKEN_CHANNEL", "HIDDEN"
|
||||
};
|
||||
|
||||
public static String[] modeNames = {
|
||||
"DEFAULT_MODE"
|
||||
};
|
||||
|
||||
private static String[] makeRuleNames() {
|
||||
return new String[] {
|
||||
"USECASE_START", "ACTOR", "SYSTEM_BOUNDARY", "END", "ARROW", "LABELED_ARROW",
|
||||
"AT", "LBRACE", "RBRACE", "LPAREN", "RPAREN", "COMMA", "COLON", "STRING",
|
||||
"IDENTIFIER", "NEWLINE", "WS", "COMMENT"
|
||||
};
|
||||
}
|
||||
public static final String[] ruleNames = makeRuleNames();
|
||||
|
||||
private static String[] makeLiteralNames() {
|
||||
return new String[] {
|
||||
null, "'usecase'", "'actor'", "'systemBoundary'", "'end'", null, null,
|
||||
"'@'", "'{'", "'}'", "'('", "')'", "','", "':'"
|
||||
};
|
||||
}
|
||||
private static final String[] _LITERAL_NAMES = makeLiteralNames();
|
||||
private static String[] makeSymbolicNames() {
|
||||
return new String[] {
|
||||
null, "USECASE_START", "ACTOR", "SYSTEM_BOUNDARY", "END", "ARROW", "LABELED_ARROW",
|
||||
"AT", "LBRACE", "RBRACE", "LPAREN", "RPAREN", "COMMA", "COLON", "STRING",
|
||||
"IDENTIFIER", "NEWLINE", "WS", "COMMENT"
|
||||
};
|
||||
}
|
||||
private static final String[] _SYMBOLIC_NAMES = makeSymbolicNames();
|
||||
public static final Vocabulary VOCABULARY = new VocabularyImpl(_LITERAL_NAMES, _SYMBOLIC_NAMES);
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #VOCABULARY} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public static final String[] tokenNames;
|
||||
static {
|
||||
tokenNames = new String[_SYMBOLIC_NAMES.length];
|
||||
for (int i = 0; i < tokenNames.length; i++) {
|
||||
tokenNames[i] = VOCABULARY.getLiteralName(i);
|
||||
if (tokenNames[i] == null) {
|
||||
tokenNames[i] = VOCABULARY.getSymbolicName(i);
|
||||
}
|
||||
|
||||
if (tokenNames[i] == null) {
|
||||
tokenNames[i] = "<INVALID>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public String[] getTokenNames() {
|
||||
return tokenNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
public Vocabulary getVocabulary() {
|
||||
return VOCABULARY;
|
||||
}
|
||||
|
||||
|
||||
public UsecaseLexer(CharStream input) {
|
||||
super(input);
|
||||
_interp = new LexerATNSimulator(this,_ATN,_decisionToDFA,_sharedContextCache);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGrammarFileName() { return "Usecase.g4"; }
|
||||
|
||||
@Override
|
||||
public String[] getRuleNames() { return ruleNames; }
|
||||
|
||||
@Override
|
||||
public String getSerializedATN() { return _serializedATN; }
|
||||
|
||||
@Override
|
||||
public String[] getChannelNames() { return channelNames; }
|
||||
|
||||
@Override
|
||||
public String[] getModeNames() { return modeNames; }
|
||||
|
||||
@Override
|
||||
public ATN getATN() { return _ATN; }
|
||||
|
||||
public static final String _serializedATN =
|
||||
"\u0004\u0000\u0012\u009a\u0006\uffff\uffff\u0002\u0000\u0007\u0000\u0002"+
|
||||
"\u0001\u0007\u0001\u0002\u0002\u0007\u0002\u0002\u0003\u0007\u0003\u0002"+
|
||||
"\u0004\u0007\u0004\u0002\u0005\u0007\u0005\u0002\u0006\u0007\u0006\u0002"+
|
||||
"\u0007\u0007\u0007\u0002\b\u0007\b\u0002\t\u0007\t\u0002\n\u0007\n\u0002"+
|
||||
"\u000b\u0007\u000b\u0002\f\u0007\f\u0002\r\u0007\r\u0002\u000e\u0007\u000e"+
|
||||
"\u0002\u000f\u0007\u000f\u0002\u0010\u0007\u0010\u0002\u0011\u0007\u0011"+
|
||||
"\u0001\u0000\u0001\u0000\u0001\u0000\u0001\u0000\u0001\u0000\u0001\u0000"+
|
||||
"\u0001\u0000\u0001\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001"+
|
||||
"\u0001\u0001\u0001\u0001\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002"+
|
||||
"\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002"+
|
||||
"\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0003"+
|
||||
"\u0001\u0003\u0001\u0003\u0001\u0003\u0001\u0004\u0001\u0004\u0001\u0004"+
|
||||
"\u0001\u0004\u0001\u0004\u0003\u0004L\b\u0004\u0001\u0005\u0001\u0005"+
|
||||
"\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0005"+
|
||||
"\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0005"+
|
||||
"\u0001\u0005\u0003\u0005]\b\u0005\u0001\u0006\u0001\u0006\u0001\u0007"+
|
||||
"\u0001\u0007\u0001\b\u0001\b\u0001\t\u0001\t\u0001\n\u0001\n\u0001\u000b"+
|
||||
"\u0001\u000b\u0001\f\u0001\f\u0001\r\u0001\r\u0005\ro\b\r\n\r\f\rr\t\r"+
|
||||
"\u0001\r\u0001\r\u0001\r\u0005\rw\b\r\n\r\f\rz\t\r\u0001\r\u0003\r}\b"+
|
||||
"\r\u0001\u000e\u0001\u000e\u0005\u000e\u0081\b\u000e\n\u000e\f\u000e\u0084"+
|
||||
"\t\u000e\u0001\u000f\u0004\u000f\u0087\b\u000f\u000b\u000f\f\u000f\u0088"+
|
||||
"\u0001\u0010\u0004\u0010\u008c\b\u0010\u000b\u0010\f\u0010\u008d\u0001"+
|
||||
"\u0010\u0001\u0010\u0001\u0011\u0001\u0011\u0005\u0011\u0094\b\u0011\n"+
|
||||
"\u0011\f\u0011\u0097\t\u0011\u0001\u0011\u0001\u0011\u0000\u0000\u0012"+
|
||||
"\u0001\u0001\u0003\u0002\u0005\u0003\u0007\u0004\t\u0005\u000b\u0006\r"+
|
||||
"\u0007\u000f\b\u0011\t\u0013\n\u0015\u000b\u0017\f\u0019\r\u001b\u000e"+
|
||||
"\u001d\u000f\u001f\u0010!\u0011#\u0012\u0001\u0000\u0006\u0003\u0000\n"+
|
||||
"\n\r\r\"\"\u0003\u0000\n\n\r\r\'\'\u0003\u0000AZ__az\u0004\u000009AZ_"+
|
||||
"_az\u0002\u0000\n\n\r\r\u0002\u0000\t\t \u00a2\u0000\u0001\u0001\u0000"+
|
||||
"\u0000\u0000\u0000\u0003\u0001\u0000\u0000\u0000\u0000\u0005\u0001\u0000"+
|
||||
"\u0000\u0000\u0000\u0007\u0001\u0000\u0000\u0000\u0000\t\u0001\u0000\u0000"+
|
||||
"\u0000\u0000\u000b\u0001\u0000\u0000\u0000\u0000\r\u0001\u0000\u0000\u0000"+
|
||||
"\u0000\u000f\u0001\u0000\u0000\u0000\u0000\u0011\u0001\u0000\u0000\u0000"+
|
||||
"\u0000\u0013\u0001\u0000\u0000\u0000\u0000\u0015\u0001\u0000\u0000\u0000"+
|
||||
"\u0000\u0017\u0001\u0000\u0000\u0000\u0000\u0019\u0001\u0000\u0000\u0000"+
|
||||
"\u0000\u001b\u0001\u0000\u0000\u0000\u0000\u001d\u0001\u0000\u0000\u0000"+
|
||||
"\u0000\u001f\u0001\u0000\u0000\u0000\u0000!\u0001\u0000\u0000\u0000\u0000"+
|
||||
"#\u0001\u0000\u0000\u0000\u0001%\u0001\u0000\u0000\u0000\u0003-\u0001"+
|
||||
"\u0000\u0000\u0000\u00053\u0001\u0000\u0000\u0000\u0007B\u0001\u0000\u0000"+
|
||||
"\u0000\tK\u0001\u0000\u0000\u0000\u000b\\\u0001\u0000\u0000\u0000\r^\u0001"+
|
||||
"\u0000\u0000\u0000\u000f`\u0001\u0000\u0000\u0000\u0011b\u0001\u0000\u0000"+
|
||||
"\u0000\u0013d\u0001\u0000\u0000\u0000\u0015f\u0001\u0000\u0000\u0000\u0017"+
|
||||
"h\u0001\u0000\u0000\u0000\u0019j\u0001\u0000\u0000\u0000\u001b|\u0001"+
|
||||
"\u0000\u0000\u0000\u001d~\u0001\u0000\u0000\u0000\u001f\u0086\u0001\u0000"+
|
||||
"\u0000\u0000!\u008b\u0001\u0000\u0000\u0000#\u0091\u0001\u0000\u0000\u0000"+
|
||||
"%&\u0005u\u0000\u0000&\'\u0005s\u0000\u0000\'(\u0005e\u0000\u0000()\u0005"+
|
||||
"c\u0000\u0000)*\u0005a\u0000\u0000*+\u0005s\u0000\u0000+,\u0005e\u0000"+
|
||||
"\u0000,\u0002\u0001\u0000\u0000\u0000-.\u0005a\u0000\u0000./\u0005c\u0000"+
|
||||
"\u0000/0\u0005t\u0000\u000001\u0005o\u0000\u000012\u0005r\u0000\u0000"+
|
||||
"2\u0004\u0001\u0000\u0000\u000034\u0005s\u0000\u000045\u0005y\u0000\u0000"+
|
||||
"56\u0005s\u0000\u000067\u0005t\u0000\u000078\u0005e\u0000\u000089\u0005"+
|
||||
"m\u0000\u00009:\u0005B\u0000\u0000:;\u0005o\u0000\u0000;<\u0005u\u0000"+
|
||||
"\u0000<=\u0005n\u0000\u0000=>\u0005d\u0000\u0000>?\u0005a\u0000\u0000"+
|
||||
"?@\u0005r\u0000\u0000@A\u0005y\u0000\u0000A\u0006\u0001\u0000\u0000\u0000"+
|
||||
"BC\u0005e\u0000\u0000CD\u0005n\u0000\u0000DE\u0005d\u0000\u0000E\b\u0001"+
|
||||
"\u0000\u0000\u0000FG\u0005-\u0000\u0000GH\u0005-\u0000\u0000HL\u0005>"+
|
||||
"\u0000\u0000IJ\u0005-\u0000\u0000JL\u0005>\u0000\u0000KF\u0001\u0000\u0000"+
|
||||
"\u0000KI\u0001\u0000\u0000\u0000L\n\u0001\u0000\u0000\u0000MN\u0005-\u0000"+
|
||||
"\u0000NO\u0005-\u0000\u0000OP\u0001\u0000\u0000\u0000PQ\u0003\u001d\u000e"+
|
||||
"\u0000QR\u0005-\u0000\u0000RS\u0005-\u0000\u0000ST\u0005>\u0000\u0000"+
|
||||
"T]\u0001\u0000\u0000\u0000UV\u0005-\u0000\u0000VW\u0005-\u0000\u0000W"+
|
||||
"X\u0001\u0000\u0000\u0000XY\u0003\u001d\u000e\u0000YZ\u0005-\u0000\u0000"+
|
||||
"Z[\u0005>\u0000\u0000[]\u0001\u0000\u0000\u0000\\M\u0001\u0000\u0000\u0000"+
|
||||
"\\U\u0001\u0000\u0000\u0000]\f\u0001\u0000\u0000\u0000^_\u0005@\u0000"+
|
||||
"\u0000_\u000e\u0001\u0000\u0000\u0000`a\u0005{\u0000\u0000a\u0010\u0001"+
|
||||
"\u0000\u0000\u0000bc\u0005}\u0000\u0000c\u0012\u0001\u0000\u0000\u0000"+
|
||||
"de\u0005(\u0000\u0000e\u0014\u0001\u0000\u0000\u0000fg\u0005)\u0000\u0000"+
|
||||
"g\u0016\u0001\u0000\u0000\u0000hi\u0005,\u0000\u0000i\u0018\u0001\u0000"+
|
||||
"\u0000\u0000jk\u0005:\u0000\u0000k\u001a\u0001\u0000\u0000\u0000lp\u0005"+
|
||||
"\"\u0000\u0000mo\b\u0000\u0000\u0000nm\u0001\u0000\u0000\u0000or\u0001"+
|
||||
"\u0000\u0000\u0000pn\u0001\u0000\u0000\u0000pq\u0001\u0000\u0000\u0000"+
|
||||
"qs\u0001\u0000\u0000\u0000rp\u0001\u0000\u0000\u0000s}\u0005\"\u0000\u0000"+
|
||||
"tx\u0005\'\u0000\u0000uw\b\u0001\u0000\u0000vu\u0001\u0000\u0000\u0000"+
|
||||
"wz\u0001\u0000\u0000\u0000xv\u0001\u0000\u0000\u0000xy\u0001\u0000\u0000"+
|
||||
"\u0000y{\u0001\u0000\u0000\u0000zx\u0001\u0000\u0000\u0000{}\u0005\'\u0000"+
|
||||
"\u0000|l\u0001\u0000\u0000\u0000|t\u0001\u0000\u0000\u0000}\u001c\u0001"+
|
||||
"\u0000\u0000\u0000~\u0082\u0007\u0002\u0000\u0000\u007f\u0081\u0007\u0003"+
|
||||
"\u0000\u0000\u0080\u007f\u0001\u0000\u0000\u0000\u0081\u0084\u0001\u0000"+
|
||||
"\u0000\u0000\u0082\u0080\u0001\u0000\u0000\u0000\u0082\u0083\u0001\u0000"+
|
||||
"\u0000\u0000\u0083\u001e\u0001\u0000\u0000\u0000\u0084\u0082\u0001\u0000"+
|
||||
"\u0000\u0000\u0085\u0087\u0007\u0004\u0000\u0000\u0086\u0085\u0001\u0000"+
|
||||
"\u0000\u0000\u0087\u0088\u0001\u0000\u0000\u0000\u0088\u0086\u0001\u0000"+
|
||||
"\u0000\u0000\u0088\u0089\u0001\u0000\u0000\u0000\u0089 \u0001\u0000\u0000"+
|
||||
"\u0000\u008a\u008c\u0007\u0005\u0000\u0000\u008b\u008a\u0001\u0000\u0000"+
|
||||
"\u0000\u008c\u008d\u0001\u0000\u0000\u0000\u008d\u008b\u0001\u0000\u0000"+
|
||||
"\u0000\u008d\u008e\u0001\u0000\u0000\u0000\u008e\u008f\u0001\u0000\u0000"+
|
||||
"\u0000\u008f\u0090\u0006\u0010\u0000\u0000\u0090\"\u0001\u0000\u0000\u0000"+
|
||||
"\u0091\u0095\u0005%\u0000\u0000\u0092\u0094\b\u0004\u0000\u0000\u0093"+
|
||||
"\u0092\u0001\u0000\u0000\u0000\u0094\u0097\u0001\u0000\u0000\u0000\u0095"+
|
||||
"\u0093\u0001\u0000\u0000\u0000\u0095\u0096\u0001\u0000\u0000\u0000\u0096"+
|
||||
"\u0098\u0001\u0000\u0000\u0000\u0097\u0095\u0001\u0000\u0000\u0000\u0098"+
|
||||
"\u0099\u0006\u0011\u0000\u0000\u0099$\u0001\u0000\u0000\u0000\n\u0000"+
|
||||
"K\\px|\u0082\u0088\u008d\u0095\u0001\u0006\u0000\u0000";
|
||||
public static final ATN _ATN =
|
||||
new ATNDeserializer().deserialize(_serializedATN.toCharArray());
|
||||
static {
|
||||
_decisionToDFA = new DFA[_ATN.getNumberOfDecisions()];
|
||||
for (int i = 0; i < _ATN.getNumberOfDecisions(); i++) {
|
||||
_decisionToDFA[i] = new DFA(_ATN.getDecisionState(i), i);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
USECASE_START=1
|
||||
ACTOR=2
|
||||
SYSTEM_BOUNDARY=3
|
||||
END=4
|
||||
ARROW=5
|
||||
LABELED_ARROW=6
|
||||
AT=7
|
||||
LBRACE=8
|
||||
RBRACE=9
|
||||
LPAREN=10
|
||||
RPAREN=11
|
||||
COMMA=12
|
||||
COLON=13
|
||||
STRING=14
|
||||
IDENTIFIER=15
|
||||
NEWLINE=16
|
||||
WS=17
|
||||
COMMENT=18
|
||||
'usecase'=1
|
||||
'actor'=2
|
||||
'systemBoundary'=3
|
||||
'end'=4
|
||||
'@'=7
|
||||
'{'=8
|
||||
'}'=9
|
||||
'('=10
|
||||
')'=11
|
||||
','=12
|
||||
':'=13
|
1574
packages/parser/src/language/useCase/.antlr/UsecaseParser.java
Normal file
1574
packages/parser/src/language/useCase/.antlr/UsecaseParser.java
Normal file
File diff suppressed because it is too large
Load Diff
184
packages/parser/src/language/useCase/Usecase.g4
Normal file
184
packages/parser/src/language/useCase/Usecase.g4
Normal file
@@ -0,0 +1,184 @@
|
||||
grammar Usecase;
|
||||
|
||||
// Parser rules
|
||||
usecaseDiagram
|
||||
: USECASE_START NEWLINE* statement* EOF
|
||||
;
|
||||
|
||||
statement
|
||||
: actor NEWLINE*
|
||||
| systemBoundary NEWLINE*
|
||||
| systemBoundaryMetadata NEWLINE*
|
||||
| useCase NEWLINE*
|
||||
| relationship NEWLINE*
|
||||
| actorRelationship NEWLINE*
|
||||
| NEWLINE
|
||||
;
|
||||
|
||||
relationship
|
||||
: actorName ARROW target
|
||||
| actorName LABELED_ARROW target
|
||||
;
|
||||
|
||||
actorRelationship
|
||||
: ACTOR actorName ARROW target
|
||||
| ACTOR actorName LABELED_ARROW target
|
||||
;
|
||||
|
||||
target
|
||||
: useCaseName
|
||||
| nodeDefinition
|
||||
;
|
||||
|
||||
nodeDefinition
|
||||
: nodeId LPAREN nodeLabel RPAREN
|
||||
;
|
||||
|
||||
nodeId
|
||||
: IDENTIFIER
|
||||
;
|
||||
|
||||
nodeLabel
|
||||
: IDENTIFIER (WS IDENTIFIER)*
|
||||
| STRING
|
||||
;
|
||||
|
||||
actorName
|
||||
: IDENTIFIER
|
||||
;
|
||||
|
||||
systemBoundary
|
||||
: SYSTEM_BOUNDARY boundaryName LBRACE NEWLINE* boundaryContent* RBRACE
|
||||
| SYSTEM_BOUNDARY boundaryName NEWLINE* boundaryContent* END
|
||||
;
|
||||
|
||||
systemBoundaryMetadata
|
||||
: boundaryName AT LBRACE metadataContent RBRACE
|
||||
;
|
||||
|
||||
boundaryContent
|
||||
: useCase NEWLINE*
|
||||
| NEWLINE
|
||||
;
|
||||
|
||||
useCase
|
||||
: useCaseName
|
||||
;
|
||||
|
||||
boundaryName
|
||||
: IDENTIFIER
|
||||
;
|
||||
|
||||
useCaseName
|
||||
: IDENTIFIER
|
||||
;
|
||||
|
||||
actor
|
||||
: ACTOR actorList
|
||||
;
|
||||
|
||||
actorList
|
||||
: actorDefinition (COMMA actorDefinition)*
|
||||
;
|
||||
|
||||
actorDefinition
|
||||
: actorName metadata?
|
||||
;
|
||||
|
||||
metadata
|
||||
: AT LBRACE metadataContent RBRACE
|
||||
;
|
||||
|
||||
metadataContent
|
||||
: metadataPair (COMMA metadataPair)*
|
||||
|
|
||||
;
|
||||
|
||||
metadataPair
|
||||
: metadataKey COLON metadataValue
|
||||
;
|
||||
|
||||
metadataKey
|
||||
: IDENTIFIER
|
||||
;
|
||||
|
||||
metadataValue
|
||||
: STRING
|
||||
| IDENTIFIER
|
||||
;
|
||||
|
||||
// Lexer rules
|
||||
USECASE_START
|
||||
: 'usecase'
|
||||
;
|
||||
|
||||
ACTOR
|
||||
: 'actor'
|
||||
;
|
||||
|
||||
SYSTEM_BOUNDARY
|
||||
: 'systemBoundary'
|
||||
;
|
||||
|
||||
END
|
||||
: 'end'
|
||||
;
|
||||
|
||||
ARROW
|
||||
: '-->'
|
||||
| '->'
|
||||
;
|
||||
|
||||
LABELED_ARROW
|
||||
: '--' IDENTIFIER '-->'
|
||||
| '--' IDENTIFIER '->'
|
||||
;
|
||||
|
||||
AT
|
||||
: '@'
|
||||
;
|
||||
|
||||
LBRACE
|
||||
: '{'
|
||||
;
|
||||
|
||||
RBRACE
|
||||
: '}'
|
||||
;
|
||||
|
||||
LPAREN
|
||||
: '('
|
||||
;
|
||||
|
||||
RPAREN
|
||||
: ')'
|
||||
;
|
||||
|
||||
COMMA
|
||||
: ','
|
||||
;
|
||||
|
||||
COLON
|
||||
: ':'
|
||||
;
|
||||
|
||||
STRING
|
||||
: '"' (~["\r\n])* '"'
|
||||
| '\'' (~['\r\n])* '\''
|
||||
;
|
||||
|
||||
IDENTIFIER
|
||||
: [a-zA-Z_][a-zA-Z0-9_]*
|
||||
;
|
||||
|
||||
NEWLINE
|
||||
: [\r\n]+
|
||||
;
|
||||
|
||||
WS
|
||||
: [ \t]+ -> skip
|
||||
;
|
||||
|
||||
COMMENT
|
||||
: '%' ~[\r\n]* -> skip
|
||||
;
|
2
packages/parser/src/language/useCase/index.ts
Normal file
2
packages/parser/src/language/useCase/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { parseUsecase } from './usecaseParser.js';
|
||||
export * from './usecaseTypes.js';
|
387
packages/parser/src/language/useCase/test.ts
Normal file
387
packages/parser/src/language/useCase/test.ts
Normal file
@@ -0,0 +1,387 @@
|
||||
import { parseUsecase } from './usecaseParser.js';
|
||||
|
||||
// Test basic usecase diagram parsing
|
||||
function testBasicUsecaseParsing() {
|
||||
const input = `usecase
|
||||
actor Developer1
|
||||
actor Developer2
|
||||
actor Developer3`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Basic Usecase Parsing:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test simple usecase diagram
|
||||
function testSimpleUsecaseParsing() {
|
||||
const input = `usecase
|
||||
actor User
|
||||
actor Admin`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Simple Usecase Parsing:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test metadata parsing
|
||||
function testMetadataParsing() {
|
||||
const input = `usecase
|
||||
actor Developer1@{ icon : 'icon_name', place: "sample place" }`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Metadata Parsing:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test complex metadata parsing
|
||||
function testComplexMetadataParsing() {
|
||||
const input = `usecase
|
||||
actor Developer1@{ icon : 'icon_name', type : 'hollow', place: "sample place", material:"sample" }`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Complex Metadata Parsing:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test mixed actors (with and without metadata)
|
||||
function testMixedActorsParsing() {
|
||||
const input = `usecase
|
||||
actor User
|
||||
actor Developer1@{ icon : 'dev_icon' }
|
||||
actor Admin@{ type: 'admin', place: "office" }`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Mixed Actors Parsing:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test multiple actors in single line
|
||||
function testMultipleActorsSingleLine() {
|
||||
const input = `usecase
|
||||
actor Developer1, Developer2, Developer3`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Multiple Actors Single Line:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test multiple actors with metadata
|
||||
function testMultipleActorsWithMetadata() {
|
||||
const input = `usecase
|
||||
actor Developer1@{ icon: 'dev' }, Developer2, Developer3@{ type: 'admin' }`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Multiple Actors With Metadata:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test five actors in single line
|
||||
function testFiveActorsSingleLine() {
|
||||
const input = `usecase
|
||||
actor Developer1, Developer2, Developer3, Developer4, Developer5`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Five Actors Single Line:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test system boundary parsing
|
||||
function testSystemBoundaryParsing() {
|
||||
const input = `usecase
|
||||
actor Developer1
|
||||
systemBoundary Tasks
|
||||
coding
|
||||
testing
|
||||
deploying
|
||||
end`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test System Boundary Parsing:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test mixed actors and system boundaries
|
||||
function testMixedActorsAndBoundaries() {
|
||||
const input = `usecase
|
||||
actor Developer1, Developer2
|
||||
systemBoundary Tasks
|
||||
coding
|
||||
testing
|
||||
end
|
||||
actor Admin`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Mixed Actors and Boundaries:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test curly brace system boundary parsing
|
||||
function testCurlyBraceSystemBoundary() {
|
||||
const input = `usecase
|
||||
actor Developer1
|
||||
systemBoundary Tasks {
|
||||
playing
|
||||
reviewing
|
||||
}`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Curly Brace System Boundary:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test relationship parsing
|
||||
function testRelationshipParsing() {
|
||||
const input = `usecase
|
||||
actor Developer1
|
||||
systemBoundary Tasks {
|
||||
playing
|
||||
reviewing
|
||||
}
|
||||
Developer1 --> playing
|
||||
Developer1 --> reviewing`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Relationship Parsing:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test complete example
|
||||
function testCompleteExample() {
|
||||
const input = `usecase
|
||||
actor Developer1
|
||||
systemBoundary Tasks {
|
||||
playing
|
||||
reviewing
|
||||
}
|
||||
Developer1 --> playing
|
||||
Developer1 --> reviewing`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Complete Example:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test node definitions
|
||||
function testNodeDefinitions() {
|
||||
const input = `usecase
|
||||
actor Tester1
|
||||
Tester1 --> c(Go through testing)`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Node Definitions:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test inline actor-node relationships
|
||||
function testInlineActorNodeRelationships() {
|
||||
const input = `usecase
|
||||
actor Developer1 --> a(Go through code)
|
||||
actor Developer2 --> b(Go through implementation)`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Inline Actor-Node Relationships:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test mixed syntax
|
||||
function testMixedSyntax() {
|
||||
const input = `usecase
|
||||
actor Tester1
|
||||
Tester1 --> c(Go through testing)
|
||||
actor Developer1 --> a(Go through code)
|
||||
actor Developer2 --> b(Go through implementation)`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Mixed Syntax:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test edge labels
|
||||
function testEdgeLabels() {
|
||||
const input = `usecase
|
||||
actor Developer1
|
||||
Developer1 --task2--> c(Go through testing)`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Edge Labels:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test edge labels with inline syntax
|
||||
function testInlineEdgeLabels() {
|
||||
const input = `usecase
|
||||
actor Developer1 --task1--> a(Go through code)`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Inline Edge Labels:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test mixed edge labels and regular arrows
|
||||
function testMixedEdgeLabels() {
|
||||
const input = `usecase
|
||||
actor Developer1
|
||||
actor Tester1
|
||||
Developer1 --task1--> a(Go through code)
|
||||
Tester1 --> b(Go through testing)`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Mixed Edge Labels:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Run tests
|
||||
console.log('Running Usecase Parser Tests...\n');
|
||||
testBasicUsecaseParsing();
|
||||
testSimpleUsecaseParsing();
|
||||
testMetadataParsing();
|
||||
testComplexMetadataParsing();
|
||||
testMixedActorsParsing();
|
||||
testMultipleActorsSingleLine();
|
||||
testMultipleActorsWithMetadata();
|
||||
testFiveActorsSingleLine();
|
||||
testSystemBoundaryParsing();
|
||||
testMixedActorsAndBoundaries();
|
||||
testCurlyBraceSystemBoundary();
|
||||
testRelationshipParsing();
|
||||
testCompleteExample();
|
||||
testNodeDefinitions();
|
||||
testInlineActorNodeRelationships();
|
||||
testMixedSyntax();
|
||||
testEdgeLabels();
|
||||
testInlineEdgeLabels();
|
||||
testMixedEdgeLabels();
|
||||
console.log('Tests completed.');
|
752
packages/parser/src/language/useCase/usecaseParser.ts
Normal file
752
packages/parser/src/language/useCase/usecaseParser.ts
Normal file
@@ -0,0 +1,752 @@
|
||||
// Simple tokenizer and parser for usecase diagrams
|
||||
// This approach is more compatible with the mermaid build system
|
||||
import type {
|
||||
UsecaseDiagram,
|
||||
Statement,
|
||||
Actor,
|
||||
Usecase,
|
||||
SystemBoundary,
|
||||
SystemBoundaryMetadata,
|
||||
ActorUseCaseRelationship,
|
||||
Node,
|
||||
ActorNodeRelationship,
|
||||
InlineActorNodeRelationship,
|
||||
ParseResult
|
||||
} from './usecaseTypes.js';
|
||||
|
||||
// Token types
|
||||
enum TokenType {
|
||||
USECASE_START = 'USECASE_START',
|
||||
ACTOR = 'ACTOR',
|
||||
SYSTEM_BOUNDARY = 'SYSTEM_BOUNDARY',
|
||||
END = 'END',
|
||||
ARROW = 'ARROW',
|
||||
LABELED_ARROW = 'LABELED_ARROW',
|
||||
AT = 'AT',
|
||||
LBRACE = 'LBRACE',
|
||||
RBRACE = 'RBRACE',
|
||||
LPAREN = 'LPAREN',
|
||||
RPAREN = 'RPAREN',
|
||||
COMMA = 'COMMA',
|
||||
COLON = 'COLON',
|
||||
STRING = 'STRING',
|
||||
IDENTIFIER = 'IDENTIFIER',
|
||||
NEWLINE = 'NEWLINE',
|
||||
EOF = 'EOF'
|
||||
}
|
||||
|
||||
interface Token {
|
||||
type: TokenType;
|
||||
value: string;
|
||||
line: number;
|
||||
column: number;
|
||||
}
|
||||
|
||||
class UsecaseLexer {
|
||||
private input: string;
|
||||
private position: number = 0;
|
||||
private line: number = 1;
|
||||
private column: number = 1;
|
||||
|
||||
constructor(input: string) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
tokenize(): Token[] {
|
||||
const tokens: Token[] = [];
|
||||
|
||||
while (this.position < this.input.length) {
|
||||
this.skipWhitespace();
|
||||
|
||||
if (this.position >= this.input.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
const token = this.nextToken();
|
||||
if (token) {
|
||||
tokens.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
tokens.push({
|
||||
type: TokenType.EOF,
|
||||
value: '',
|
||||
line: this.line,
|
||||
column: this.column
|
||||
});
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private nextToken(): Token | null {
|
||||
const startLine = this.line;
|
||||
const startColumn = this.column;
|
||||
|
||||
// Skip comments
|
||||
if (this.peek() === '%') {
|
||||
this.skipComment();
|
||||
return null;
|
||||
}
|
||||
|
||||
// Newlines
|
||||
if (this.peek() === '\n' || this.peek() === '\r') {
|
||||
this.advance();
|
||||
if (this.peek() === '\n') {
|
||||
this.advance();
|
||||
}
|
||||
this.line++;
|
||||
this.column = 1;
|
||||
return {
|
||||
type: TokenType.NEWLINE,
|
||||
value: '\n',
|
||||
line: startLine,
|
||||
column: startColumn
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Strings
|
||||
if (this.peek() === '"' || this.peek() === "'") {
|
||||
return this.readString(startLine, startColumn);
|
||||
}
|
||||
|
||||
// Arrow tokens (-->, ->, --label-->, --label->)
|
||||
if (this.peek() === '-') {
|
||||
if (this.peek(1) === '-') {
|
||||
// Check for labeled arrow: --label--> or --label->
|
||||
const labeledArrowMatch = this.tryParseLabeledArrow();
|
||||
if (labeledArrowMatch) {
|
||||
return labeledArrowMatch;
|
||||
}
|
||||
|
||||
// Regular arrow: -->
|
||||
if (this.peek(2) === '>') {
|
||||
this.advance(3);
|
||||
return { type: TokenType.ARROW, value: '-->', line: startLine, column: startColumn };
|
||||
}
|
||||
} else if (this.peek(1) === '>') {
|
||||
// Regular arrow: ->
|
||||
this.advance(2);
|
||||
return { type: TokenType.ARROW, value: '->', line: startLine, column: startColumn };
|
||||
}
|
||||
}
|
||||
|
||||
// Single character tokens
|
||||
switch (this.peek()) {
|
||||
case '@':
|
||||
this.advance();
|
||||
return { type: TokenType.AT, value: '@', line: startLine, column: startColumn };
|
||||
case '{':
|
||||
this.advance();
|
||||
return { type: TokenType.LBRACE, value: '{', line: startLine, column: startColumn };
|
||||
case '}':
|
||||
this.advance();
|
||||
return { type: TokenType.RBRACE, value: '}', line: startLine, column: startColumn };
|
||||
case ',':
|
||||
this.advance();
|
||||
return { type: TokenType.COMMA, value: ',', line: startLine, column: startColumn };
|
||||
case ':':
|
||||
this.advance();
|
||||
return { type: TokenType.COLON, value: ':', line: startLine, column: startColumn };
|
||||
case '(':
|
||||
this.advance();
|
||||
return { type: TokenType.LPAREN, value: '(', line: startLine, column: startColumn };
|
||||
case ')':
|
||||
this.advance();
|
||||
return { type: TokenType.RPAREN, value: ')', line: startLine, column: startColumn };
|
||||
}
|
||||
|
||||
// Keywords and identifiers
|
||||
if (this.isAlpha(this.peek())) {
|
||||
return this.readIdentifierOrKeyword(startLine, startColumn);
|
||||
}
|
||||
|
||||
// Skip unknown characters
|
||||
this.advance();
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private readIdentifierOrKeyword(line: number, column: number): Token {
|
||||
let value = '';
|
||||
|
||||
while (this.position < this.input.length &&
|
||||
(this.isAlphaNumeric(this.peek()) || this.peek() === '_')) {
|
||||
value += this.peek();
|
||||
this.advance();
|
||||
}
|
||||
|
||||
// Check for keywords
|
||||
const type = this.getKeywordType(value);
|
||||
|
||||
return {
|
||||
type,
|
||||
value,
|
||||
line,
|
||||
column
|
||||
};
|
||||
}
|
||||
|
||||
private readString(line: number, column: number): Token {
|
||||
const quote = this.peek();
|
||||
this.advance(); // Skip opening quote
|
||||
|
||||
let value = '';
|
||||
while (this.position < this.input.length && this.peek() !== quote) {
|
||||
value += this.peek();
|
||||
this.advance();
|
||||
}
|
||||
|
||||
if (this.peek() === quote) {
|
||||
this.advance(); // Skip closing quote
|
||||
}
|
||||
|
||||
return {
|
||||
type: TokenType.STRING,
|
||||
value: value, // Return the content without quotes
|
||||
line,
|
||||
column
|
||||
};
|
||||
}
|
||||
|
||||
private getKeywordType(value: string): TokenType {
|
||||
switch (value.toLowerCase()) {
|
||||
case 'usecase': return TokenType.USECASE_START;
|
||||
case 'actor': return TokenType.ACTOR;
|
||||
case 'systemboundary': return TokenType.SYSTEM_BOUNDARY;
|
||||
case 'end': return TokenType.END;
|
||||
default: return TokenType.IDENTIFIER;
|
||||
}
|
||||
}
|
||||
|
||||
private skipWhitespace(): void {
|
||||
while (this.position < this.input.length &&
|
||||
(this.peek() === ' ' || this.peek() === '\t')) {
|
||||
this.advance();
|
||||
}
|
||||
}
|
||||
|
||||
private skipComment(): void {
|
||||
while (this.position < this.input.length &&
|
||||
this.peek() !== '\n' && this.peek() !== '\r') {
|
||||
this.advance();
|
||||
}
|
||||
}
|
||||
|
||||
private peek(offset: number = 0): string {
|
||||
const pos = this.position + offset;
|
||||
return pos < this.input.length ? this.input[pos] : '';
|
||||
}
|
||||
|
||||
private tryParseLabeledArrow(): Token | null {
|
||||
// Try to parse --label--> or --label->
|
||||
const startPos = this.position;
|
||||
const startLine = this.line;
|
||||
const startColumn = this.column;
|
||||
|
||||
// Skip initial '--'
|
||||
if (this.peek() !== '-' || this.peek(1) !== '-') {
|
||||
return null;
|
||||
}
|
||||
|
||||
let pos = 2;
|
||||
let label = '';
|
||||
|
||||
// Read the label
|
||||
while (pos < this.input.length - this.position) {
|
||||
const char = this.peek(pos);
|
||||
if (char === '-') {
|
||||
// Check if this is the end pattern
|
||||
if (this.peek(pos + 1) === '-' && this.peek(pos + 2) === '>') {
|
||||
// Found --label-->
|
||||
this.advance(pos + 3);
|
||||
return {
|
||||
type: TokenType.LABELED_ARROW,
|
||||
value: `--${label}-->`,
|
||||
line: startLine,
|
||||
column: startColumn
|
||||
};
|
||||
} else if (this.peek(pos + 1) === '>') {
|
||||
// Found --label->
|
||||
this.advance(pos + 2);
|
||||
return {
|
||||
type: TokenType.LABELED_ARROW,
|
||||
value: `--${label}->`,
|
||||
line: startLine,
|
||||
column: startColumn
|
||||
};
|
||||
} else {
|
||||
label += char;
|
||||
pos++;
|
||||
}
|
||||
} else if (char.match(/[a-zA-Z0-9_]/)) {
|
||||
label += char;
|
||||
pos++;
|
||||
} else {
|
||||
// Invalid character in label
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private advance(count: number = 1): void {
|
||||
for (let i = 0; i < count && this.position < this.input.length; i++) {
|
||||
this.position++;
|
||||
this.column++;
|
||||
}
|
||||
}
|
||||
|
||||
private isAlpha(char: string): boolean {
|
||||
return /[a-zA-Z]/.test(char);
|
||||
}
|
||||
|
||||
private isAlphaNumeric(char: string): boolean {
|
||||
return /[a-zA-Z0-9]/.test(char);
|
||||
}
|
||||
}
|
||||
|
||||
class UsecaseParser {
|
||||
private tokens: Token[];
|
||||
private position: number = 0;
|
||||
|
||||
constructor(tokens: Token[]) {
|
||||
this.tokens = tokens;
|
||||
}
|
||||
|
||||
parse(): UsecaseDiagram {
|
||||
const statements: Statement[] = [];
|
||||
|
||||
// Expect 'usecase' keyword at the start
|
||||
this.consume(TokenType.USECASE_START);
|
||||
this.skipNewlines();
|
||||
|
||||
while (!this.isAtEnd()) {
|
||||
this.skipNewlines();
|
||||
|
||||
if (this.isAtEnd()) {
|
||||
break;
|
||||
}
|
||||
|
||||
const parsedStatements = this.parseStatement();
|
||||
if (parsedStatements) {
|
||||
if (Array.isArray(parsedStatements)) {
|
||||
statements.push(...parsedStatements);
|
||||
} else {
|
||||
statements.push(parsedStatements);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'usecaseDiagram',
|
||||
statements
|
||||
};
|
||||
}
|
||||
|
||||
private parseStatement(): Statement | Statement[] | null {
|
||||
const token = this.peek();
|
||||
|
||||
switch (token.type) {
|
||||
case TokenType.ACTOR:
|
||||
return this.parseActorStatement();
|
||||
case TokenType.SYSTEM_BOUNDARY:
|
||||
return this.parseSystemBoundary();
|
||||
case TokenType.IDENTIFIER:
|
||||
// Look ahead to see if this is a systemBoundaryMetadata, relationship, or use case
|
||||
if (this.isSystemBoundaryMetadata()) {
|
||||
return this.parseSystemBoundaryMetadata();
|
||||
} else if (this.isRelationship()) {
|
||||
return this.parseRelationship();
|
||||
} else {
|
||||
return this.parseUseCase();
|
||||
}
|
||||
default:
|
||||
this.advance(); // Skip unknown tokens
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private parseActorStatement(): Statement | Statement[] {
|
||||
this.consume(TokenType.ACTOR);
|
||||
|
||||
// Check if this is an inline actor-node relationship
|
||||
// Look ahead: IDENTIFIER ARROW IDENTIFIER LPAREN
|
||||
if (this.isInlineActorNodeRelationship()) {
|
||||
return this.parseInlineActorNodeRelationship();
|
||||
}
|
||||
|
||||
const actors: Actor[] = [];
|
||||
|
||||
// Parse first actor
|
||||
actors.push(this.parseActorDefinition());
|
||||
|
||||
// Parse additional actors separated by commas
|
||||
while (this.check(TokenType.COMMA)) {
|
||||
this.consume(TokenType.COMMA);
|
||||
actors.push(this.parseActorDefinition());
|
||||
}
|
||||
|
||||
return actors;
|
||||
}
|
||||
|
||||
private parseActorDefinition(): Actor {
|
||||
const name = this.consume(TokenType.IDENTIFIER).value;
|
||||
|
||||
let metadata: Record<string, string> | undefined;
|
||||
|
||||
// Check for optional metadata
|
||||
if (this.check(TokenType.AT)) {
|
||||
metadata = this.parseMetadata();
|
||||
}
|
||||
|
||||
const actor: Actor = { type: 'actor', name };
|
||||
if (metadata) {
|
||||
actor.metadata = metadata;
|
||||
}
|
||||
|
||||
return actor;
|
||||
}
|
||||
|
||||
private parseSystemBoundary(): SystemBoundary {
|
||||
this.consume(TokenType.SYSTEM_BOUNDARY);
|
||||
const name = this.consume(TokenType.IDENTIFIER).value;
|
||||
this.consume(TokenType.LBRACE);
|
||||
|
||||
// Skip newlines after opening brace
|
||||
this.skipNewlines();
|
||||
|
||||
const useCases: Usecase[] = [];
|
||||
|
||||
// Parse use cases until we hit closing brace
|
||||
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
||||
this.skipNewlines();
|
||||
|
||||
if (this.check(TokenType.RBRACE) || this.isAtEnd()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.check(TokenType.IDENTIFIER)) {
|
||||
const useCase = this.parseUseCase();
|
||||
if (useCase) {
|
||||
useCases.push(useCase as Usecase);
|
||||
}
|
||||
} else {
|
||||
this.advance(); // Skip unknown tokens
|
||||
}
|
||||
}
|
||||
|
||||
this.consume(TokenType.RBRACE);
|
||||
|
||||
return {
|
||||
type: 'systemBoundary',
|
||||
name,
|
||||
useCases
|
||||
};
|
||||
}
|
||||
|
||||
private parseSystemBoundaryMetadata(): SystemBoundaryMetadata {
|
||||
const name = this.consume(TokenType.IDENTIFIER).value;
|
||||
this.consume(TokenType.AT);
|
||||
this.consume(TokenType.LBRACE);
|
||||
|
||||
const metadata: Record<string, string> = {};
|
||||
|
||||
// Parse metadata content
|
||||
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
||||
if (this.check(TokenType.IDENTIFIER)) {
|
||||
const key = this.consume(TokenType.IDENTIFIER).value;
|
||||
this.consume(TokenType.COLON);
|
||||
|
||||
let value = '';
|
||||
if (this.check(TokenType.STRING)) {
|
||||
value = this.consume(TokenType.STRING).value;
|
||||
// Remove quotes from string value
|
||||
value = value.slice(1, -1);
|
||||
} else if (this.check(TokenType.IDENTIFIER)) {
|
||||
value = this.consume(TokenType.IDENTIFIER).value;
|
||||
}
|
||||
|
||||
metadata[key] = value;
|
||||
|
||||
// Optional comma
|
||||
if (this.check(TokenType.COMMA)) {
|
||||
this.advance();
|
||||
}
|
||||
} else {
|
||||
this.advance(); // Skip unknown tokens
|
||||
}
|
||||
}
|
||||
|
||||
this.consume(TokenType.RBRACE);
|
||||
|
||||
return {
|
||||
type: 'systemBoundaryMetadata',
|
||||
name,
|
||||
metadata
|
||||
};
|
||||
}
|
||||
|
||||
private parseUseCase(): Usecase {
|
||||
const name = this.consume(TokenType.IDENTIFIER).value;
|
||||
|
||||
return {
|
||||
type: 'usecase',
|
||||
name
|
||||
};
|
||||
}
|
||||
|
||||
private isRelationship(): boolean {
|
||||
// Look ahead to see if there's an arrow after the identifier
|
||||
const currentPos = this.position;
|
||||
this.advance(); // Skip the identifier
|
||||
const hasArrow = this.check(TokenType.ARROW) || this.check(TokenType.LABELED_ARROW);
|
||||
this.position = currentPos; // Reset position
|
||||
return hasArrow;
|
||||
}
|
||||
|
||||
private isSystemBoundaryMetadata(): boolean {
|
||||
// Look ahead to see if there's an @ after the identifier
|
||||
const currentPos = this.position;
|
||||
this.advance(); // Skip the identifier
|
||||
const hasAt = this.check(TokenType.AT);
|
||||
this.position = currentPos; // Reset position
|
||||
return hasAt;
|
||||
}
|
||||
|
||||
private parseRelationship(): ActorUseCaseRelationship | ActorNodeRelationship {
|
||||
const from = this.consume(TokenType.IDENTIFIER).value;
|
||||
|
||||
let arrowToken: Token;
|
||||
let label: string | undefined;
|
||||
|
||||
if (this.check(TokenType.LABELED_ARROW)) {
|
||||
arrowToken = this.consume(TokenType.LABELED_ARROW);
|
||||
// Extract label from --label--> or --label->
|
||||
const arrowValue = arrowToken.value;
|
||||
const match = arrowValue.match(/^--(.+?)-+>$/);
|
||||
if (match) {
|
||||
label = match[1];
|
||||
}
|
||||
} else {
|
||||
arrowToken = this.consume(TokenType.ARROW);
|
||||
}
|
||||
|
||||
// Check if target is a node definition (ID followed by parentheses)
|
||||
if (this.isNodeDefinition()) {
|
||||
const node = this.parseNodeDefinition();
|
||||
return {
|
||||
type: 'actorNodeRelationship',
|
||||
from,
|
||||
to: node.id,
|
||||
arrow: arrowToken.value,
|
||||
label
|
||||
};
|
||||
} else {
|
||||
const to = this.consume(TokenType.IDENTIFIER).value;
|
||||
return {
|
||||
type: 'actorUseCaseRelationship',
|
||||
from,
|
||||
to,
|
||||
arrow: arrowToken.value,
|
||||
label
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private isInlineActorNodeRelationship(): boolean {
|
||||
// Look ahead: IDENTIFIER (ARROW|LABELED_ARROW) IDENTIFIER LPAREN
|
||||
const currentPos = this.position;
|
||||
|
||||
if (!this.check(TokenType.IDENTIFIER)) {
|
||||
this.position = currentPos;
|
||||
return false;
|
||||
}
|
||||
this.advance(); // Skip actor name
|
||||
|
||||
if (!this.check(TokenType.ARROW) && !this.check(TokenType.LABELED_ARROW)) {
|
||||
this.position = currentPos;
|
||||
return false;
|
||||
}
|
||||
this.advance(); // Skip arrow
|
||||
|
||||
if (!this.check(TokenType.IDENTIFIER)) {
|
||||
this.position = currentPos;
|
||||
return false;
|
||||
}
|
||||
this.advance(); // Skip node ID
|
||||
|
||||
const hasLParen = this.check(TokenType.LPAREN);
|
||||
this.position = currentPos; // Reset position
|
||||
return hasLParen;
|
||||
}
|
||||
|
||||
private parseInlineActorNodeRelationship(): InlineActorNodeRelationship {
|
||||
const actor = this.consume(TokenType.IDENTIFIER).value;
|
||||
|
||||
let arrowToken: Token;
|
||||
let label: string | undefined;
|
||||
|
||||
if (this.check(TokenType.LABELED_ARROW)) {
|
||||
arrowToken = this.consume(TokenType.LABELED_ARROW);
|
||||
// Extract label from --label--> or --label->
|
||||
const arrowValue = arrowToken.value;
|
||||
const match = arrowValue.match(/^--(.+?)-+>$/);
|
||||
if (match) {
|
||||
label = match[1];
|
||||
}
|
||||
} else {
|
||||
arrowToken = this.consume(TokenType.ARROW);
|
||||
}
|
||||
|
||||
const node = this.parseNodeDefinition();
|
||||
|
||||
return {
|
||||
type: 'inlineActorNodeRelationship',
|
||||
actor,
|
||||
node,
|
||||
arrow: arrowToken.value,
|
||||
label
|
||||
};
|
||||
}
|
||||
|
||||
private isNodeDefinition(): boolean {
|
||||
// Look ahead: IDENTIFIER LPAREN
|
||||
const currentPos = this.position;
|
||||
|
||||
if (!this.check(TokenType.IDENTIFIER)) {
|
||||
this.position = currentPos;
|
||||
return false;
|
||||
}
|
||||
this.advance(); // Skip node ID
|
||||
|
||||
const hasLParen = this.check(TokenType.LPAREN);
|
||||
this.position = currentPos; // Reset position
|
||||
return hasLParen;
|
||||
}
|
||||
|
||||
private parseNodeDefinition(): Node {
|
||||
const id = this.consume(TokenType.IDENTIFIER).value;
|
||||
this.consume(TokenType.LPAREN);
|
||||
|
||||
// Parse node label (can be multiple words or a string)
|
||||
let label = '';
|
||||
if (this.check(TokenType.STRING)) {
|
||||
label = this.consume(TokenType.STRING).value;
|
||||
// Remove quotes
|
||||
label = label.slice(1, -1);
|
||||
} else {
|
||||
// Parse multiple identifiers as label
|
||||
const labelParts: string[] = [];
|
||||
while (this.check(TokenType.IDENTIFIER) && !this.check(TokenType.RPAREN)) {
|
||||
labelParts.push(this.consume(TokenType.IDENTIFIER).value);
|
||||
}
|
||||
label = labelParts.join(' ');
|
||||
}
|
||||
|
||||
this.consume(TokenType.RPAREN);
|
||||
|
||||
return {
|
||||
type: 'node',
|
||||
id,
|
||||
label
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
private parseMetadata(): Record<string, string> {
|
||||
this.consume(TokenType.AT);
|
||||
this.consume(TokenType.LBRACE);
|
||||
|
||||
const metadata: Record<string, string> = {};
|
||||
|
||||
// Handle empty metadata
|
||||
if (this.check(TokenType.RBRACE)) {
|
||||
this.consume(TokenType.RBRACE);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
// Parse key-value pairs
|
||||
do {
|
||||
const key = this.consume(TokenType.IDENTIFIER).value;
|
||||
this.consume(TokenType.COLON);
|
||||
|
||||
let value: string;
|
||||
if (this.check(TokenType.STRING)) {
|
||||
value = this.consume(TokenType.STRING).value;
|
||||
} else {
|
||||
value = this.consume(TokenType.IDENTIFIER).value;
|
||||
}
|
||||
|
||||
metadata[key] = value;
|
||||
|
||||
// Check for comma (more pairs) or closing brace
|
||||
if (this.check(TokenType.COMMA)) {
|
||||
this.consume(TokenType.COMMA);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} while (!this.check(TokenType.RBRACE) && !this.isAtEnd());
|
||||
|
||||
this.consume(TokenType.RBRACE);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private skipNewlines(): void {
|
||||
while (this.check(TokenType.NEWLINE)) {
|
||||
this.advance();
|
||||
}
|
||||
}
|
||||
|
||||
private peek(): Token {
|
||||
return this.tokens[this.position];
|
||||
}
|
||||
|
||||
private advance(): Token {
|
||||
if (!this.isAtEnd()) {
|
||||
this.position++;
|
||||
}
|
||||
return this.tokens[this.position - 1];
|
||||
}
|
||||
|
||||
private check(type: TokenType): boolean {
|
||||
if (this.isAtEnd()) return false;
|
||||
return this.peek().type === type;
|
||||
}
|
||||
|
||||
private consume(type: TokenType): Token {
|
||||
if (this.check(type)) {
|
||||
return this.advance();
|
||||
}
|
||||
|
||||
const current = this.peek();
|
||||
throw new Error(`Expected ${type}, got ${current.type} at line ${current.line}`);
|
||||
}
|
||||
|
||||
private isAtEnd(): boolean {
|
||||
return this.position >= this.tokens.length || this.peek().type === TokenType.EOF;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseUsecase(input: string): ParseResult {
|
||||
try {
|
||||
const lexer = new UsecaseLexer(input);
|
||||
const tokens = lexer.tokenize();
|
||||
const parser = new UsecaseParser(tokens);
|
||||
const ast = parser.parse();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
ast
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
errors: [error instanceof Error ? error.message : String(error)]
|
||||
};
|
||||
}
|
||||
}
|
113
packages/parser/src/language/useCase/usecaseTypes.ts
Normal file
113
packages/parser/src/language/useCase/usecaseTypes.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
// AST types for usecase diagrams
|
||||
|
||||
export interface UsecaseDiagram {
|
||||
type: 'usecaseDiagram';
|
||||
statements: Statement[];
|
||||
}
|
||||
|
||||
export type Statement = Actor | SystemBoundary | SystemBoundaryMetadata | Usecase | Relationship | ActorUseCaseRelationship | Node | ActorNodeRelationship | InlineActorNodeRelationship;
|
||||
|
||||
export interface Title {
|
||||
type: 'title';
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface AccDescr {
|
||||
type: 'accDescr';
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface AccTitle {
|
||||
type: 'accTitle';
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface Actor {
|
||||
type: 'actor';
|
||||
name: string;
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface Usecase {
|
||||
type: 'usecase';
|
||||
name: string;
|
||||
alias?: string;
|
||||
}
|
||||
|
||||
export interface SystemBoundary {
|
||||
type: 'systemBoundary';
|
||||
name: string;
|
||||
useCases: Usecase[];
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface SystemBoundaryMetadata {
|
||||
type: 'systemBoundaryMetadata';
|
||||
name: string; // boundary name
|
||||
metadata: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ActorUseCaseRelationship {
|
||||
type: 'actorUseCaseRelationship';
|
||||
from: string; // actor name
|
||||
to: string; // use case name
|
||||
arrow: string; // '-->' or '->'
|
||||
label?: string; // edge label (optional)
|
||||
}
|
||||
|
||||
export interface Node {
|
||||
type: 'node';
|
||||
id: string; // node ID (e.g., 'a', 'b', 'c')
|
||||
label: string; // node label (e.g., 'Go through code')
|
||||
}
|
||||
|
||||
export interface ActorNodeRelationship {
|
||||
type: 'actorNodeRelationship';
|
||||
from: string; // actor name
|
||||
to: string; // node ID
|
||||
arrow: string; // '-->' or '->'
|
||||
label?: string; // edge label (optional)
|
||||
}
|
||||
|
||||
export interface InlineActorNodeRelationship {
|
||||
type: 'inlineActorNodeRelationship';
|
||||
actor: string; // actor name
|
||||
node: Node; // node definition
|
||||
arrow: string; // '-->' or '->'
|
||||
label?: string; // edge label (optional)
|
||||
}
|
||||
|
||||
export interface Relationship {
|
||||
type: 'relationship';
|
||||
from: string;
|
||||
to: string;
|
||||
relationshipType: RelationshipType;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface Note {
|
||||
type: 'note';
|
||||
position: NotePosition;
|
||||
target: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export type RelationshipType =
|
||||
| 'arrow-left'
|
||||
| 'arrow-right'
|
||||
| 'arrow-both'
|
||||
| 'extends'
|
||||
| 'includes';
|
||||
|
||||
export type NotePosition =
|
||||
| 'left'
|
||||
| 'right'
|
||||
| 'top'
|
||||
| 'bottom';
|
||||
|
||||
// Parser result type
|
||||
export interface ParseResult {
|
||||
success: boolean;
|
||||
ast?: UsecaseDiagram;
|
||||
errors?: string[];
|
||||
}
|
719
pnpm-lock.yaml
generated
719
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,6 @@
|
||||
"./demos/dev",
|
||||
"./vite.config.ts",
|
||||
"./vitest.workspace.js",
|
||||
"eslint.config.js",
|
||||
"./appli.config.ts"
|
||||
"eslint.config.js"
|
||||
]
|
||||
}
|
||||
|
Reference in New Issue
Block a user