mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-14 21:09:50 +02:00
Compare commits
7 Commits
6671-code-
...
demo/useca
Author | SHA1 | Date | |
---|---|---|---|
![]() |
89b29898d2 | ||
![]() |
2972bf25bf | ||
![]() |
6b1a7a9e1a | ||
![]() |
33bc4a0b4e | ||
![]() |
c6f25167a2 | ||
![]() |
0ef3130510 | ||
![]() |
862d40cc3a |
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
|
114
.github/workflows/e2e.yml
vendored
114
.github/workflows/e2e.yml
vendored
@@ -38,8 +38,6 @@ jobs:
|
|||||||
options: --user 1001
|
options: --user 1001
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
|
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
|
||||||
@@ -57,7 +55,6 @@ jobs:
|
|||||||
if: ${{ steps.cache-snapshot.outputs.cache-hit != 'true' }}
|
if: ${{ steps.cache-snapshot.outputs.cache-hit != 'true' }}
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
|
||||||
ref: ${{ env.targetHash }}
|
ref: ${{ env.targetHash }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -86,8 +83,6 @@ jobs:
|
|||||||
containers: [1, 2, 3, 4, 5]
|
containers: [1, 2, 3, 4, 5]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
# uses version from "packageManager" field in package.json
|
# uses version from "packageManager" field in package.json
|
||||||
@@ -142,118 +137,13 @@ jobs:
|
|||||||
SPLIT_INDEX: ${{ strategy.job-index }}
|
SPLIT_INDEX: ${{ strategy.job-index }}
|
||||||
SPLIT_FILE: 'cypress/timings.json'
|
SPLIT_FILE: 'cypress/timings.json'
|
||||||
VITEST_COVERAGE: true
|
VITEST_COVERAGE: true
|
||||||
- name: Debug coverage generation
|
|
||||||
if: ${{ steps.cypress.conclusion == 'success' }}
|
|
||||||
run: |
|
|
||||||
echo "Checking if coverage files were generated:"
|
|
||||||
ls -la coverage/ || echo "No coverage directory"
|
|
||||||
ls -la coverage/cypress/ || echo "No coverage/cypress directory"
|
|
||||||
echo "Looking for any .info files:"
|
|
||||||
find . -name "*.info" -type f | head -10 || echo "No .info files found"
|
|
||||||
- name: Prepare coverage artifacts
|
|
||||||
if: ${{ steps.cypress.conclusion == 'success' }}
|
|
||||||
run: |
|
|
||||||
mkdir -p coverage/cypress
|
|
||||||
if [ -f coverage/cypress/coverage-final.json ]; then
|
|
||||||
cp coverage/cypress/coverage-final.json coverage/cypress/coverage-final-${{ matrix.containers }}.json
|
|
||||||
echo "Created coverage-final-${{ matrix.containers }}.json"
|
|
||||||
ls -la coverage/cypress/coverage-final-${{ matrix.containers }}.json
|
|
||||||
else
|
|
||||||
echo "Error: coverage/cypress/coverage-final.json not found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
- name: Upload e2e coverage artifact
|
|
||||||
if: ${{ steps.cypress.conclusion == 'success' }}
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: e2e-coverage-${{ matrix.containers }}
|
|
||||||
path: coverage/cypress/coverage-final-${{ matrix.containers }}.json
|
|
||||||
|
|
||||||
coverage-merge:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: e2e
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
|
|
||||||
with:
|
|
||||||
node-version-file: '.node-version'
|
|
||||||
- name: Download e2e coverage shards
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
pattern: e2e-coverage-*
|
|
||||||
path: coverage/e2e-shards
|
|
||||||
merge-multiple: true
|
|
||||||
- name: Debug downloaded artifacts
|
|
||||||
run: |
|
|
||||||
echo "Contents of coverage/e2e-shards:"
|
|
||||||
find coverage/e2e-shards -type f -name "*.info" | head -20
|
|
||||||
echo "All files in coverage/e2e-shards:"
|
|
||||||
ls -la coverage/e2e-shards/
|
|
||||||
echo "Directory structure:"
|
|
||||||
find coverage/e2e-shards -type f | head -20
|
|
||||||
echo "Looking for coverage-final.json files:"
|
|
||||||
find coverage/e2e-shards -name "coverage-final.json" | head -20
|
|
||||||
- name: Install dependencies for merging
|
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
env:
|
|
||||||
CYPRESS_CACHE_FOLDER: .cache/Cypress
|
|
||||||
- name: Prepare coverage files for merge script
|
|
||||||
run: |
|
|
||||||
mkdir -p coverage/vitest coverage/cypress
|
|
||||||
# Copy E2E coverage-final.json files to the structure expected by scripts/coverage.ts
|
|
||||||
for i in {1..5}; do
|
|
||||||
if [ -f "coverage/e2e-shards/coverage-final-$i.json" ]; then
|
|
||||||
cp "coverage/e2e-shards/coverage-final-$i.json" "coverage/cypress/coverage-final.json"
|
|
||||||
echo "Copied coverage-final-$i.json to cypress/"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
# Create a minimal but valid vitest coverage-final.json
|
|
||||||
echo '{"type":"Coverage","version":"1.1","data":{}}' > coverage/vitest/coverage-final.json
|
|
||||||
echo "Prepared coverage files:"
|
|
||||||
ls -la coverage/vitest/
|
|
||||||
ls -la coverage/cypress/
|
|
||||||
echo "Checking file contents:"
|
|
||||||
echo "Vitest coverage file:"
|
|
||||||
cat coverage/vitest/coverage-final.json
|
|
||||||
echo "Cypress coverage file:"
|
|
||||||
cat coverage/cypress/coverage-final.json
|
|
||||||
echo "Validating JSON files:"
|
|
||||||
if jq . coverage/vitest/coverage-final.json > /dev/null; then
|
|
||||||
echo "✓ Vitest coverage file is valid JSON"
|
|
||||||
else
|
|
||||||
echo "✗ Vitest coverage file is invalid JSON"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if jq . coverage/cypress/coverage-final.json > /dev/null; then
|
|
||||||
echo "✓ Cypress coverage file is valid JSON"
|
|
||||||
else
|
|
||||||
echo "✗ Cypress coverage file is invalid JSON"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
- name: Generate LCOV from coverage data
|
|
||||||
run: |
|
|
||||||
mkdir -p coverage/combined
|
|
||||||
# Convert coverage-final.json to LCOV format using nyc
|
|
||||||
if [ -f coverage/cypress/coverage-final.json ]; then
|
|
||||||
echo "Converting Cypress coverage to LCOV..."
|
|
||||||
npx nyc report --reporter=lcov --report-dir=coverage/combined --cwd=. --temp-dir=coverage/cypress
|
|
||||||
echo "LCOV generation completed"
|
|
||||||
ls -la coverage/combined/
|
|
||||||
else
|
|
||||||
echo "No Cypress coverage file found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
- name: Upload Coverage to Codecov
|
- name: Upload Coverage to Codecov
|
||||||
uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5.3.1
|
uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5.3.1
|
||||||
# Run step only pushes to develop and pull_requests
|
# Run step only pushes to develop and pull_requests
|
||||||
if: ${{ github.event_name == 'pull_request' || github.ref == 'refs/heads/develop'}}
|
if: ${{ steps.cypress.conclusion == 'success' && (github.event_name == 'pull_request' || github.ref == 'refs/heads/develop')}}
|
||||||
with:
|
with:
|
||||||
files: coverage/combined/lcov.info
|
files: coverage/cypress/lcov.info
|
||||||
flags: e2e
|
flags: e2e
|
||||||
name: mermaid-codecov
|
name: mermaid-codecov
|
||||||
fail_ci_if_error: false
|
fail_ci_if_error: false
|
||||||
|
3
.github/workflows/test.yml
vendored
3
.github/workflows/test.yml
vendored
@@ -10,8 +10,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
# uses version from "packageManager" field in package.json
|
# uses version from "packageManager" field in package.json
|
||||||
@@ -43,6 +41,7 @@ jobs:
|
|||||||
- name: Verify out-of-tree build with TypeScript
|
- name: Verify out-of-tree build with TypeScript
|
||||||
run: |
|
run: |
|
||||||
pnpm test:check:tsc
|
pnpm test:check:tsc
|
||||||
|
|
||||||
- name: Upload Coverage to Codecov
|
- name: Upload Coverage to Codecov
|
||||||
uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5.3.1
|
uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5.3.1
|
||||||
# Run step only pushes to develop and pull_requests
|
# Run step only pushes to develop and pull_requests
|
||||||
|
14
.nycrc
14
.nycrc
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"reporter": ["text", "lcov", "json", "html"],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules/**/*",
|
|
||||||
"cypress/**/*",
|
|
||||||
"coverage/**/*",
|
|
||||||
"**/*.spec.js",
|
|
||||||
"**/*.spec.ts",
|
|
||||||
"**/*.test.js",
|
|
||||||
"**/*.test.ts"
|
|
||||||
],
|
|
||||||
"all": true,
|
|
||||||
"check-coverage": false
|
|
||||||
}
|
|
27
codecov.yml
27
codecov.yml
@@ -1,27 +0,0 @@
|
|||||||
coverage:
|
|
||||||
status:
|
|
||||||
project:
|
|
||||||
default:
|
|
||||||
target: auto
|
|
||||||
threshold: 1%
|
|
||||||
patch:
|
|
||||||
default:
|
|
||||||
target: auto
|
|
||||||
threshold: 1%
|
|
||||||
|
|
||||||
comment:
|
|
||||||
layout: 'reach,diff,flags,tree'
|
|
||||||
behavior: default
|
|
||||||
require_changes: false
|
|
||||||
|
|
||||||
flags:
|
|
||||||
unit:
|
|
||||||
paths:
|
|
||||||
- packages/
|
|
||||||
e2e:
|
|
||||||
paths:
|
|
||||||
- packages/
|
|
||||||
|
|
||||||
# Wait for both unit and e2e coverage uploads before finalizing
|
|
||||||
notify:
|
|
||||||
after_n_builds: 2
|
|
@@ -15,13 +15,6 @@ export default eyesPlugin(
|
|||||||
setupNodeEvents(on, config) {
|
setupNodeEvents(on, config) {
|
||||||
coverage(on, config);
|
coverage(on, config);
|
||||||
cypressSplit(on, config);
|
cypressSplit(on, config);
|
||||||
|
|
||||||
// Ensure coverage generates LCOV format
|
|
||||||
on('task', {
|
|
||||||
coverage: () => {
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
on('before:browser:launch', (browser, launchOptions) => {
|
on('before:browser:launch', (browser, launchOptions) => {
|
||||||
if (browser.name === 'chrome' && browser.isHeadless) {
|
if (browser.name === 'chrome' && browser.isHeadless) {
|
||||||
launchOptions.args.push('--window-size=1440,1024', '--force-device-scale-factor=1');
|
launchOptions.args.push('--window-size=1440,1024', '--force-device-scale-factor=1');
|
||||||
|
@@ -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' } }
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
# Frequently Asked Questions
|
# 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)
|
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)
|
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)
|
4. [How to specify gantt diagram xAxis format?](https://github.com/mermaid-js/mermaid/issues/269#issuecomment-373229136)
|
||||||
|
@@ -28,6 +28,7 @@ import architecture from '../diagrams/architecture/architectureDetector.js';
|
|||||||
import { registerLazyLoadedDiagrams } from './detectType.js';
|
import { registerLazyLoadedDiagrams } from './detectType.js';
|
||||||
import { registerDiagram } from './diagramAPI.js';
|
import { registerDiagram } from './diagramAPI.js';
|
||||||
import { treemap } from '../diagrams/treemap/detector.js';
|
import { treemap } from '../diagrams/treemap/detector.js';
|
||||||
|
import usecase from '../diagrams/useCase/useCaseDetector.js';
|
||||||
import '../type.d.ts';
|
import '../type.d.ts';
|
||||||
|
|
||||||
let hasLoadedDiagrams = false;
|
let hasLoadedDiagrams = false;
|
||||||
@@ -101,6 +102,7 @@ export const addDiagrams = () => {
|
|||||||
xychart,
|
xychart,
|
||||||
block,
|
block,
|
||||||
radar,
|
radar,
|
||||||
treemap
|
treemap,
|
||||||
|
usecase
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
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,
|
||||||
|
};
|
@@ -19,7 +19,9 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf dist src/language/generated",
|
"clean": "rimraf dist src/language/generated",
|
||||||
"langium:generate": "langium generate",
|
"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": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -33,6 +35,8 @@
|
|||||||
"ast"
|
"ast"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"antlr4ts": "0.5.0-alpha.4",
|
||||||
|
"antlr4ts-cli": "0.5.0-alpha.4",
|
||||||
"langium": "3.3.1"
|
"langium": "3.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@@ -45,3 +45,4 @@ export * from './pie/index.js';
|
|||||||
export * from './architecture/index.js';
|
export * from './architecture/index.js';
|
||||||
export * from './radar/index.js';
|
export * from './radar/index.js';
|
||||||
export * from './treemap/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[];
|
||||||
|
}
|
Reference in New Issue
Block a user