Compare commits

..

1 Commits

Author SHA1 Message Date
omkarht
89b29898d2 implemented demo usecase diagram with antlr flow 2025-09-02 14:05:20 +05:30
25 changed files with 6244 additions and 93 deletions

View File

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

View File

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

View File

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

View File

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

View 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;

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

View 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;

File diff suppressed because it is too large Load Diff

View 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();
}
},
};

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View 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

View File

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

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

View 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

File diff suppressed because it is too large Load Diff

View 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
;

View File

@@ -0,0 +1,2 @@
export { parseUsecase } from './usecaseParser.js';
export * from './usecaseTypes.js';

View 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.');

View 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)]
};
}
}

View 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[];
}