mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-03 15:46:43 +02:00
Compare commits
1 Commits
develop
...
demo/useca
Author | SHA1 | Date | |
---|---|---|---|
![]() |
89b29898d2 |
@@ -138,7 +138,7 @@ xychart
|
||||
|
||||
## Chart Theme Variables
|
||||
|
||||
Themes for xychart reside inside the `xychart` attribute, allowing customization through the following syntax:
|
||||
Themes for xychart resides inside xychart attribute so to set the variables use this syntax:
|
||||
|
||||
```yaml
|
||||
---
|
||||
@@ -163,52 +163,6 @@ config:
|
||||
| yAxisLineColor | Color of the y-axis line |
|
||||
| plotColorPalette | String of colors separated by comma e.g. "#f3456, #43445" |
|
||||
|
||||
### Setting Colors for Lines and Bars
|
||||
|
||||
To set the color for lines and bars, use the `plotColorPalette` parameter. Colors in the palette will correspond sequentially to the elements in your chart (e.g., first bar/line will use the first color specified in the palette).
|
||||
|
||||
```mermaid-example
|
||||
---
|
||||
config:
|
||||
themeVariables:
|
||||
xyChart:
|
||||
plotColorPalette: '#000000, #0000FF, #00FF00, #FF0000'
|
||||
---
|
||||
xychart
|
||||
title "Different Colors in xyChart"
|
||||
x-axis "categoriesX" ["Category 1", "Category 2", "Category 3", "Category 4"]
|
||||
y-axis "valuesY" 0 --> 50
|
||||
%% Black line
|
||||
line [10,20,30,40]
|
||||
%% Blue bar
|
||||
bar [20,30,25,35]
|
||||
%% Green bar
|
||||
bar [15,25,20,30]
|
||||
%% Red line
|
||||
line [5,15,25,35]
|
||||
```
|
||||
|
||||
```mermaid
|
||||
---
|
||||
config:
|
||||
themeVariables:
|
||||
xyChart:
|
||||
plotColorPalette: '#000000, #0000FF, #00FF00, #FF0000'
|
||||
---
|
||||
xychart
|
||||
title "Different Colors in xyChart"
|
||||
x-axis "categoriesX" ["Category 1", "Category 2", "Category 3", "Category 4"]
|
||||
y-axis "valuesY" 0 --> 50
|
||||
%% Black line
|
||||
line [10,20,30,40]
|
||||
%% Blue bar
|
||||
bar [20,30,25,35]
|
||||
%% Green bar
|
||||
bar [15,25,20,30]
|
||||
%% Red line
|
||||
line [5,15,25,35]
|
||||
```
|
||||
|
||||
## Example on config and theme
|
||||
|
||||
```mermaid-example
|
||||
|
@@ -28,6 +28,7 @@ import architecture from '../diagrams/architecture/architectureDetector.js';
|
||||
import { registerLazyLoadedDiagrams } from './detectType.js';
|
||||
import { registerDiagram } from './diagramAPI.js';
|
||||
import { treemap } from '../diagrams/treemap/detector.js';
|
||||
import usecase from '../diagrams/useCase/useCaseDetector.js';
|
||||
import '../type.d.ts';
|
||||
|
||||
let hasLoadedDiagrams = false;
|
||||
@@ -101,6 +102,7 @@ export const addDiagrams = () => {
|
||||
xychart,
|
||||
block,
|
||||
radar,
|
||||
treemap
|
||||
treemap,
|
||||
usecase
|
||||
);
|
||||
};
|
||||
|
@@ -1070,14 +1070,6 @@ describe('given a class diagram with members and methods ', function () {
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
it('should handle an empty class body with {}', function () {
|
||||
const str = 'classDiagram\nclass EmptyClass {}';
|
||||
parser.parse(str);
|
||||
const actual = parser.yy.getClass('EmptyClass');
|
||||
expect(actual.label).toBe('EmptyClass');
|
||||
expect(actual.members.length).toBe(0);
|
||||
expect(actual.methods.length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -293,7 +293,6 @@ classStatement
|
||||
: classIdentifier
|
||||
| classIdentifier STYLE_SEPARATOR alphaNumToken {yy.setCssClass($1, $3);}
|
||||
| classIdentifier STRUCT_START members STRUCT_STOP {yy.addMembers($1,$3);}
|
||||
| classIdentifier STRUCT_START STRUCT_STOP {}
|
||||
| classIdentifier STYLE_SEPARATOR alphaNumToken STRUCT_START members STRUCT_STOP {yy.setCssClass($1, $3);yy.addMembers($1,$5);}
|
||||
;
|
||||
|
||||
@@ -302,15 +301,8 @@ classIdentifier
|
||||
| CLASS className classLabel {$$=$2; yy.addClass($2);yy.setClassLabel($2, $3);}
|
||||
;
|
||||
|
||||
|
||||
emptyBody
|
||||
:
|
||||
| SPACE emptyBody
|
||||
| NEWLINE emptyBody
|
||||
;
|
||||
|
||||
annotationStatement
|
||||
: ANNOTATION_START alphaNumToken ANNOTATION_END className { yy.addAnnotation($4,$2); }
|
||||
:ANNOTATION_START alphaNumToken ANNOTATION_END className { yy.addAnnotation($4,$2); }
|
||||
;
|
||||
|
||||
members
|
||||
|
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,
|
||||
};
|
@@ -1,6 +1,6 @@
|
||||
# Frequently Asked Questions
|
||||
|
||||
1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/1433#issuecomment-1991554712)
|
||||
1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/556#issuecomment-363182217)
|
||||
1. [How to specify custom CSS file?](https://github.com/mermaidjs/mermaid.cli/pull/24#issuecomment-373402785)
|
||||
1. [How to fix tooltip misplacement issue?](https://github.com/mermaid-js/mermaid/issues/542#issuecomment-3343564621)
|
||||
1. [How to specify gantt diagram xAxis format?](https://github.com/mermaid-js/mermaid/issues/269#issuecomment-373229136)
|
||||
|
@@ -126,7 +126,7 @@ xychart
|
||||
|
||||
## Chart Theme Variables
|
||||
|
||||
Themes for xychart reside inside the `xychart` attribute, allowing customization through the following syntax:
|
||||
Themes for xychart resides inside xychart attribute so to set the variables use this syntax:
|
||||
|
||||
```yaml
|
||||
---
|
||||
@@ -151,31 +151,6 @@ config:
|
||||
| yAxisLineColor | Color of the y-axis line |
|
||||
| plotColorPalette | String of colors separated by comma e.g. "#f3456, #43445" |
|
||||
|
||||
### Setting Colors for Lines and Bars
|
||||
|
||||
To set the color for lines and bars, use the `plotColorPalette` parameter. Colors in the palette will correspond sequentially to the elements in your chart (e.g., first bar/line will use the first color specified in the palette).
|
||||
|
||||
```mermaid-example
|
||||
---
|
||||
config:
|
||||
themeVariables:
|
||||
xyChart:
|
||||
plotColorPalette: '#000000, #0000FF, #00FF00, #FF0000'
|
||||
---
|
||||
xychart
|
||||
title "Different Colors in xyChart"
|
||||
x-axis "categoriesX" ["Category 1", "Category 2", "Category 3", "Category 4"]
|
||||
y-axis "valuesY" 0 --> 50
|
||||
%% Black line
|
||||
line [10,20,30,40]
|
||||
%% Blue bar
|
||||
bar [20,30,25,35]
|
||||
%% Green bar
|
||||
bar [15,25,20,30]
|
||||
%% Red line
|
||||
line [5,15,25,35]
|
||||
```
|
||||
|
||||
## Example on config and theme
|
||||
|
||||
```mermaid-example
|
||||
|
@@ -19,7 +19,9 @@
|
||||
"scripts": {
|
||||
"clean": "rimraf dist src/language/generated",
|
||||
"langium:generate": "langium generate",
|
||||
"langium:watch": "langium generate --watch"
|
||||
"langium:watch": "langium generate --watch",
|
||||
"antlr:generate": "antlr4ts -visitor -listener -o src/language/useCase/generated src/language/useCase/Usecase.g4",
|
||||
"generate": "npm run langium:generate && npm run antlr:generate"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -33,6 +35,8 @@
|
||||
"ast"
|
||||
],
|
||||
"dependencies": {
|
||||
"antlr4ts": "0.5.0-alpha.4",
|
||||
"antlr4ts-cli": "0.5.0-alpha.4",
|
||||
"langium": "3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@@ -45,3 +45,4 @@ export * from './pie/index.js';
|
||||
export * from './architecture/index.js';
|
||||
export * from './radar/index.js';
|
||||
export * from './treemap/index.js';
|
||||
export * from './useCase/index.js';
|
||||
|
70
packages/parser/src/language/useCase/.antlr/Usecase.interp
Normal file
70
packages/parser/src/language/useCase/.antlr/Usecase.interp
Normal file
File diff suppressed because one or more lines are too long
29
packages/parser/src/language/useCase/.antlr/Usecase.tokens
Normal file
29
packages/parser/src/language/useCase/.antlr/Usecase.tokens
Normal file
@@ -0,0 +1,29 @@
|
||||
USECASE_START=1
|
||||
ACTOR=2
|
||||
SYSTEM_BOUNDARY=3
|
||||
END=4
|
||||
ARROW=5
|
||||
LABELED_ARROW=6
|
||||
AT=7
|
||||
LBRACE=8
|
||||
RBRACE=9
|
||||
LPAREN=10
|
||||
RPAREN=11
|
||||
COMMA=12
|
||||
COLON=13
|
||||
STRING=14
|
||||
IDENTIFIER=15
|
||||
NEWLINE=16
|
||||
WS=17
|
||||
COMMENT=18
|
||||
'usecase'=1
|
||||
'actor'=2
|
||||
'systemBoundary'=3
|
||||
'end'=4
|
||||
'@'=7
|
||||
'{'=8
|
||||
'}'=9
|
||||
'('=10
|
||||
')'=11
|
||||
','=12
|
||||
':'=13
|
@@ -0,0 +1,71 @@
|
||||
token literal names:
|
||||
null
|
||||
'usecase'
|
||||
'actor'
|
||||
'systemBoundary'
|
||||
'end'
|
||||
null
|
||||
null
|
||||
'@'
|
||||
'{'
|
||||
'}'
|
||||
'('
|
||||
')'
|
||||
','
|
||||
':'
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
|
||||
token symbolic names:
|
||||
null
|
||||
USECASE_START
|
||||
ACTOR
|
||||
SYSTEM_BOUNDARY
|
||||
END
|
||||
ARROW
|
||||
LABELED_ARROW
|
||||
AT
|
||||
LBRACE
|
||||
RBRACE
|
||||
LPAREN
|
||||
RPAREN
|
||||
COMMA
|
||||
COLON
|
||||
STRING
|
||||
IDENTIFIER
|
||||
NEWLINE
|
||||
WS
|
||||
COMMENT
|
||||
|
||||
rule names:
|
||||
USECASE_START
|
||||
ACTOR
|
||||
SYSTEM_BOUNDARY
|
||||
END
|
||||
ARROW
|
||||
LABELED_ARROW
|
||||
AT
|
||||
LBRACE
|
||||
RBRACE
|
||||
LPAREN
|
||||
RPAREN
|
||||
COMMA
|
||||
COLON
|
||||
STRING
|
||||
IDENTIFIER
|
||||
NEWLINE
|
||||
WS
|
||||
COMMENT
|
||||
|
||||
channel names:
|
||||
DEFAULT_TOKEN_CHANNEL
|
||||
HIDDEN
|
||||
|
||||
mode names:
|
||||
DEFAULT_MODE
|
||||
|
||||
atn:
|
||||
[4, 0, 18, 154, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 3, 4, 76, 8, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 93, 8, 5, 1, 6, 1, 6, 1, 7, 1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 12, 1, 12, 1, 13, 1, 13, 5, 13, 111, 8, 13, 10, 13, 12, 13, 114, 9, 13, 1, 13, 1, 13, 1, 13, 5, 13, 119, 8, 13, 10, 13, 12, 13, 122, 9, 13, 1, 13, 3, 13, 125, 8, 13, 1, 14, 1, 14, 5, 14, 129, 8, 14, 10, 14, 12, 14, 132, 9, 14, 1, 15, 4, 15, 135, 8, 15, 11, 15, 12, 15, 136, 1, 16, 4, 16, 140, 8, 16, 11, 16, 12, 16, 141, 1, 16, 1, 16, 1, 17, 1, 17, 5, 17, 148, 8, 17, 10, 17, 12, 17, 151, 9, 17, 1, 17, 1, 17, 0, 0, 18, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, 23, 12, 25, 13, 27, 14, 29, 15, 31, 16, 33, 17, 35, 18, 1, 0, 6, 3, 0, 10, 10, 13, 13, 34, 34, 3, 0, 10, 10, 13, 13, 39, 39, 3, 0, 65, 90, 95, 95, 97, 122, 4, 0, 48, 57, 65, 90, 95, 95, 97, 122, 2, 0, 10, 10, 13, 13, 2, 0, 9, 9, 32, 32, 162, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0, 0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 1, 37, 1, 0, 0, 0, 3, 45, 1, 0, 0, 0, 5, 51, 1, 0, 0, 0, 7, 66, 1, 0, 0, 0, 9, 75, 1, 0, 0, 0, 11, 92, 1, 0, 0, 0, 13, 94, 1, 0, 0, 0, 15, 96, 1, 0, 0, 0, 17, 98, 1, 0, 0, 0, 19, 100, 1, 0, 0, 0, 21, 102, 1, 0, 0, 0, 23, 104, 1, 0, 0, 0, 25, 106, 1, 0, 0, 0, 27, 124, 1, 0, 0, 0, 29, 126, 1, 0, 0, 0, 31, 134, 1, 0, 0, 0, 33, 139, 1, 0, 0, 0, 35, 145, 1, 0, 0, 0, 37, 38, 5, 117, 0, 0, 38, 39, 5, 115, 0, 0, 39, 40, 5, 101, 0, 0, 40, 41, 5, 99, 0, 0, 41, 42, 5, 97, 0, 0, 42, 43, 5, 115, 0, 0, 43, 44, 5, 101, 0, 0, 44, 2, 1, 0, 0, 0, 45, 46, 5, 97, 0, 0, 46, 47, 5, 99, 0, 0, 47, 48, 5, 116, 0, 0, 48, 49, 5, 111, 0, 0, 49, 50, 5, 114, 0, 0, 50, 4, 1, 0, 0, 0, 51, 52, 5, 115, 0, 0, 52, 53, 5, 121, 0, 0, 53, 54, 5, 115, 0, 0, 54, 55, 5, 116, 0, 0, 55, 56, 5, 101, 0, 0, 56, 57, 5, 109, 0, 0, 57, 58, 5, 66, 0, 0, 58, 59, 5, 111, 0, 0, 59, 60, 5, 117, 0, 0, 60, 61, 5, 110, 0, 0, 61, 62, 5, 100, 0, 0, 62, 63, 5, 97, 0, 0, 63, 64, 5, 114, 0, 0, 64, 65, 5, 121, 0, 0, 65, 6, 1, 0, 0, 0, 66, 67, 5, 101, 0, 0, 67, 68, 5, 110, 0, 0, 68, 69, 5, 100, 0, 0, 69, 8, 1, 0, 0, 0, 70, 71, 5, 45, 0, 0, 71, 72, 5, 45, 0, 0, 72, 76, 5, 62, 0, 0, 73, 74, 5, 45, 0, 0, 74, 76, 5, 62, 0, 0, 75, 70, 1, 0, 0, 0, 75, 73, 1, 0, 0, 0, 76, 10, 1, 0, 0, 0, 77, 78, 5, 45, 0, 0, 78, 79, 5, 45, 0, 0, 79, 80, 1, 0, 0, 0, 80, 81, 3, 29, 14, 0, 81, 82, 5, 45, 0, 0, 82, 83, 5, 45, 0, 0, 83, 84, 5, 62, 0, 0, 84, 93, 1, 0, 0, 0, 85, 86, 5, 45, 0, 0, 86, 87, 5, 45, 0, 0, 87, 88, 1, 0, 0, 0, 88, 89, 3, 29, 14, 0, 89, 90, 5, 45, 0, 0, 90, 91, 5, 62, 0, 0, 91, 93, 1, 0, 0, 0, 92, 77, 1, 0, 0, 0, 92, 85, 1, 0, 0, 0, 93, 12, 1, 0, 0, 0, 94, 95, 5, 64, 0, 0, 95, 14, 1, 0, 0, 0, 96, 97, 5, 123, 0, 0, 97, 16, 1, 0, 0, 0, 98, 99, 5, 125, 0, 0, 99, 18, 1, 0, 0, 0, 100, 101, 5, 40, 0, 0, 101, 20, 1, 0, 0, 0, 102, 103, 5, 41, 0, 0, 103, 22, 1, 0, 0, 0, 104, 105, 5, 44, 0, 0, 105, 24, 1, 0, 0, 0, 106, 107, 5, 58, 0, 0, 107, 26, 1, 0, 0, 0, 108, 112, 5, 34, 0, 0, 109, 111, 8, 0, 0, 0, 110, 109, 1, 0, 0, 0, 111, 114, 1, 0, 0, 0, 112, 110, 1, 0, 0, 0, 112, 113, 1, 0, 0, 0, 113, 115, 1, 0, 0, 0, 114, 112, 1, 0, 0, 0, 115, 125, 5, 34, 0, 0, 116, 120, 5, 39, 0, 0, 117, 119, 8, 1, 0, 0, 118, 117, 1, 0, 0, 0, 119, 122, 1, 0, 0, 0, 120, 118, 1, 0, 0, 0, 120, 121, 1, 0, 0, 0, 121, 123, 1, 0, 0, 0, 122, 120, 1, 0, 0, 0, 123, 125, 5, 39, 0, 0, 124, 108, 1, 0, 0, 0, 124, 116, 1, 0, 0, 0, 125, 28, 1, 0, 0, 0, 126, 130, 7, 2, 0, 0, 127, 129, 7, 3, 0, 0, 128, 127, 1, 0, 0, 0, 129, 132, 1, 0, 0, 0, 130, 128, 1, 0, 0, 0, 130, 131, 1, 0, 0, 0, 131, 30, 1, 0, 0, 0, 132, 130, 1, 0, 0, 0, 133, 135, 7, 4, 0, 0, 134, 133, 1, 0, 0, 0, 135, 136, 1, 0, 0, 0, 136, 134, 1, 0, 0, 0, 136, 137, 1, 0, 0, 0, 137, 32, 1, 0, 0, 0, 138, 140, 7, 5, 0, 0, 139, 138, 1, 0, 0, 0, 140, 141, 1, 0, 0, 0, 141, 139, 1, 0, 0, 0, 141, 142, 1, 0, 0, 0, 142, 143, 1, 0, 0, 0, 143, 144, 6, 16, 0, 0, 144, 34, 1, 0, 0, 0, 145, 149, 5, 37, 0, 0, 146, 148, 8, 4, 0, 0, 147, 146, 1, 0, 0, 0, 148, 151, 1, 0, 0, 0, 149, 147, 1, 0, 0, 0, 149, 150, 1, 0, 0, 0, 150, 152, 1, 0, 0, 0, 151, 149, 1, 0, 0, 0, 152, 153, 6, 17, 0, 0, 153, 36, 1, 0, 0, 0, 10, 0, 75, 92, 112, 120, 124, 130, 136, 141, 149, 1, 6, 0, 0]
|
213
packages/parser/src/language/useCase/.antlr/UsecaseLexer.java
Normal file
213
packages/parser/src/language/useCase/.antlr/UsecaseLexer.java
Normal file
@@ -0,0 +1,213 @@
|
||||
// Generated from /home/omkar-kadam/Public/mermaid/mermaid/packages/parser/src/language/useCase/Usecase.g4 by ANTLR 4.13.1
|
||||
import org.antlr.v4.runtime.Lexer;
|
||||
import org.antlr.v4.runtime.CharStream;
|
||||
import org.antlr.v4.runtime.Token;
|
||||
import org.antlr.v4.runtime.TokenStream;
|
||||
import org.antlr.v4.runtime.*;
|
||||
import org.antlr.v4.runtime.atn.*;
|
||||
import org.antlr.v4.runtime.dfa.DFA;
|
||||
import org.antlr.v4.runtime.misc.*;
|
||||
|
||||
@SuppressWarnings({"all", "warnings", "unchecked", "unused", "cast", "CheckReturnValue", "this-escape"})
|
||||
public class UsecaseLexer extends Lexer {
|
||||
static { RuntimeMetaData.checkVersion("4.13.1", RuntimeMetaData.VERSION); }
|
||||
|
||||
protected static final DFA[] _decisionToDFA;
|
||||
protected static final PredictionContextCache _sharedContextCache =
|
||||
new PredictionContextCache();
|
||||
public static final int
|
||||
USECASE_START=1, ACTOR=2, SYSTEM_BOUNDARY=3, END=4, ARROW=5, LABELED_ARROW=6,
|
||||
AT=7, LBRACE=8, RBRACE=9, LPAREN=10, RPAREN=11, COMMA=12, COLON=13, STRING=14,
|
||||
IDENTIFIER=15, NEWLINE=16, WS=17, COMMENT=18;
|
||||
public static String[] channelNames = {
|
||||
"DEFAULT_TOKEN_CHANNEL", "HIDDEN"
|
||||
};
|
||||
|
||||
public static String[] modeNames = {
|
||||
"DEFAULT_MODE"
|
||||
};
|
||||
|
||||
private static String[] makeRuleNames() {
|
||||
return new String[] {
|
||||
"USECASE_START", "ACTOR", "SYSTEM_BOUNDARY", "END", "ARROW", "LABELED_ARROW",
|
||||
"AT", "LBRACE", "RBRACE", "LPAREN", "RPAREN", "COMMA", "COLON", "STRING",
|
||||
"IDENTIFIER", "NEWLINE", "WS", "COMMENT"
|
||||
};
|
||||
}
|
||||
public static final String[] ruleNames = makeRuleNames();
|
||||
|
||||
private static String[] makeLiteralNames() {
|
||||
return new String[] {
|
||||
null, "'usecase'", "'actor'", "'systemBoundary'", "'end'", null, null,
|
||||
"'@'", "'{'", "'}'", "'('", "')'", "','", "':'"
|
||||
};
|
||||
}
|
||||
private static final String[] _LITERAL_NAMES = makeLiteralNames();
|
||||
private static String[] makeSymbolicNames() {
|
||||
return new String[] {
|
||||
null, "USECASE_START", "ACTOR", "SYSTEM_BOUNDARY", "END", "ARROW", "LABELED_ARROW",
|
||||
"AT", "LBRACE", "RBRACE", "LPAREN", "RPAREN", "COMMA", "COLON", "STRING",
|
||||
"IDENTIFIER", "NEWLINE", "WS", "COMMENT"
|
||||
};
|
||||
}
|
||||
private static final String[] _SYMBOLIC_NAMES = makeSymbolicNames();
|
||||
public static final Vocabulary VOCABULARY = new VocabularyImpl(_LITERAL_NAMES, _SYMBOLIC_NAMES);
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #VOCABULARY} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public static final String[] tokenNames;
|
||||
static {
|
||||
tokenNames = new String[_SYMBOLIC_NAMES.length];
|
||||
for (int i = 0; i < tokenNames.length; i++) {
|
||||
tokenNames[i] = VOCABULARY.getLiteralName(i);
|
||||
if (tokenNames[i] == null) {
|
||||
tokenNames[i] = VOCABULARY.getSymbolicName(i);
|
||||
}
|
||||
|
||||
if (tokenNames[i] == null) {
|
||||
tokenNames[i] = "<INVALID>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public String[] getTokenNames() {
|
||||
return tokenNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
public Vocabulary getVocabulary() {
|
||||
return VOCABULARY;
|
||||
}
|
||||
|
||||
|
||||
public UsecaseLexer(CharStream input) {
|
||||
super(input);
|
||||
_interp = new LexerATNSimulator(this,_ATN,_decisionToDFA,_sharedContextCache);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGrammarFileName() { return "Usecase.g4"; }
|
||||
|
||||
@Override
|
||||
public String[] getRuleNames() { return ruleNames; }
|
||||
|
||||
@Override
|
||||
public String getSerializedATN() { return _serializedATN; }
|
||||
|
||||
@Override
|
||||
public String[] getChannelNames() { return channelNames; }
|
||||
|
||||
@Override
|
||||
public String[] getModeNames() { return modeNames; }
|
||||
|
||||
@Override
|
||||
public ATN getATN() { return _ATN; }
|
||||
|
||||
public static final String _serializedATN =
|
||||
"\u0004\u0000\u0012\u009a\u0006\uffff\uffff\u0002\u0000\u0007\u0000\u0002"+
|
||||
"\u0001\u0007\u0001\u0002\u0002\u0007\u0002\u0002\u0003\u0007\u0003\u0002"+
|
||||
"\u0004\u0007\u0004\u0002\u0005\u0007\u0005\u0002\u0006\u0007\u0006\u0002"+
|
||||
"\u0007\u0007\u0007\u0002\b\u0007\b\u0002\t\u0007\t\u0002\n\u0007\n\u0002"+
|
||||
"\u000b\u0007\u000b\u0002\f\u0007\f\u0002\r\u0007\r\u0002\u000e\u0007\u000e"+
|
||||
"\u0002\u000f\u0007\u000f\u0002\u0010\u0007\u0010\u0002\u0011\u0007\u0011"+
|
||||
"\u0001\u0000\u0001\u0000\u0001\u0000\u0001\u0000\u0001\u0000\u0001\u0000"+
|
||||
"\u0001\u0000\u0001\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001"+
|
||||
"\u0001\u0001\u0001\u0001\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002"+
|
||||
"\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002"+
|
||||
"\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0003"+
|
||||
"\u0001\u0003\u0001\u0003\u0001\u0003\u0001\u0004\u0001\u0004\u0001\u0004"+
|
||||
"\u0001\u0004\u0001\u0004\u0003\u0004L\b\u0004\u0001\u0005\u0001\u0005"+
|
||||
"\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0005"+
|
||||
"\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0005"+
|
||||
"\u0001\u0005\u0003\u0005]\b\u0005\u0001\u0006\u0001\u0006\u0001\u0007"+
|
||||
"\u0001\u0007\u0001\b\u0001\b\u0001\t\u0001\t\u0001\n\u0001\n\u0001\u000b"+
|
||||
"\u0001\u000b\u0001\f\u0001\f\u0001\r\u0001\r\u0005\ro\b\r\n\r\f\rr\t\r"+
|
||||
"\u0001\r\u0001\r\u0001\r\u0005\rw\b\r\n\r\f\rz\t\r\u0001\r\u0003\r}\b"+
|
||||
"\r\u0001\u000e\u0001\u000e\u0005\u000e\u0081\b\u000e\n\u000e\f\u000e\u0084"+
|
||||
"\t\u000e\u0001\u000f\u0004\u000f\u0087\b\u000f\u000b\u000f\f\u000f\u0088"+
|
||||
"\u0001\u0010\u0004\u0010\u008c\b\u0010\u000b\u0010\f\u0010\u008d\u0001"+
|
||||
"\u0010\u0001\u0010\u0001\u0011\u0001\u0011\u0005\u0011\u0094\b\u0011\n"+
|
||||
"\u0011\f\u0011\u0097\t\u0011\u0001\u0011\u0001\u0011\u0000\u0000\u0012"+
|
||||
"\u0001\u0001\u0003\u0002\u0005\u0003\u0007\u0004\t\u0005\u000b\u0006\r"+
|
||||
"\u0007\u000f\b\u0011\t\u0013\n\u0015\u000b\u0017\f\u0019\r\u001b\u000e"+
|
||||
"\u001d\u000f\u001f\u0010!\u0011#\u0012\u0001\u0000\u0006\u0003\u0000\n"+
|
||||
"\n\r\r\"\"\u0003\u0000\n\n\r\r\'\'\u0003\u0000AZ__az\u0004\u000009AZ_"+
|
||||
"_az\u0002\u0000\n\n\r\r\u0002\u0000\t\t \u00a2\u0000\u0001\u0001\u0000"+
|
||||
"\u0000\u0000\u0000\u0003\u0001\u0000\u0000\u0000\u0000\u0005\u0001\u0000"+
|
||||
"\u0000\u0000\u0000\u0007\u0001\u0000\u0000\u0000\u0000\t\u0001\u0000\u0000"+
|
||||
"\u0000\u0000\u000b\u0001\u0000\u0000\u0000\u0000\r\u0001\u0000\u0000\u0000"+
|
||||
"\u0000\u000f\u0001\u0000\u0000\u0000\u0000\u0011\u0001\u0000\u0000\u0000"+
|
||||
"\u0000\u0013\u0001\u0000\u0000\u0000\u0000\u0015\u0001\u0000\u0000\u0000"+
|
||||
"\u0000\u0017\u0001\u0000\u0000\u0000\u0000\u0019\u0001\u0000\u0000\u0000"+
|
||||
"\u0000\u001b\u0001\u0000\u0000\u0000\u0000\u001d\u0001\u0000\u0000\u0000"+
|
||||
"\u0000\u001f\u0001\u0000\u0000\u0000\u0000!\u0001\u0000\u0000\u0000\u0000"+
|
||||
"#\u0001\u0000\u0000\u0000\u0001%\u0001\u0000\u0000\u0000\u0003-\u0001"+
|
||||
"\u0000\u0000\u0000\u00053\u0001\u0000\u0000\u0000\u0007B\u0001\u0000\u0000"+
|
||||
"\u0000\tK\u0001\u0000\u0000\u0000\u000b\\\u0001\u0000\u0000\u0000\r^\u0001"+
|
||||
"\u0000\u0000\u0000\u000f`\u0001\u0000\u0000\u0000\u0011b\u0001\u0000\u0000"+
|
||||
"\u0000\u0013d\u0001\u0000\u0000\u0000\u0015f\u0001\u0000\u0000\u0000\u0017"+
|
||||
"h\u0001\u0000\u0000\u0000\u0019j\u0001\u0000\u0000\u0000\u001b|\u0001"+
|
||||
"\u0000\u0000\u0000\u001d~\u0001\u0000\u0000\u0000\u001f\u0086\u0001\u0000"+
|
||||
"\u0000\u0000!\u008b\u0001\u0000\u0000\u0000#\u0091\u0001\u0000\u0000\u0000"+
|
||||
"%&\u0005u\u0000\u0000&\'\u0005s\u0000\u0000\'(\u0005e\u0000\u0000()\u0005"+
|
||||
"c\u0000\u0000)*\u0005a\u0000\u0000*+\u0005s\u0000\u0000+,\u0005e\u0000"+
|
||||
"\u0000,\u0002\u0001\u0000\u0000\u0000-.\u0005a\u0000\u0000./\u0005c\u0000"+
|
||||
"\u0000/0\u0005t\u0000\u000001\u0005o\u0000\u000012\u0005r\u0000\u0000"+
|
||||
"2\u0004\u0001\u0000\u0000\u000034\u0005s\u0000\u000045\u0005y\u0000\u0000"+
|
||||
"56\u0005s\u0000\u000067\u0005t\u0000\u000078\u0005e\u0000\u000089\u0005"+
|
||||
"m\u0000\u00009:\u0005B\u0000\u0000:;\u0005o\u0000\u0000;<\u0005u\u0000"+
|
||||
"\u0000<=\u0005n\u0000\u0000=>\u0005d\u0000\u0000>?\u0005a\u0000\u0000"+
|
||||
"?@\u0005r\u0000\u0000@A\u0005y\u0000\u0000A\u0006\u0001\u0000\u0000\u0000"+
|
||||
"BC\u0005e\u0000\u0000CD\u0005n\u0000\u0000DE\u0005d\u0000\u0000E\b\u0001"+
|
||||
"\u0000\u0000\u0000FG\u0005-\u0000\u0000GH\u0005-\u0000\u0000HL\u0005>"+
|
||||
"\u0000\u0000IJ\u0005-\u0000\u0000JL\u0005>\u0000\u0000KF\u0001\u0000\u0000"+
|
||||
"\u0000KI\u0001\u0000\u0000\u0000L\n\u0001\u0000\u0000\u0000MN\u0005-\u0000"+
|
||||
"\u0000NO\u0005-\u0000\u0000OP\u0001\u0000\u0000\u0000PQ\u0003\u001d\u000e"+
|
||||
"\u0000QR\u0005-\u0000\u0000RS\u0005-\u0000\u0000ST\u0005>\u0000\u0000"+
|
||||
"T]\u0001\u0000\u0000\u0000UV\u0005-\u0000\u0000VW\u0005-\u0000\u0000W"+
|
||||
"X\u0001\u0000\u0000\u0000XY\u0003\u001d\u000e\u0000YZ\u0005-\u0000\u0000"+
|
||||
"Z[\u0005>\u0000\u0000[]\u0001\u0000\u0000\u0000\\M\u0001\u0000\u0000\u0000"+
|
||||
"\\U\u0001\u0000\u0000\u0000]\f\u0001\u0000\u0000\u0000^_\u0005@\u0000"+
|
||||
"\u0000_\u000e\u0001\u0000\u0000\u0000`a\u0005{\u0000\u0000a\u0010\u0001"+
|
||||
"\u0000\u0000\u0000bc\u0005}\u0000\u0000c\u0012\u0001\u0000\u0000\u0000"+
|
||||
"de\u0005(\u0000\u0000e\u0014\u0001\u0000\u0000\u0000fg\u0005)\u0000\u0000"+
|
||||
"g\u0016\u0001\u0000\u0000\u0000hi\u0005,\u0000\u0000i\u0018\u0001\u0000"+
|
||||
"\u0000\u0000jk\u0005:\u0000\u0000k\u001a\u0001\u0000\u0000\u0000lp\u0005"+
|
||||
"\"\u0000\u0000mo\b\u0000\u0000\u0000nm\u0001\u0000\u0000\u0000or\u0001"+
|
||||
"\u0000\u0000\u0000pn\u0001\u0000\u0000\u0000pq\u0001\u0000\u0000\u0000"+
|
||||
"qs\u0001\u0000\u0000\u0000rp\u0001\u0000\u0000\u0000s}\u0005\"\u0000\u0000"+
|
||||
"tx\u0005\'\u0000\u0000uw\b\u0001\u0000\u0000vu\u0001\u0000\u0000\u0000"+
|
||||
"wz\u0001\u0000\u0000\u0000xv\u0001\u0000\u0000\u0000xy\u0001\u0000\u0000"+
|
||||
"\u0000y{\u0001\u0000\u0000\u0000zx\u0001\u0000\u0000\u0000{}\u0005\'\u0000"+
|
||||
"\u0000|l\u0001\u0000\u0000\u0000|t\u0001\u0000\u0000\u0000}\u001c\u0001"+
|
||||
"\u0000\u0000\u0000~\u0082\u0007\u0002\u0000\u0000\u007f\u0081\u0007\u0003"+
|
||||
"\u0000\u0000\u0080\u007f\u0001\u0000\u0000\u0000\u0081\u0084\u0001\u0000"+
|
||||
"\u0000\u0000\u0082\u0080\u0001\u0000\u0000\u0000\u0082\u0083\u0001\u0000"+
|
||||
"\u0000\u0000\u0083\u001e\u0001\u0000\u0000\u0000\u0084\u0082\u0001\u0000"+
|
||||
"\u0000\u0000\u0085\u0087\u0007\u0004\u0000\u0000\u0086\u0085\u0001\u0000"+
|
||||
"\u0000\u0000\u0087\u0088\u0001\u0000\u0000\u0000\u0088\u0086\u0001\u0000"+
|
||||
"\u0000\u0000\u0088\u0089\u0001\u0000\u0000\u0000\u0089 \u0001\u0000\u0000"+
|
||||
"\u0000\u008a\u008c\u0007\u0005\u0000\u0000\u008b\u008a\u0001\u0000\u0000"+
|
||||
"\u0000\u008c\u008d\u0001\u0000\u0000\u0000\u008d\u008b\u0001\u0000\u0000"+
|
||||
"\u0000\u008d\u008e\u0001\u0000\u0000\u0000\u008e\u008f\u0001\u0000\u0000"+
|
||||
"\u0000\u008f\u0090\u0006\u0010\u0000\u0000\u0090\"\u0001\u0000\u0000\u0000"+
|
||||
"\u0091\u0095\u0005%\u0000\u0000\u0092\u0094\b\u0004\u0000\u0000\u0093"+
|
||||
"\u0092\u0001\u0000\u0000\u0000\u0094\u0097\u0001\u0000\u0000\u0000\u0095"+
|
||||
"\u0093\u0001\u0000\u0000\u0000\u0095\u0096\u0001\u0000\u0000\u0000\u0096"+
|
||||
"\u0098\u0001\u0000\u0000\u0000\u0097\u0095\u0001\u0000\u0000\u0000\u0098"+
|
||||
"\u0099\u0006\u0011\u0000\u0000\u0099$\u0001\u0000\u0000\u0000\n\u0000"+
|
||||
"K\\px|\u0082\u0088\u008d\u0095\u0001\u0006\u0000\u0000";
|
||||
public static final ATN _ATN =
|
||||
new ATNDeserializer().deserialize(_serializedATN.toCharArray());
|
||||
static {
|
||||
_decisionToDFA = new DFA[_ATN.getNumberOfDecisions()];
|
||||
for (int i = 0; i < _ATN.getNumberOfDecisions(); i++) {
|
||||
_decisionToDFA[i] = new DFA(_ATN.getDecisionState(i), i);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
USECASE_START=1
|
||||
ACTOR=2
|
||||
SYSTEM_BOUNDARY=3
|
||||
END=4
|
||||
ARROW=5
|
||||
LABELED_ARROW=6
|
||||
AT=7
|
||||
LBRACE=8
|
||||
RBRACE=9
|
||||
LPAREN=10
|
||||
RPAREN=11
|
||||
COMMA=12
|
||||
COLON=13
|
||||
STRING=14
|
||||
IDENTIFIER=15
|
||||
NEWLINE=16
|
||||
WS=17
|
||||
COMMENT=18
|
||||
'usecase'=1
|
||||
'actor'=2
|
||||
'systemBoundary'=3
|
||||
'end'=4
|
||||
'@'=7
|
||||
'{'=8
|
||||
'}'=9
|
||||
'('=10
|
||||
')'=11
|
||||
','=12
|
||||
':'=13
|
1574
packages/parser/src/language/useCase/.antlr/UsecaseParser.java
Normal file
1574
packages/parser/src/language/useCase/.antlr/UsecaseParser.java
Normal file
File diff suppressed because it is too large
Load Diff
184
packages/parser/src/language/useCase/Usecase.g4
Normal file
184
packages/parser/src/language/useCase/Usecase.g4
Normal file
@@ -0,0 +1,184 @@
|
||||
grammar Usecase;
|
||||
|
||||
// Parser rules
|
||||
usecaseDiagram
|
||||
: USECASE_START NEWLINE* statement* EOF
|
||||
;
|
||||
|
||||
statement
|
||||
: actor NEWLINE*
|
||||
| systemBoundary NEWLINE*
|
||||
| systemBoundaryMetadata NEWLINE*
|
||||
| useCase NEWLINE*
|
||||
| relationship NEWLINE*
|
||||
| actorRelationship NEWLINE*
|
||||
| NEWLINE
|
||||
;
|
||||
|
||||
relationship
|
||||
: actorName ARROW target
|
||||
| actorName LABELED_ARROW target
|
||||
;
|
||||
|
||||
actorRelationship
|
||||
: ACTOR actorName ARROW target
|
||||
| ACTOR actorName LABELED_ARROW target
|
||||
;
|
||||
|
||||
target
|
||||
: useCaseName
|
||||
| nodeDefinition
|
||||
;
|
||||
|
||||
nodeDefinition
|
||||
: nodeId LPAREN nodeLabel RPAREN
|
||||
;
|
||||
|
||||
nodeId
|
||||
: IDENTIFIER
|
||||
;
|
||||
|
||||
nodeLabel
|
||||
: IDENTIFIER (WS IDENTIFIER)*
|
||||
| STRING
|
||||
;
|
||||
|
||||
actorName
|
||||
: IDENTIFIER
|
||||
;
|
||||
|
||||
systemBoundary
|
||||
: SYSTEM_BOUNDARY boundaryName LBRACE NEWLINE* boundaryContent* RBRACE
|
||||
| SYSTEM_BOUNDARY boundaryName NEWLINE* boundaryContent* END
|
||||
;
|
||||
|
||||
systemBoundaryMetadata
|
||||
: boundaryName AT LBRACE metadataContent RBRACE
|
||||
;
|
||||
|
||||
boundaryContent
|
||||
: useCase NEWLINE*
|
||||
| NEWLINE
|
||||
;
|
||||
|
||||
useCase
|
||||
: useCaseName
|
||||
;
|
||||
|
||||
boundaryName
|
||||
: IDENTIFIER
|
||||
;
|
||||
|
||||
useCaseName
|
||||
: IDENTIFIER
|
||||
;
|
||||
|
||||
actor
|
||||
: ACTOR actorList
|
||||
;
|
||||
|
||||
actorList
|
||||
: actorDefinition (COMMA actorDefinition)*
|
||||
;
|
||||
|
||||
actorDefinition
|
||||
: actorName metadata?
|
||||
;
|
||||
|
||||
metadata
|
||||
: AT LBRACE metadataContent RBRACE
|
||||
;
|
||||
|
||||
metadataContent
|
||||
: metadataPair (COMMA metadataPair)*
|
||||
|
|
||||
;
|
||||
|
||||
metadataPair
|
||||
: metadataKey COLON metadataValue
|
||||
;
|
||||
|
||||
metadataKey
|
||||
: IDENTIFIER
|
||||
;
|
||||
|
||||
metadataValue
|
||||
: STRING
|
||||
| IDENTIFIER
|
||||
;
|
||||
|
||||
// Lexer rules
|
||||
USECASE_START
|
||||
: 'usecase'
|
||||
;
|
||||
|
||||
ACTOR
|
||||
: 'actor'
|
||||
;
|
||||
|
||||
SYSTEM_BOUNDARY
|
||||
: 'systemBoundary'
|
||||
;
|
||||
|
||||
END
|
||||
: 'end'
|
||||
;
|
||||
|
||||
ARROW
|
||||
: '-->'
|
||||
| '->'
|
||||
;
|
||||
|
||||
LABELED_ARROW
|
||||
: '--' IDENTIFIER '-->'
|
||||
| '--' IDENTIFIER '->'
|
||||
;
|
||||
|
||||
AT
|
||||
: '@'
|
||||
;
|
||||
|
||||
LBRACE
|
||||
: '{'
|
||||
;
|
||||
|
||||
RBRACE
|
||||
: '}'
|
||||
;
|
||||
|
||||
LPAREN
|
||||
: '('
|
||||
;
|
||||
|
||||
RPAREN
|
||||
: ')'
|
||||
;
|
||||
|
||||
COMMA
|
||||
: ','
|
||||
;
|
||||
|
||||
COLON
|
||||
: ':'
|
||||
;
|
||||
|
||||
STRING
|
||||
: '"' (~["\r\n])* '"'
|
||||
| '\'' (~['\r\n])* '\''
|
||||
;
|
||||
|
||||
IDENTIFIER
|
||||
: [a-zA-Z_][a-zA-Z0-9_]*
|
||||
;
|
||||
|
||||
NEWLINE
|
||||
: [\r\n]+
|
||||
;
|
||||
|
||||
WS
|
||||
: [ \t]+ -> skip
|
||||
;
|
||||
|
||||
COMMENT
|
||||
: '%' ~[\r\n]* -> skip
|
||||
;
|
2
packages/parser/src/language/useCase/index.ts
Normal file
2
packages/parser/src/language/useCase/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { parseUsecase } from './usecaseParser.js';
|
||||
export * from './usecaseTypes.js';
|
387
packages/parser/src/language/useCase/test.ts
Normal file
387
packages/parser/src/language/useCase/test.ts
Normal file
@@ -0,0 +1,387 @@
|
||||
import { parseUsecase } from './usecaseParser.js';
|
||||
|
||||
// Test basic usecase diagram parsing
|
||||
function testBasicUsecaseParsing() {
|
||||
const input = `usecase
|
||||
actor Developer1
|
||||
actor Developer2
|
||||
actor Developer3`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Basic Usecase Parsing:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test simple usecase diagram
|
||||
function testSimpleUsecaseParsing() {
|
||||
const input = `usecase
|
||||
actor User
|
||||
actor Admin`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Simple Usecase Parsing:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test metadata parsing
|
||||
function testMetadataParsing() {
|
||||
const input = `usecase
|
||||
actor Developer1@{ icon : 'icon_name', place: "sample place" }`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Metadata Parsing:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test complex metadata parsing
|
||||
function testComplexMetadataParsing() {
|
||||
const input = `usecase
|
||||
actor Developer1@{ icon : 'icon_name', type : 'hollow', place: "sample place", material:"sample" }`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Complex Metadata Parsing:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test mixed actors (with and without metadata)
|
||||
function testMixedActorsParsing() {
|
||||
const input = `usecase
|
||||
actor User
|
||||
actor Developer1@{ icon : 'dev_icon' }
|
||||
actor Admin@{ type: 'admin', place: "office" }`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Mixed Actors Parsing:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test multiple actors in single line
|
||||
function testMultipleActorsSingleLine() {
|
||||
const input = `usecase
|
||||
actor Developer1, Developer2, Developer3`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Multiple Actors Single Line:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test multiple actors with metadata
|
||||
function testMultipleActorsWithMetadata() {
|
||||
const input = `usecase
|
||||
actor Developer1@{ icon: 'dev' }, Developer2, Developer3@{ type: 'admin' }`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Multiple Actors With Metadata:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test five actors in single line
|
||||
function testFiveActorsSingleLine() {
|
||||
const input = `usecase
|
||||
actor Developer1, Developer2, Developer3, Developer4, Developer5`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Five Actors Single Line:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test system boundary parsing
|
||||
function testSystemBoundaryParsing() {
|
||||
const input = `usecase
|
||||
actor Developer1
|
||||
systemBoundary Tasks
|
||||
coding
|
||||
testing
|
||||
deploying
|
||||
end`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test System Boundary Parsing:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test mixed actors and system boundaries
|
||||
function testMixedActorsAndBoundaries() {
|
||||
const input = `usecase
|
||||
actor Developer1, Developer2
|
||||
systemBoundary Tasks
|
||||
coding
|
||||
testing
|
||||
end
|
||||
actor Admin`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Mixed Actors and Boundaries:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test curly brace system boundary parsing
|
||||
function testCurlyBraceSystemBoundary() {
|
||||
const input = `usecase
|
||||
actor Developer1
|
||||
systemBoundary Tasks {
|
||||
playing
|
||||
reviewing
|
||||
}`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Curly Brace System Boundary:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test relationship parsing
|
||||
function testRelationshipParsing() {
|
||||
const input = `usecase
|
||||
actor Developer1
|
||||
systemBoundary Tasks {
|
||||
playing
|
||||
reviewing
|
||||
}
|
||||
Developer1 --> playing
|
||||
Developer1 --> reviewing`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Relationship Parsing:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test complete example
|
||||
function testCompleteExample() {
|
||||
const input = `usecase
|
||||
actor Developer1
|
||||
systemBoundary Tasks {
|
||||
playing
|
||||
reviewing
|
||||
}
|
||||
Developer1 --> playing
|
||||
Developer1 --> reviewing`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Complete Example:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test node definitions
|
||||
function testNodeDefinitions() {
|
||||
const input = `usecase
|
||||
actor Tester1
|
||||
Tester1 --> c(Go through testing)`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Node Definitions:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test inline actor-node relationships
|
||||
function testInlineActorNodeRelationships() {
|
||||
const input = `usecase
|
||||
actor Developer1 --> a(Go through code)
|
||||
actor Developer2 --> b(Go through implementation)`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Inline Actor-Node Relationships:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test mixed syntax
|
||||
function testMixedSyntax() {
|
||||
const input = `usecase
|
||||
actor Tester1
|
||||
Tester1 --> c(Go through testing)
|
||||
actor Developer1 --> a(Go through code)
|
||||
actor Developer2 --> b(Go through implementation)`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Mixed Syntax:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test edge labels
|
||||
function testEdgeLabels() {
|
||||
const input = `usecase
|
||||
actor Developer1
|
||||
Developer1 --task2--> c(Go through testing)`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Edge Labels:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test edge labels with inline syntax
|
||||
function testInlineEdgeLabels() {
|
||||
const input = `usecase
|
||||
actor Developer1 --task1--> a(Go through code)`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Inline Edge Labels:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Test mixed edge labels and regular arrows
|
||||
function testMixedEdgeLabels() {
|
||||
const input = `usecase
|
||||
actor Developer1
|
||||
actor Tester1
|
||||
Developer1 --task1--> a(Go through code)
|
||||
Tester1 --> b(Go through testing)`;
|
||||
|
||||
const result = parseUsecase(input);
|
||||
console.log('Test Mixed Edge Labels:');
|
||||
console.log('Success:', result.success);
|
||||
if (result.success && result.ast) {
|
||||
console.log('Statements:', result.ast.statements.length);
|
||||
console.log('AST:', JSON.stringify(result.ast, null, 2));
|
||||
} else {
|
||||
console.log('Errors:', result.errors);
|
||||
}
|
||||
console.log('---');
|
||||
}
|
||||
|
||||
// Run tests
|
||||
console.log('Running Usecase Parser Tests...\n');
|
||||
testBasicUsecaseParsing();
|
||||
testSimpleUsecaseParsing();
|
||||
testMetadataParsing();
|
||||
testComplexMetadataParsing();
|
||||
testMixedActorsParsing();
|
||||
testMultipleActorsSingleLine();
|
||||
testMultipleActorsWithMetadata();
|
||||
testFiveActorsSingleLine();
|
||||
testSystemBoundaryParsing();
|
||||
testMixedActorsAndBoundaries();
|
||||
testCurlyBraceSystemBoundary();
|
||||
testRelationshipParsing();
|
||||
testCompleteExample();
|
||||
testNodeDefinitions();
|
||||
testInlineActorNodeRelationships();
|
||||
testMixedSyntax();
|
||||
testEdgeLabels();
|
||||
testInlineEdgeLabels();
|
||||
testMixedEdgeLabels();
|
||||
console.log('Tests completed.');
|
752
packages/parser/src/language/useCase/usecaseParser.ts
Normal file
752
packages/parser/src/language/useCase/usecaseParser.ts
Normal file
@@ -0,0 +1,752 @@
|
||||
// Simple tokenizer and parser for usecase diagrams
|
||||
// This approach is more compatible with the mermaid build system
|
||||
import type {
|
||||
UsecaseDiagram,
|
||||
Statement,
|
||||
Actor,
|
||||
Usecase,
|
||||
SystemBoundary,
|
||||
SystemBoundaryMetadata,
|
||||
ActorUseCaseRelationship,
|
||||
Node,
|
||||
ActorNodeRelationship,
|
||||
InlineActorNodeRelationship,
|
||||
ParseResult
|
||||
} from './usecaseTypes.js';
|
||||
|
||||
// Token types
|
||||
enum TokenType {
|
||||
USECASE_START = 'USECASE_START',
|
||||
ACTOR = 'ACTOR',
|
||||
SYSTEM_BOUNDARY = 'SYSTEM_BOUNDARY',
|
||||
END = 'END',
|
||||
ARROW = 'ARROW',
|
||||
LABELED_ARROW = 'LABELED_ARROW',
|
||||
AT = 'AT',
|
||||
LBRACE = 'LBRACE',
|
||||
RBRACE = 'RBRACE',
|
||||
LPAREN = 'LPAREN',
|
||||
RPAREN = 'RPAREN',
|
||||
COMMA = 'COMMA',
|
||||
COLON = 'COLON',
|
||||
STRING = 'STRING',
|
||||
IDENTIFIER = 'IDENTIFIER',
|
||||
NEWLINE = 'NEWLINE',
|
||||
EOF = 'EOF'
|
||||
}
|
||||
|
||||
interface Token {
|
||||
type: TokenType;
|
||||
value: string;
|
||||
line: number;
|
||||
column: number;
|
||||
}
|
||||
|
||||
class UsecaseLexer {
|
||||
private input: string;
|
||||
private position: number = 0;
|
||||
private line: number = 1;
|
||||
private column: number = 1;
|
||||
|
||||
constructor(input: string) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
tokenize(): Token[] {
|
||||
const tokens: Token[] = [];
|
||||
|
||||
while (this.position < this.input.length) {
|
||||
this.skipWhitespace();
|
||||
|
||||
if (this.position >= this.input.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
const token = this.nextToken();
|
||||
if (token) {
|
||||
tokens.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
tokens.push({
|
||||
type: TokenType.EOF,
|
||||
value: '',
|
||||
line: this.line,
|
||||
column: this.column
|
||||
});
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private nextToken(): Token | null {
|
||||
const startLine = this.line;
|
||||
const startColumn = this.column;
|
||||
|
||||
// Skip comments
|
||||
if (this.peek() === '%') {
|
||||
this.skipComment();
|
||||
return null;
|
||||
}
|
||||
|
||||
// Newlines
|
||||
if (this.peek() === '\n' || this.peek() === '\r') {
|
||||
this.advance();
|
||||
if (this.peek() === '\n') {
|
||||
this.advance();
|
||||
}
|
||||
this.line++;
|
||||
this.column = 1;
|
||||
return {
|
||||
type: TokenType.NEWLINE,
|
||||
value: '\n',
|
||||
line: startLine,
|
||||
column: startColumn
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Strings
|
||||
if (this.peek() === '"' || this.peek() === "'") {
|
||||
return this.readString(startLine, startColumn);
|
||||
}
|
||||
|
||||
// Arrow tokens (-->, ->, --label-->, --label->)
|
||||
if (this.peek() === '-') {
|
||||
if (this.peek(1) === '-') {
|
||||
// Check for labeled arrow: --label--> or --label->
|
||||
const labeledArrowMatch = this.tryParseLabeledArrow();
|
||||
if (labeledArrowMatch) {
|
||||
return labeledArrowMatch;
|
||||
}
|
||||
|
||||
// Regular arrow: -->
|
||||
if (this.peek(2) === '>') {
|
||||
this.advance(3);
|
||||
return { type: TokenType.ARROW, value: '-->', line: startLine, column: startColumn };
|
||||
}
|
||||
} else if (this.peek(1) === '>') {
|
||||
// Regular arrow: ->
|
||||
this.advance(2);
|
||||
return { type: TokenType.ARROW, value: '->', line: startLine, column: startColumn };
|
||||
}
|
||||
}
|
||||
|
||||
// Single character tokens
|
||||
switch (this.peek()) {
|
||||
case '@':
|
||||
this.advance();
|
||||
return { type: TokenType.AT, value: '@', line: startLine, column: startColumn };
|
||||
case '{':
|
||||
this.advance();
|
||||
return { type: TokenType.LBRACE, value: '{', line: startLine, column: startColumn };
|
||||
case '}':
|
||||
this.advance();
|
||||
return { type: TokenType.RBRACE, value: '}', line: startLine, column: startColumn };
|
||||
case ',':
|
||||
this.advance();
|
||||
return { type: TokenType.COMMA, value: ',', line: startLine, column: startColumn };
|
||||
case ':':
|
||||
this.advance();
|
||||
return { type: TokenType.COLON, value: ':', line: startLine, column: startColumn };
|
||||
case '(':
|
||||
this.advance();
|
||||
return { type: TokenType.LPAREN, value: '(', line: startLine, column: startColumn };
|
||||
case ')':
|
||||
this.advance();
|
||||
return { type: TokenType.RPAREN, value: ')', line: startLine, column: startColumn };
|
||||
}
|
||||
|
||||
// Keywords and identifiers
|
||||
if (this.isAlpha(this.peek())) {
|
||||
return this.readIdentifierOrKeyword(startLine, startColumn);
|
||||
}
|
||||
|
||||
// Skip unknown characters
|
||||
this.advance();
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private readIdentifierOrKeyword(line: number, column: number): Token {
|
||||
let value = '';
|
||||
|
||||
while (this.position < this.input.length &&
|
||||
(this.isAlphaNumeric(this.peek()) || this.peek() === '_')) {
|
||||
value += this.peek();
|
||||
this.advance();
|
||||
}
|
||||
|
||||
// Check for keywords
|
||||
const type = this.getKeywordType(value);
|
||||
|
||||
return {
|
||||
type,
|
||||
value,
|
||||
line,
|
||||
column
|
||||
};
|
||||
}
|
||||
|
||||
private readString(line: number, column: number): Token {
|
||||
const quote = this.peek();
|
||||
this.advance(); // Skip opening quote
|
||||
|
||||
let value = '';
|
||||
while (this.position < this.input.length && this.peek() !== quote) {
|
||||
value += this.peek();
|
||||
this.advance();
|
||||
}
|
||||
|
||||
if (this.peek() === quote) {
|
||||
this.advance(); // Skip closing quote
|
||||
}
|
||||
|
||||
return {
|
||||
type: TokenType.STRING,
|
||||
value: value, // Return the content without quotes
|
||||
line,
|
||||
column
|
||||
};
|
||||
}
|
||||
|
||||
private getKeywordType(value: string): TokenType {
|
||||
switch (value.toLowerCase()) {
|
||||
case 'usecase': return TokenType.USECASE_START;
|
||||
case 'actor': return TokenType.ACTOR;
|
||||
case 'systemboundary': return TokenType.SYSTEM_BOUNDARY;
|
||||
case 'end': return TokenType.END;
|
||||
default: return TokenType.IDENTIFIER;
|
||||
}
|
||||
}
|
||||
|
||||
private skipWhitespace(): void {
|
||||
while (this.position < this.input.length &&
|
||||
(this.peek() === ' ' || this.peek() === '\t')) {
|
||||
this.advance();
|
||||
}
|
||||
}
|
||||
|
||||
private skipComment(): void {
|
||||
while (this.position < this.input.length &&
|
||||
this.peek() !== '\n' && this.peek() !== '\r') {
|
||||
this.advance();
|
||||
}
|
||||
}
|
||||
|
||||
private peek(offset: number = 0): string {
|
||||
const pos = this.position + offset;
|
||||
return pos < this.input.length ? this.input[pos] : '';
|
||||
}
|
||||
|
||||
private tryParseLabeledArrow(): Token | null {
|
||||
// Try to parse --label--> or --label->
|
||||
const startPos = this.position;
|
||||
const startLine = this.line;
|
||||
const startColumn = this.column;
|
||||
|
||||
// Skip initial '--'
|
||||
if (this.peek() !== '-' || this.peek(1) !== '-') {
|
||||
return null;
|
||||
}
|
||||
|
||||
let pos = 2;
|
||||
let label = '';
|
||||
|
||||
// Read the label
|
||||
while (pos < this.input.length - this.position) {
|
||||
const char = this.peek(pos);
|
||||
if (char === '-') {
|
||||
// Check if this is the end pattern
|
||||
if (this.peek(pos + 1) === '-' && this.peek(pos + 2) === '>') {
|
||||
// Found --label-->
|
||||
this.advance(pos + 3);
|
||||
return {
|
||||
type: TokenType.LABELED_ARROW,
|
||||
value: `--${label}-->`,
|
||||
line: startLine,
|
||||
column: startColumn
|
||||
};
|
||||
} else if (this.peek(pos + 1) === '>') {
|
||||
// Found --label->
|
||||
this.advance(pos + 2);
|
||||
return {
|
||||
type: TokenType.LABELED_ARROW,
|
||||
value: `--${label}->`,
|
||||
line: startLine,
|
||||
column: startColumn
|
||||
};
|
||||
} else {
|
||||
label += char;
|
||||
pos++;
|
||||
}
|
||||
} else if (char.match(/[a-zA-Z0-9_]/)) {
|
||||
label += char;
|
||||
pos++;
|
||||
} else {
|
||||
// Invalid character in label
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private advance(count: number = 1): void {
|
||||
for (let i = 0; i < count && this.position < this.input.length; i++) {
|
||||
this.position++;
|
||||
this.column++;
|
||||
}
|
||||
}
|
||||
|
||||
private isAlpha(char: string): boolean {
|
||||
return /[a-zA-Z]/.test(char);
|
||||
}
|
||||
|
||||
private isAlphaNumeric(char: string): boolean {
|
||||
return /[a-zA-Z0-9]/.test(char);
|
||||
}
|
||||
}
|
||||
|
||||
class UsecaseParser {
|
||||
private tokens: Token[];
|
||||
private position: number = 0;
|
||||
|
||||
constructor(tokens: Token[]) {
|
||||
this.tokens = tokens;
|
||||
}
|
||||
|
||||
parse(): UsecaseDiagram {
|
||||
const statements: Statement[] = [];
|
||||
|
||||
// Expect 'usecase' keyword at the start
|
||||
this.consume(TokenType.USECASE_START);
|
||||
this.skipNewlines();
|
||||
|
||||
while (!this.isAtEnd()) {
|
||||
this.skipNewlines();
|
||||
|
||||
if (this.isAtEnd()) {
|
||||
break;
|
||||
}
|
||||
|
||||
const parsedStatements = this.parseStatement();
|
||||
if (parsedStatements) {
|
||||
if (Array.isArray(parsedStatements)) {
|
||||
statements.push(...parsedStatements);
|
||||
} else {
|
||||
statements.push(parsedStatements);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'usecaseDiagram',
|
||||
statements
|
||||
};
|
||||
}
|
||||
|
||||
private parseStatement(): Statement | Statement[] | null {
|
||||
const token = this.peek();
|
||||
|
||||
switch (token.type) {
|
||||
case TokenType.ACTOR:
|
||||
return this.parseActorStatement();
|
||||
case TokenType.SYSTEM_BOUNDARY:
|
||||
return this.parseSystemBoundary();
|
||||
case TokenType.IDENTIFIER:
|
||||
// Look ahead to see if this is a systemBoundaryMetadata, relationship, or use case
|
||||
if (this.isSystemBoundaryMetadata()) {
|
||||
return this.parseSystemBoundaryMetadata();
|
||||
} else if (this.isRelationship()) {
|
||||
return this.parseRelationship();
|
||||
} else {
|
||||
return this.parseUseCase();
|
||||
}
|
||||
default:
|
||||
this.advance(); // Skip unknown tokens
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private parseActorStatement(): Statement | Statement[] {
|
||||
this.consume(TokenType.ACTOR);
|
||||
|
||||
// Check if this is an inline actor-node relationship
|
||||
// Look ahead: IDENTIFIER ARROW IDENTIFIER LPAREN
|
||||
if (this.isInlineActorNodeRelationship()) {
|
||||
return this.parseInlineActorNodeRelationship();
|
||||
}
|
||||
|
||||
const actors: Actor[] = [];
|
||||
|
||||
// Parse first actor
|
||||
actors.push(this.parseActorDefinition());
|
||||
|
||||
// Parse additional actors separated by commas
|
||||
while (this.check(TokenType.COMMA)) {
|
||||
this.consume(TokenType.COMMA);
|
||||
actors.push(this.parseActorDefinition());
|
||||
}
|
||||
|
||||
return actors;
|
||||
}
|
||||
|
||||
private parseActorDefinition(): Actor {
|
||||
const name = this.consume(TokenType.IDENTIFIER).value;
|
||||
|
||||
let metadata: Record<string, string> | undefined;
|
||||
|
||||
// Check for optional metadata
|
||||
if (this.check(TokenType.AT)) {
|
||||
metadata = this.parseMetadata();
|
||||
}
|
||||
|
||||
const actor: Actor = { type: 'actor', name };
|
||||
if (metadata) {
|
||||
actor.metadata = metadata;
|
||||
}
|
||||
|
||||
return actor;
|
||||
}
|
||||
|
||||
private parseSystemBoundary(): SystemBoundary {
|
||||
this.consume(TokenType.SYSTEM_BOUNDARY);
|
||||
const name = this.consume(TokenType.IDENTIFIER).value;
|
||||
this.consume(TokenType.LBRACE);
|
||||
|
||||
// Skip newlines after opening brace
|
||||
this.skipNewlines();
|
||||
|
||||
const useCases: Usecase[] = [];
|
||||
|
||||
// Parse use cases until we hit closing brace
|
||||
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
||||
this.skipNewlines();
|
||||
|
||||
if (this.check(TokenType.RBRACE) || this.isAtEnd()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.check(TokenType.IDENTIFIER)) {
|
||||
const useCase = this.parseUseCase();
|
||||
if (useCase) {
|
||||
useCases.push(useCase as Usecase);
|
||||
}
|
||||
} else {
|
||||
this.advance(); // Skip unknown tokens
|
||||
}
|
||||
}
|
||||
|
||||
this.consume(TokenType.RBRACE);
|
||||
|
||||
return {
|
||||
type: 'systemBoundary',
|
||||
name,
|
||||
useCases
|
||||
};
|
||||
}
|
||||
|
||||
private parseSystemBoundaryMetadata(): SystemBoundaryMetadata {
|
||||
const name = this.consume(TokenType.IDENTIFIER).value;
|
||||
this.consume(TokenType.AT);
|
||||
this.consume(TokenType.LBRACE);
|
||||
|
||||
const metadata: Record<string, string> = {};
|
||||
|
||||
// Parse metadata content
|
||||
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
||||
if (this.check(TokenType.IDENTIFIER)) {
|
||||
const key = this.consume(TokenType.IDENTIFIER).value;
|
||||
this.consume(TokenType.COLON);
|
||||
|
||||
let value = '';
|
||||
if (this.check(TokenType.STRING)) {
|
||||
value = this.consume(TokenType.STRING).value;
|
||||
// Remove quotes from string value
|
||||
value = value.slice(1, -1);
|
||||
} else if (this.check(TokenType.IDENTIFIER)) {
|
||||
value = this.consume(TokenType.IDENTIFIER).value;
|
||||
}
|
||||
|
||||
metadata[key] = value;
|
||||
|
||||
// Optional comma
|
||||
if (this.check(TokenType.COMMA)) {
|
||||
this.advance();
|
||||
}
|
||||
} else {
|
||||
this.advance(); // Skip unknown tokens
|
||||
}
|
||||
}
|
||||
|
||||
this.consume(TokenType.RBRACE);
|
||||
|
||||
return {
|
||||
type: 'systemBoundaryMetadata',
|
||||
name,
|
||||
metadata
|
||||
};
|
||||
}
|
||||
|
||||
private parseUseCase(): Usecase {
|
||||
const name = this.consume(TokenType.IDENTIFIER).value;
|
||||
|
||||
return {
|
||||
type: 'usecase',
|
||||
name
|
||||
};
|
||||
}
|
||||
|
||||
private isRelationship(): boolean {
|
||||
// Look ahead to see if there's an arrow after the identifier
|
||||
const currentPos = this.position;
|
||||
this.advance(); // Skip the identifier
|
||||
const hasArrow = this.check(TokenType.ARROW) || this.check(TokenType.LABELED_ARROW);
|
||||
this.position = currentPos; // Reset position
|
||||
return hasArrow;
|
||||
}
|
||||
|
||||
private isSystemBoundaryMetadata(): boolean {
|
||||
// Look ahead to see if there's an @ after the identifier
|
||||
const currentPos = this.position;
|
||||
this.advance(); // Skip the identifier
|
||||
const hasAt = this.check(TokenType.AT);
|
||||
this.position = currentPos; // Reset position
|
||||
return hasAt;
|
||||
}
|
||||
|
||||
private parseRelationship(): ActorUseCaseRelationship | ActorNodeRelationship {
|
||||
const from = this.consume(TokenType.IDENTIFIER).value;
|
||||
|
||||
let arrowToken: Token;
|
||||
let label: string | undefined;
|
||||
|
||||
if (this.check(TokenType.LABELED_ARROW)) {
|
||||
arrowToken = this.consume(TokenType.LABELED_ARROW);
|
||||
// Extract label from --label--> or --label->
|
||||
const arrowValue = arrowToken.value;
|
||||
const match = arrowValue.match(/^--(.+?)-+>$/);
|
||||
if (match) {
|
||||
label = match[1];
|
||||
}
|
||||
} else {
|
||||
arrowToken = this.consume(TokenType.ARROW);
|
||||
}
|
||||
|
||||
// Check if target is a node definition (ID followed by parentheses)
|
||||
if (this.isNodeDefinition()) {
|
||||
const node = this.parseNodeDefinition();
|
||||
return {
|
||||
type: 'actorNodeRelationship',
|
||||
from,
|
||||
to: node.id,
|
||||
arrow: arrowToken.value,
|
||||
label
|
||||
};
|
||||
} else {
|
||||
const to = this.consume(TokenType.IDENTIFIER).value;
|
||||
return {
|
||||
type: 'actorUseCaseRelationship',
|
||||
from,
|
||||
to,
|
||||
arrow: arrowToken.value,
|
||||
label
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private isInlineActorNodeRelationship(): boolean {
|
||||
// Look ahead: IDENTIFIER (ARROW|LABELED_ARROW) IDENTIFIER LPAREN
|
||||
const currentPos = this.position;
|
||||
|
||||
if (!this.check(TokenType.IDENTIFIER)) {
|
||||
this.position = currentPos;
|
||||
return false;
|
||||
}
|
||||
this.advance(); // Skip actor name
|
||||
|
||||
if (!this.check(TokenType.ARROW) && !this.check(TokenType.LABELED_ARROW)) {
|
||||
this.position = currentPos;
|
||||
return false;
|
||||
}
|
||||
this.advance(); // Skip arrow
|
||||
|
||||
if (!this.check(TokenType.IDENTIFIER)) {
|
||||
this.position = currentPos;
|
||||
return false;
|
||||
}
|
||||
this.advance(); // Skip node ID
|
||||
|
||||
const hasLParen = this.check(TokenType.LPAREN);
|
||||
this.position = currentPos; // Reset position
|
||||
return hasLParen;
|
||||
}
|
||||
|
||||
private parseInlineActorNodeRelationship(): InlineActorNodeRelationship {
|
||||
const actor = this.consume(TokenType.IDENTIFIER).value;
|
||||
|
||||
let arrowToken: Token;
|
||||
let label: string | undefined;
|
||||
|
||||
if (this.check(TokenType.LABELED_ARROW)) {
|
||||
arrowToken = this.consume(TokenType.LABELED_ARROW);
|
||||
// Extract label from --label--> or --label->
|
||||
const arrowValue = arrowToken.value;
|
||||
const match = arrowValue.match(/^--(.+?)-+>$/);
|
||||
if (match) {
|
||||
label = match[1];
|
||||
}
|
||||
} else {
|
||||
arrowToken = this.consume(TokenType.ARROW);
|
||||
}
|
||||
|
||||
const node = this.parseNodeDefinition();
|
||||
|
||||
return {
|
||||
type: 'inlineActorNodeRelationship',
|
||||
actor,
|
||||
node,
|
||||
arrow: arrowToken.value,
|
||||
label
|
||||
};
|
||||
}
|
||||
|
||||
private isNodeDefinition(): boolean {
|
||||
// Look ahead: IDENTIFIER LPAREN
|
||||
const currentPos = this.position;
|
||||
|
||||
if (!this.check(TokenType.IDENTIFIER)) {
|
||||
this.position = currentPos;
|
||||
return false;
|
||||
}
|
||||
this.advance(); // Skip node ID
|
||||
|
||||
const hasLParen = this.check(TokenType.LPAREN);
|
||||
this.position = currentPos; // Reset position
|
||||
return hasLParen;
|
||||
}
|
||||
|
||||
private parseNodeDefinition(): Node {
|
||||
const id = this.consume(TokenType.IDENTIFIER).value;
|
||||
this.consume(TokenType.LPAREN);
|
||||
|
||||
// Parse node label (can be multiple words or a string)
|
||||
let label = '';
|
||||
if (this.check(TokenType.STRING)) {
|
||||
label = this.consume(TokenType.STRING).value;
|
||||
// Remove quotes
|
||||
label = label.slice(1, -1);
|
||||
} else {
|
||||
// Parse multiple identifiers as label
|
||||
const labelParts: string[] = [];
|
||||
while (this.check(TokenType.IDENTIFIER) && !this.check(TokenType.RPAREN)) {
|
||||
labelParts.push(this.consume(TokenType.IDENTIFIER).value);
|
||||
}
|
||||
label = labelParts.join(' ');
|
||||
}
|
||||
|
||||
this.consume(TokenType.RPAREN);
|
||||
|
||||
return {
|
||||
type: 'node',
|
||||
id,
|
||||
label
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
private parseMetadata(): Record<string, string> {
|
||||
this.consume(TokenType.AT);
|
||||
this.consume(TokenType.LBRACE);
|
||||
|
||||
const metadata: Record<string, string> = {};
|
||||
|
||||
// Handle empty metadata
|
||||
if (this.check(TokenType.RBRACE)) {
|
||||
this.consume(TokenType.RBRACE);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
// Parse key-value pairs
|
||||
do {
|
||||
const key = this.consume(TokenType.IDENTIFIER).value;
|
||||
this.consume(TokenType.COLON);
|
||||
|
||||
let value: string;
|
||||
if (this.check(TokenType.STRING)) {
|
||||
value = this.consume(TokenType.STRING).value;
|
||||
} else {
|
||||
value = this.consume(TokenType.IDENTIFIER).value;
|
||||
}
|
||||
|
||||
metadata[key] = value;
|
||||
|
||||
// Check for comma (more pairs) or closing brace
|
||||
if (this.check(TokenType.COMMA)) {
|
||||
this.consume(TokenType.COMMA);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} while (!this.check(TokenType.RBRACE) && !this.isAtEnd());
|
||||
|
||||
this.consume(TokenType.RBRACE);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private skipNewlines(): void {
|
||||
while (this.check(TokenType.NEWLINE)) {
|
||||
this.advance();
|
||||
}
|
||||
}
|
||||
|
||||
private peek(): Token {
|
||||
return this.tokens[this.position];
|
||||
}
|
||||
|
||||
private advance(): Token {
|
||||
if (!this.isAtEnd()) {
|
||||
this.position++;
|
||||
}
|
||||
return this.tokens[this.position - 1];
|
||||
}
|
||||
|
||||
private check(type: TokenType): boolean {
|
||||
if (this.isAtEnd()) return false;
|
||||
return this.peek().type === type;
|
||||
}
|
||||
|
||||
private consume(type: TokenType): Token {
|
||||
if (this.check(type)) {
|
||||
return this.advance();
|
||||
}
|
||||
|
||||
const current = this.peek();
|
||||
throw new Error(`Expected ${type}, got ${current.type} at line ${current.line}`);
|
||||
}
|
||||
|
||||
private isAtEnd(): boolean {
|
||||
return this.position >= this.tokens.length || this.peek().type === TokenType.EOF;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseUsecase(input: string): ParseResult {
|
||||
try {
|
||||
const lexer = new UsecaseLexer(input);
|
||||
const tokens = lexer.tokenize();
|
||||
const parser = new UsecaseParser(tokens);
|
||||
const ast = parser.parse();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
ast
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
errors: [error instanceof Error ? error.message : String(error)]
|
||||
};
|
||||
}
|
||||
}
|
113
packages/parser/src/language/useCase/usecaseTypes.ts
Normal file
113
packages/parser/src/language/useCase/usecaseTypes.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
// AST types for usecase diagrams
|
||||
|
||||
export interface UsecaseDiagram {
|
||||
type: 'usecaseDiagram';
|
||||
statements: Statement[];
|
||||
}
|
||||
|
||||
export type Statement = Actor | SystemBoundary | SystemBoundaryMetadata | Usecase | Relationship | ActorUseCaseRelationship | Node | ActorNodeRelationship | InlineActorNodeRelationship;
|
||||
|
||||
export interface Title {
|
||||
type: 'title';
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface AccDescr {
|
||||
type: 'accDescr';
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface AccTitle {
|
||||
type: 'accTitle';
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface Actor {
|
||||
type: 'actor';
|
||||
name: string;
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface Usecase {
|
||||
type: 'usecase';
|
||||
name: string;
|
||||
alias?: string;
|
||||
}
|
||||
|
||||
export interface SystemBoundary {
|
||||
type: 'systemBoundary';
|
||||
name: string;
|
||||
useCases: Usecase[];
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface SystemBoundaryMetadata {
|
||||
type: 'systemBoundaryMetadata';
|
||||
name: string; // boundary name
|
||||
metadata: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ActorUseCaseRelationship {
|
||||
type: 'actorUseCaseRelationship';
|
||||
from: string; // actor name
|
||||
to: string; // use case name
|
||||
arrow: string; // '-->' or '->'
|
||||
label?: string; // edge label (optional)
|
||||
}
|
||||
|
||||
export interface Node {
|
||||
type: 'node';
|
||||
id: string; // node ID (e.g., 'a', 'b', 'c')
|
||||
label: string; // node label (e.g., 'Go through code')
|
||||
}
|
||||
|
||||
export interface ActorNodeRelationship {
|
||||
type: 'actorNodeRelationship';
|
||||
from: string; // actor name
|
||||
to: string; // node ID
|
||||
arrow: string; // '-->' or '->'
|
||||
label?: string; // edge label (optional)
|
||||
}
|
||||
|
||||
export interface InlineActorNodeRelationship {
|
||||
type: 'inlineActorNodeRelationship';
|
||||
actor: string; // actor name
|
||||
node: Node; // node definition
|
||||
arrow: string; // '-->' or '->'
|
||||
label?: string; // edge label (optional)
|
||||
}
|
||||
|
||||
export interface Relationship {
|
||||
type: 'relationship';
|
||||
from: string;
|
||||
to: string;
|
||||
relationshipType: RelationshipType;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface Note {
|
||||
type: 'note';
|
||||
position: NotePosition;
|
||||
target: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export type RelationshipType =
|
||||
| 'arrow-left'
|
||||
| 'arrow-right'
|
||||
| 'arrow-both'
|
||||
| 'extends'
|
||||
| 'includes';
|
||||
|
||||
export type NotePosition =
|
||||
| 'left'
|
||||
| 'right'
|
||||
| 'top'
|
||||
| 'bottom';
|
||||
|
||||
// Parser result type
|
||||
export interface ParseResult {
|
||||
success: boolean;
|
||||
ast?: UsecaseDiagram;
|
||||
errors?: string[];
|
||||
}
|
Reference in New Issue
Block a user