mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-21 08:19:43 +02:00
feat: Adding support for the new Usecase diagram type
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
This commit is contained in:
@@ -19,7 +19,8 @@
|
||||
"scripts": {
|
||||
"clean": "rimraf dist src/language/generated",
|
||||
"langium:generate": "langium generate",
|
||||
"langium:watch": "langium generate --watch"
|
||||
"langium:watch": "langium generate --watch",
|
||||
"antlr:generate": "cd src/language/usecase && npx antlr-ng -Dlanguage=TypeScript --generate-visitor --generate-listener -o generated Usecase.g4"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -33,9 +34,11 @@
|
||||
"ast"
|
||||
],
|
||||
"dependencies": {
|
||||
"antlr4ng": "^3.0.7",
|
||||
"langium": "3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"antlr-ng": "^1.0.10",
|
||||
"chevrotain": "^11.0.3"
|
||||
},
|
||||
"files": [
|
||||
|
@@ -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';
|
||||
|
170
packages/parser/src/language/usecase/Usecase.g4
Normal file
170
packages/parser/src/language/usecase/Usecase.g4
Normal file
@@ -0,0 +1,170 @@
|
||||
grammar Usecase;
|
||||
|
||||
// Parser rules
|
||||
usecaseDiagram
|
||||
: 'usecase' NEWLINE* statement* EOF
|
||||
;
|
||||
|
||||
statement
|
||||
: actorStatement
|
||||
| relationshipStatement
|
||||
| systemBoundaryStatement
|
||||
| systemBoundaryTypeStatement
|
||||
| NEWLINE
|
||||
;
|
||||
|
||||
actorStatement
|
||||
: 'actor' actorList NEWLINE*
|
||||
;
|
||||
|
||||
actorList
|
||||
: actorName (',' actorName)*
|
||||
;
|
||||
|
||||
actorName
|
||||
: (IDENTIFIER | STRING) metadata?
|
||||
;
|
||||
|
||||
metadata
|
||||
: '@' '{' metadataContent '}'
|
||||
;
|
||||
|
||||
metadataContent
|
||||
: metadataProperty (',' metadataProperty)*
|
||||
;
|
||||
|
||||
metadataProperty
|
||||
: STRING ':' STRING
|
||||
;
|
||||
|
||||
|
||||
|
||||
relationshipStatement
|
||||
: entityName arrow entityName NEWLINE*
|
||||
| actorDeclaration arrow entityName NEWLINE*
|
||||
;
|
||||
|
||||
systemBoundaryStatement
|
||||
: 'systemBoundary' systemBoundaryName NEWLINE* systemBoundaryContent* 'end' NEWLINE*
|
||||
;
|
||||
|
||||
systemBoundaryName
|
||||
: IDENTIFIER
|
||||
| STRING
|
||||
;
|
||||
|
||||
systemBoundaryContent
|
||||
: usecaseInBoundary NEWLINE*
|
||||
| NEWLINE
|
||||
;
|
||||
|
||||
usecaseInBoundary
|
||||
: IDENTIFIER
|
||||
| STRING
|
||||
;
|
||||
|
||||
systemBoundaryTypeStatement
|
||||
: systemBoundaryName '@' '{' systemBoundaryTypeContent '}' NEWLINE*
|
||||
;
|
||||
|
||||
systemBoundaryTypeContent
|
||||
: systemBoundaryTypeProperty (',' systemBoundaryTypeProperty)*
|
||||
;
|
||||
|
||||
systemBoundaryTypeProperty
|
||||
: 'type' ':' systemBoundaryType
|
||||
;
|
||||
|
||||
systemBoundaryType
|
||||
: 'package'
|
||||
| 'rect'
|
||||
;
|
||||
|
||||
entityName
|
||||
: IDENTIFIER
|
||||
| STRING
|
||||
| nodeIdWithLabel
|
||||
;
|
||||
|
||||
actorDeclaration
|
||||
: 'actor' actorName
|
||||
;
|
||||
|
||||
nodeIdWithLabel
|
||||
: IDENTIFIER '(' nodeLabel ')'
|
||||
;
|
||||
|
||||
nodeLabel
|
||||
: IDENTIFIER
|
||||
| STRING
|
||||
| nodeLabel IDENTIFIER
|
||||
| nodeLabel STRING
|
||||
;
|
||||
|
||||
arrow
|
||||
: SOLID_ARROW
|
||||
| BACK_ARROW
|
||||
| LINE_SOLID
|
||||
| labeledArrow
|
||||
;
|
||||
|
||||
labeledArrow
|
||||
: LINE_SOLID edgeLabel SOLID_ARROW
|
||||
| BACK_ARROW edgeLabel LINE_SOLID
|
||||
| LINE_SOLID edgeLabel LINE_SOLID
|
||||
;
|
||||
|
||||
edgeLabel
|
||||
: IDENTIFIER
|
||||
| STRING
|
||||
;
|
||||
|
||||
// Lexer rules
|
||||
SOLID_ARROW
|
||||
: '-->'
|
||||
;
|
||||
|
||||
BACK_ARROW
|
||||
: '<--'
|
||||
;
|
||||
|
||||
LINE_SOLID
|
||||
: '--'
|
||||
;
|
||||
|
||||
COMMA
|
||||
: ','
|
||||
;
|
||||
|
||||
AT
|
||||
: '@'
|
||||
;
|
||||
|
||||
LBRACE
|
||||
: '{'
|
||||
;
|
||||
|
||||
RBRACE
|
||||
: '}'
|
||||
;
|
||||
|
||||
COLON
|
||||
: ':'
|
||||
;
|
||||
|
||||
IDENTIFIER
|
||||
: [a-zA-Z_][a-zA-Z0-9_]*
|
||||
;
|
||||
|
||||
STRING
|
||||
: '"' (~["\r\n])* '"'
|
||||
| '\'' (~['\r\n])* '\''
|
||||
;
|
||||
|
||||
NEWLINE
|
||||
: [\r\n]+
|
||||
;
|
||||
|
||||
WS
|
||||
: [ \t]+ -> skip
|
||||
;
|
4
packages/parser/src/language/usecase/index.ts
Normal file
4
packages/parser/src/language/usecase/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './module.js';
|
||||
export * from './types.js';
|
||||
export * from './parser.js';
|
||||
export * from './visitor.js';
|
50
packages/parser/src/language/usecase/module.ts
Normal file
50
packages/parser/src/language/usecase/module.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* ANTLR UseCase Module
|
||||
*
|
||||
* This module provides dependency injection and service creation
|
||||
* for the ANTLR-based UseCase parser, following the Langium pattern.
|
||||
*/
|
||||
|
||||
import type { AntlrUsecaseServices } from './types.js';
|
||||
import { UsecaseAntlrParser } from './parser.js';
|
||||
import { UsecaseAntlrVisitor } from './visitor.js';
|
||||
|
||||
/**
|
||||
* ANTLR UseCase Module for dependency injection
|
||||
*/
|
||||
export const AntlrUsecaseModule = {
|
||||
parser: () => new UsecaseAntlrParser(),
|
||||
visitor: () => new UsecaseAntlrVisitor(),
|
||||
};
|
||||
|
||||
/**
|
||||
* Create the full set of ANTLR UseCase services
|
||||
*
|
||||
* This follows the Langium pattern but for ANTLR services
|
||||
*
|
||||
* @returns An object with ANTLR UseCase services
|
||||
*/
|
||||
export function createAntlrUsecaseServices(): AntlrUsecaseServices {
|
||||
const parser = new UsecaseAntlrParser();
|
||||
const visitor = new UsecaseAntlrVisitor();
|
||||
|
||||
return {
|
||||
parser,
|
||||
visitor,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton instance of ANTLR UseCase services
|
||||
*/
|
||||
let antlrUsecaseServices: AntlrUsecaseServices | undefined;
|
||||
|
||||
/**
|
||||
* Get or create the singleton ANTLR UseCase services
|
||||
*/
|
||||
export function getAntlrUsecaseServices(): AntlrUsecaseServices {
|
||||
if (!antlrUsecaseServices) {
|
||||
antlrUsecaseServices = createAntlrUsecaseServices();
|
||||
}
|
||||
return antlrUsecaseServices;
|
||||
}
|
194
packages/parser/src/language/usecase/parser.ts
Normal file
194
packages/parser/src/language/usecase/parser.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* True ANTLR Parser Implementation for UseCase Diagrams
|
||||
*
|
||||
* This parser uses the actual ANTLR-generated files from Usecase.g4
|
||||
* and implements the visitor pattern to build the AST.
|
||||
*/
|
||||
|
||||
import { CharStream, CommonTokenStream, BaseErrorListener } from 'antlr4ng';
|
||||
import type { RecognitionException, Recognizer } from 'antlr4ng';
|
||||
import { UsecaseLexer } from './generated/UsecaseLexer.js';
|
||||
import { UsecaseParser } from './generated/UsecaseParser.js';
|
||||
import { UsecaseAntlrVisitor } from './visitor.js';
|
||||
import type { AntlrUsecaseParser, UsecaseParseResult } from './types.js';
|
||||
|
||||
/**
|
||||
* Custom error listener for ANTLR parser to capture syntax errors
|
||||
*/
|
||||
class UsecaseErrorListener extends BaseErrorListener {
|
||||
private errors: string[] = [];
|
||||
|
||||
syntaxError(
|
||||
_recognizer: Recognizer<any>,
|
||||
_offendingSymbol: any,
|
||||
line: number,
|
||||
charPositionInLine: number,
|
||||
message: string,
|
||||
_e: RecognitionException | null
|
||||
): void {
|
||||
const errorMsg = `Syntax error at line ${line}:${charPositionInLine} - ${message}`;
|
||||
this.errors.push(errorMsg);
|
||||
}
|
||||
|
||||
reportAmbiguity(): void {
|
||||
// Optional: handle ambiguity reports
|
||||
}
|
||||
|
||||
reportAttemptingFullContext(): void {
|
||||
// Optional: handle full context attempts
|
||||
}
|
||||
|
||||
reportContextSensitivity(): void {
|
||||
// Optional: handle context sensitivity reports
|
||||
}
|
||||
|
||||
getErrors(): string[] {
|
||||
return this.errors;
|
||||
}
|
||||
|
||||
hasErrors(): boolean {
|
||||
return this.errors.length > 0;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.errors = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom error class for usecase parsing errors
|
||||
*/
|
||||
export class UsecaseParseError extends Error {
|
||||
public line?: number;
|
||||
public column?: number;
|
||||
public token?: string;
|
||||
public expected?: string[];
|
||||
public hash?: Record<string, any>;
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
details?: {
|
||||
line?: number;
|
||||
column?: number;
|
||||
token?: string;
|
||||
expected?: string[];
|
||||
}
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'UsecaseParseError';
|
||||
this.line = details?.line;
|
||||
this.column = details?.column;
|
||||
this.token = details?.token;
|
||||
this.expected = details?.expected;
|
||||
|
||||
// Create hash object similar to other diagram types
|
||||
this.hash = {
|
||||
text: details?.token ?? '',
|
||||
token: details?.token ?? '',
|
||||
line: details?.line?.toString() ?? '1',
|
||||
loc: {
|
||||
first_line: details?.line ?? 1,
|
||||
last_line: details?.line ?? 1,
|
||||
first_column: details?.column ?? 1,
|
||||
last_column: (details?.column ?? 1) + (details?.token?.length ?? 0),
|
||||
},
|
||||
expected: details?.expected ?? [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ANTLR-based UseCase parser implementation
|
||||
*/
|
||||
export class UsecaseAntlrParser implements AntlrUsecaseParser {
|
||||
private visitor: UsecaseAntlrVisitor;
|
||||
private errorListener: UsecaseErrorListener;
|
||||
|
||||
constructor() {
|
||||
this.visitor = new UsecaseAntlrVisitor();
|
||||
this.errorListener = new UsecaseErrorListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse UseCase diagram input using true ANTLR parsing
|
||||
*
|
||||
* @param input - The UseCase diagram text to parse
|
||||
* @returns Parsed result with actors, use cases, and relationships
|
||||
* @throws UsecaseParseError when syntax errors are encountered
|
||||
*/
|
||||
parse(input: string): UsecaseParseResult {
|
||||
// Clear previous errors
|
||||
this.errorListener.clear();
|
||||
|
||||
try {
|
||||
// Step 1: Create ANTLR input stream
|
||||
const chars = CharStream.fromString(input);
|
||||
|
||||
// Step 2: Create lexer from generated ANTLR lexer
|
||||
const lexer = new UsecaseLexer(chars);
|
||||
|
||||
// Add error listener to lexer
|
||||
lexer.removeErrorListeners();
|
||||
lexer.addErrorListener(this.errorListener);
|
||||
|
||||
// Step 3: Create token stream
|
||||
const tokens = new CommonTokenStream(lexer);
|
||||
|
||||
// Step 4: Create parser from generated ANTLR parser
|
||||
const parser = new UsecaseParser(tokens);
|
||||
|
||||
// Add error listener to parser
|
||||
parser.removeErrorListeners();
|
||||
parser.addErrorListener(this.errorListener);
|
||||
|
||||
// Step 5: Parse using the grammar rule: usecaseDiagram
|
||||
const tree = parser.usecaseDiagram();
|
||||
|
||||
// Check for syntax errors before proceeding
|
||||
if (this.errorListener.hasErrors()) {
|
||||
const errors = this.errorListener.getErrors();
|
||||
throw new UsecaseParseError(`Syntax error in usecase diagram: ${errors.join('; ')}`, {
|
||||
token: 'unknown',
|
||||
expected: ['valid usecase syntax'],
|
||||
});
|
||||
}
|
||||
|
||||
// Step 6: Visit the parse tree using our visitor
|
||||
this.visitor.visitUsecaseDiagram!(tree);
|
||||
|
||||
// Step 7: Get the parse result
|
||||
return this.visitor.getParseResult();
|
||||
} catch (error) {
|
||||
if (error instanceof UsecaseParseError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Handle other types of errors
|
||||
throw new UsecaseParseError(
|
||||
`Failed to parse usecase diagram: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
{
|
||||
token: 'unknown',
|
||||
expected: ['valid usecase syntax'],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create a new ANTLR UseCase parser
|
||||
*/
|
||||
export function createUsecaseAntlrParser(): AntlrUsecaseParser {
|
||||
return new UsecaseAntlrParser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function for parsing UseCase diagrams
|
||||
*
|
||||
* @param input - The UseCase diagram text to parse
|
||||
* @returns Parsed result with actors, use cases, and relationships
|
||||
*/
|
||||
export function parseUsecaseWithAntlr(input: string): UsecaseParseResult {
|
||||
const parser = createUsecaseAntlrParser();
|
||||
return parser.parse(input);
|
||||
}
|
70
packages/parser/src/language/usecase/types.ts
Normal file
70
packages/parser/src/language/usecase/types.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Type definitions for ANTLR UseCase parser
|
||||
*/
|
||||
|
||||
// Arrow types for usecase diagrams (similar to sequence diagram LINETYPE)
|
||||
export const ARROW_TYPE = {
|
||||
SOLID_ARROW: 0, // -->
|
||||
BACK_ARROW: 1, // <--
|
||||
LINE_SOLID: 2, // --
|
||||
} as const;
|
||||
|
||||
export type ArrowType = (typeof ARROW_TYPE)[keyof typeof ARROW_TYPE];
|
||||
|
||||
export type ActorMetadata = Record<string, string>;
|
||||
|
||||
export interface Actor {
|
||||
id: string;
|
||||
name: string;
|
||||
metadata?: ActorMetadata;
|
||||
}
|
||||
|
||||
export interface UseCase {
|
||||
id: string;
|
||||
name: string;
|
||||
nodeId?: string; // Optional node ID (e.g., 'a' in 'a(Go through code)')
|
||||
systemBoundary?: string; // Optional reference to system boundary
|
||||
}
|
||||
|
||||
export type SystemBoundaryType = 'package' | 'rect';
|
||||
|
||||
export interface SystemBoundary {
|
||||
id: string;
|
||||
name: string;
|
||||
useCases: string[]; // Array of use case IDs within this boundary
|
||||
type?: SystemBoundaryType; // Type of boundary rendering (default: 'rect')
|
||||
}
|
||||
|
||||
export interface Relationship {
|
||||
id: string;
|
||||
from: string;
|
||||
to: string;
|
||||
type: 'association' | 'include' | 'extend';
|
||||
arrowType: ArrowType;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface UsecaseParseResult {
|
||||
actors: Actor[];
|
||||
useCases: UseCase[];
|
||||
systemBoundaries: SystemBoundary[];
|
||||
relationships: Relationship[];
|
||||
accDescr?: string;
|
||||
accTitle?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ANTLR Parser Services interface
|
||||
*/
|
||||
export interface AntlrUsecaseServices {
|
||||
parser: AntlrUsecaseParser;
|
||||
visitor: any; // UsecaseAntlrVisitor - using any to avoid circular dependency
|
||||
}
|
||||
|
||||
/**
|
||||
* ANTLR Parser interface
|
||||
*/
|
||||
export interface AntlrUsecaseParser {
|
||||
parse(input: string): UsecaseParseResult;
|
||||
}
|
605
packages/parser/src/language/usecase/visitor.ts
Normal file
605
packages/parser/src/language/usecase/visitor.ts
Normal file
@@ -0,0 +1,605 @@
|
||||
/**
|
||||
* ANTLR Visitor Implementation for UseCase Diagrams
|
||||
*
|
||||
* This visitor traverses the ANTLR parse tree and builds the AST
|
||||
* according to the grammar rules defined in Usecase.g4
|
||||
*/
|
||||
|
||||
import { UsecaseVisitor } from './generated/UsecaseVisitor.js';
|
||||
import type {
|
||||
UsecaseDiagramContext,
|
||||
StatementContext,
|
||||
ActorStatementContext,
|
||||
ActorListContext,
|
||||
RelationshipStatementContext,
|
||||
SystemBoundaryStatementContext,
|
||||
SystemBoundaryTypeStatementContext,
|
||||
SystemBoundaryNameContext,
|
||||
SystemBoundaryTypeContentContext,
|
||||
SystemBoundaryTypePropertyContext,
|
||||
SystemBoundaryTypeContext,
|
||||
UsecaseInBoundaryContext,
|
||||
ActorNameContext,
|
||||
ActorDeclarationContext,
|
||||
NodeIdWithLabelContext,
|
||||
NodeLabelContext,
|
||||
MetadataContext,
|
||||
MetadataContentContext,
|
||||
MetadataPropertyContext,
|
||||
EntityNameContext,
|
||||
ArrowContext,
|
||||
LabeledArrowContext,
|
||||
EdgeLabelContext,
|
||||
} from './generated/UsecaseParser.js';
|
||||
import { ARROW_TYPE } from './types.js';
|
||||
import type {
|
||||
Actor,
|
||||
UseCase,
|
||||
SystemBoundary,
|
||||
Relationship,
|
||||
UsecaseParseResult,
|
||||
ArrowType,
|
||||
} from './types.js';
|
||||
|
||||
export class UsecaseAntlrVisitor extends UsecaseVisitor<void> {
|
||||
private actors: Actor[] = [];
|
||||
private useCases: UseCase[] = [];
|
||||
private systemBoundaries: SystemBoundary[] = [];
|
||||
private relationships: Relationship[] = [];
|
||||
private relationshipCounter = 0;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Assign visitor functions as properties
|
||||
this.visitUsecaseDiagram = this.visitUsecaseDiagramImpl.bind(this);
|
||||
this.visitStatement = this.visitStatementImpl.bind(this);
|
||||
this.visitActorStatement = this.visitActorStatementImpl.bind(this);
|
||||
this.visitRelationshipStatement = this.visitRelationshipStatementImpl.bind(this);
|
||||
this.visitSystemBoundaryStatement = this.visitSystemBoundaryStatementImpl.bind(this);
|
||||
this.visitSystemBoundaryTypeStatement = this.visitSystemBoundaryTypeStatementImpl.bind(this);
|
||||
this.visitActorName = this.visitActorNameImpl.bind(this);
|
||||
this.visitArrow = this.visitArrowImpl.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit the root usecaseDiagram rule
|
||||
* Grammar: usecaseDiagram : 'usecase' statement* EOF ;
|
||||
*/
|
||||
visitUsecaseDiagramImpl(ctx: UsecaseDiagramContext): void {
|
||||
// Reset state
|
||||
this.actors = [];
|
||||
this.useCases = [];
|
||||
this.relationships = [];
|
||||
this.relationshipCounter = 0;
|
||||
|
||||
// Visit all statement children
|
||||
if (ctx.statement) {
|
||||
const statements = Array.isArray(ctx.statement()) ? ctx.statement() : [ctx.statement()];
|
||||
for (const statementCtx of statements) {
|
||||
if (Array.isArray(statementCtx)) {
|
||||
for (const stmt of statementCtx) {
|
||||
this.visitStatementImpl(stmt);
|
||||
}
|
||||
} else {
|
||||
this.visitStatementImpl(statementCtx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit statement rule
|
||||
* Grammar: statement : actorStatement | relationshipStatement | systemBoundaryStatement | systemBoundaryTypeStatement | NEWLINE ;
|
||||
*/
|
||||
private visitStatementImpl(ctx: StatementContext): void {
|
||||
if (ctx.actorStatement?.()) {
|
||||
this.visitActorStatementImpl(ctx.actorStatement()!);
|
||||
} else if (ctx.relationshipStatement?.()) {
|
||||
this.visitRelationshipStatementImpl(ctx.relationshipStatement()!);
|
||||
} else if (ctx.systemBoundaryStatement?.()) {
|
||||
this.visitSystemBoundaryStatementImpl(ctx.systemBoundaryStatement()!);
|
||||
} else if (ctx.systemBoundaryTypeStatement?.()) {
|
||||
this.visitSystemBoundaryTypeStatementImpl(ctx.systemBoundaryTypeStatement()!);
|
||||
}
|
||||
// NEWLINE is ignored
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit actorStatement rule
|
||||
* Grammar: actorStatement : 'actor' actorList NEWLINE* ;
|
||||
*/
|
||||
visitActorStatementImpl(ctx: ActorStatementContext): void {
|
||||
if (ctx.actorList?.()) {
|
||||
this.visitActorListImpl(ctx.actorList());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit actorList rule
|
||||
* Grammar: actorList : actorName (',' actorName)* ;
|
||||
*/
|
||||
visitActorListImpl(ctx: ActorListContext): void {
|
||||
// Get all actorName contexts from the list
|
||||
const actorNameContexts = ctx.actorName();
|
||||
|
||||
for (const actorNameCtx of actorNameContexts) {
|
||||
const actorResult = this.visitActorNameImpl(actorNameCtx);
|
||||
this.actors.push({
|
||||
id: actorResult.name,
|
||||
name: actorResult.name,
|
||||
metadata: actorResult.metadata,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit relationshipStatement rule
|
||||
* Grammar: relationshipStatement : entityName arrow entityName NEWLINE* | actorDeclaration arrow entityName NEWLINE* ;
|
||||
*/
|
||||
visitRelationshipStatementImpl(ctx: RelationshipStatementContext): void {
|
||||
let from = '';
|
||||
let to = '';
|
||||
|
||||
// Handle different relationship patterns
|
||||
if (ctx.actorDeclaration?.()) {
|
||||
// Pattern: actor ActorName --> entityName
|
||||
from = this.visitActorDeclarationImpl(ctx.actorDeclaration()!);
|
||||
to = this.visitEntityNameImpl(ctx.entityName(0)!);
|
||||
} else if (ctx.entityName && ctx.entityName().length >= 2) {
|
||||
// Pattern: entityName --> entityName
|
||||
from = this.visitEntityNameImpl(ctx.entityName(0)!);
|
||||
to = this.visitEntityNameImpl(ctx.entityName(1)!);
|
||||
}
|
||||
|
||||
// Get arrow information (type and optional label)
|
||||
const arrowInfo = this.visitArrowImpl(ctx.arrow());
|
||||
|
||||
// Auto-create use cases for entities that are not actors
|
||||
this.ensureUseCaseExists(from);
|
||||
this.ensureUseCaseExists(to);
|
||||
|
||||
const relationship: Relationship = {
|
||||
id: `rel_${this.relationshipCounter++}`,
|
||||
from,
|
||||
to,
|
||||
type: 'association',
|
||||
arrowType: arrowInfo.arrowType,
|
||||
};
|
||||
|
||||
// Add label if present
|
||||
if (arrowInfo.label) {
|
||||
relationship.label = arrowInfo.label;
|
||||
}
|
||||
|
||||
this.relationships.push(relationship);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a use case exists for the given entity name if it's not an actor
|
||||
*/
|
||||
private ensureUseCaseExists(entityName: string): void {
|
||||
// Check if it's already an actor
|
||||
const isActor = this.actors.some((actor) => actor.id === entityName);
|
||||
|
||||
// If it's not an actor, create it as a use case (if not already exists)
|
||||
if (!isActor) {
|
||||
const existingUseCase = this.useCases.some((useCase) => useCase.id === entityName);
|
||||
if (!existingUseCase) {
|
||||
this.useCases.push({
|
||||
id: entityName,
|
||||
name: entityName,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit systemBoundaryStatement rule
|
||||
* Grammar: systemBoundaryStatement : 'systemBoundary' systemBoundaryName NEWLINE* systemBoundaryContent* 'end' NEWLINE* ;
|
||||
*/
|
||||
visitSystemBoundaryStatementImpl(ctx: SystemBoundaryStatementContext): void {
|
||||
let boundaryName = '';
|
||||
|
||||
// Get the system boundary name
|
||||
if (ctx.systemBoundaryName?.()) {
|
||||
boundaryName = this.visitSystemBoundaryNameImpl(ctx.systemBoundaryName());
|
||||
}
|
||||
|
||||
// Collect use cases within this boundary
|
||||
const useCasesInBoundary: string[] = [];
|
||||
|
||||
if (ctx.systemBoundaryContent?.()?.length > 0) {
|
||||
for (const contentCtx of ctx.systemBoundaryContent()) {
|
||||
const usecaseInBoundary = contentCtx.usecaseInBoundary?.();
|
||||
if (usecaseInBoundary) {
|
||||
const useCaseName = this.visitUsecaseInBoundaryImpl(usecaseInBoundary);
|
||||
useCasesInBoundary.push(useCaseName);
|
||||
|
||||
// Create the use case and mark it as being in this boundary
|
||||
const existingUseCase = this.useCases.find((uc) => uc.id === useCaseName);
|
||||
if (existingUseCase) {
|
||||
existingUseCase.systemBoundary = boundaryName;
|
||||
} else {
|
||||
this.useCases.push({
|
||||
id: useCaseName,
|
||||
name: useCaseName,
|
||||
systemBoundary: boundaryName,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create the system boundary with default type
|
||||
this.systemBoundaries.push({
|
||||
id: boundaryName,
|
||||
name: boundaryName,
|
||||
useCases: useCasesInBoundary,
|
||||
type: 'rect', // default type
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit systemBoundaryName rule
|
||||
* Grammar: systemBoundaryName : IDENTIFIER | STRING ;
|
||||
*/
|
||||
private visitSystemBoundaryNameImpl(ctx: SystemBoundaryNameContext): string {
|
||||
const identifier = ctx.IDENTIFIER?.();
|
||||
if (identifier) {
|
||||
return identifier.getText();
|
||||
}
|
||||
|
||||
const string = ctx.STRING?.();
|
||||
if (string) {
|
||||
const text = string.getText();
|
||||
// Remove quotes from string
|
||||
return text.slice(1, -1);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit usecaseInBoundary rule
|
||||
* Grammar: usecaseInBoundary : IDENTIFIER | STRING ;
|
||||
*/
|
||||
private visitUsecaseInBoundaryImpl(ctx: UsecaseInBoundaryContext): string {
|
||||
const identifier = ctx.IDENTIFIER?.();
|
||||
if (identifier) {
|
||||
return identifier.getText();
|
||||
}
|
||||
|
||||
const string = ctx.STRING?.();
|
||||
if (string) {
|
||||
const text = string.getText();
|
||||
// Remove quotes from string
|
||||
return text.slice(1, -1);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit systemBoundaryTypeStatement rule
|
||||
* Grammar: systemBoundaryTypeStatement : systemBoundaryName '\@' '\{' systemBoundaryTypeContent '\}' NEWLINE* ;
|
||||
*/
|
||||
visitSystemBoundaryTypeStatementImpl(ctx: SystemBoundaryTypeStatementContext): void {
|
||||
let boundaryName = '';
|
||||
|
||||
// Get the system boundary name
|
||||
const systemBoundaryName = ctx.systemBoundaryName?.();
|
||||
if (systemBoundaryName) {
|
||||
boundaryName = this.visitSystemBoundaryNameImpl(systemBoundaryName);
|
||||
}
|
||||
|
||||
// Get the type configuration
|
||||
let boundaryType: 'package' | 'rect' = 'rect'; // default
|
||||
const systemBoundaryTypeContent = ctx.systemBoundaryTypeContent?.();
|
||||
if (systemBoundaryTypeContent) {
|
||||
boundaryType = this.visitSystemBoundaryTypeContentImpl(systemBoundaryTypeContent);
|
||||
}
|
||||
|
||||
// Find the existing system boundary and update its type
|
||||
const existingBoundary = this.systemBoundaries.find((b) => b.id === boundaryName);
|
||||
if (existingBoundary) {
|
||||
existingBoundary.type = boundaryType;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit systemBoundaryTypeContent rule
|
||||
* Grammar: systemBoundaryTypeContent : systemBoundaryTypeProperty (',' systemBoundaryTypeProperty)* ;
|
||||
*/
|
||||
private visitSystemBoundaryTypeContentImpl(
|
||||
ctx: SystemBoundaryTypeContentContext
|
||||
): 'package' | 'rect' {
|
||||
// Get all type properties
|
||||
const typeProperties = ctx.systemBoundaryTypeProperty();
|
||||
|
||||
for (const propCtx of typeProperties) {
|
||||
const type = this.visitSystemBoundaryTypePropertyImpl(propCtx);
|
||||
if (type) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
return 'rect'; // default
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit systemBoundaryTypeProperty rule
|
||||
* Grammar: systemBoundaryTypeProperty : 'type' ':' systemBoundaryType ;
|
||||
*/
|
||||
private visitSystemBoundaryTypePropertyImpl(
|
||||
ctx: SystemBoundaryTypePropertyContext
|
||||
): 'package' | 'rect' | null {
|
||||
const systemBoundaryType = ctx.systemBoundaryType?.();
|
||||
if (systemBoundaryType) {
|
||||
return this.visitSystemBoundaryTypeImpl(systemBoundaryType);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit systemBoundaryType rule
|
||||
* Grammar: systemBoundaryType : 'package' | 'rect' ;
|
||||
*/
|
||||
private visitSystemBoundaryTypeImpl(ctx: SystemBoundaryTypeContext): 'package' | 'rect' {
|
||||
const text = ctx.getText();
|
||||
if (text === 'package') {
|
||||
return 'package';
|
||||
} else if (text === 'rect') {
|
||||
return 'rect';
|
||||
}
|
||||
return 'rect'; // default
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit actorName rule
|
||||
* Grammar: actorName : (IDENTIFIER | STRING) metadata? ;
|
||||
*/
|
||||
private visitActorNameImpl(ctx: ActorNameContext): {
|
||||
name: string;
|
||||
metadata?: Record<string, string>;
|
||||
} {
|
||||
let name = '';
|
||||
|
||||
if (ctx.IDENTIFIER?.()) {
|
||||
name = ctx.IDENTIFIER()!.getText();
|
||||
} else if (ctx.STRING?.()) {
|
||||
const text = ctx.STRING()!.getText();
|
||||
// Remove quotes from string
|
||||
name = text.slice(1, -1);
|
||||
}
|
||||
|
||||
let metadata = undefined;
|
||||
if (ctx.metadata?.()) {
|
||||
metadata = this.visitMetadataImpl(ctx.metadata()!);
|
||||
}
|
||||
|
||||
return { name, metadata };
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit metadata rule
|
||||
* Grammar: metadata : '\@' '\{' metadataContent '\}' ;
|
||||
*/
|
||||
private visitMetadataImpl(ctx: MetadataContext): Record<string, string> {
|
||||
const metadataContent = ctx.metadataContent?.();
|
||||
if (metadataContent) {
|
||||
return this.visitMetadataContentImpl(metadataContent);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit metadataContent rule
|
||||
* Grammar: metadataContent : metadataProperty (',' metadataProperty)* ;
|
||||
*/
|
||||
private visitMetadataContentImpl(ctx: MetadataContentContext): Record<string, string> {
|
||||
const metadata: Record<string, string> = {};
|
||||
const properties = ctx.metadataProperty();
|
||||
|
||||
for (const property of properties) {
|
||||
const { key, value } = this.visitMetadataPropertyImpl(property);
|
||||
metadata[key] = value;
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit metadataProperty rule
|
||||
* Grammar: metadataProperty : STRING ':' STRING ;
|
||||
*/
|
||||
private visitMetadataPropertyImpl(ctx: MetadataPropertyContext): { key: string; value: string } {
|
||||
const strings = ctx.STRING();
|
||||
if (strings.length >= 2) {
|
||||
const key = strings[0].getText().slice(1, -1); // Remove quotes
|
||||
const value = strings[1].getText().slice(1, -1); // Remove quotes
|
||||
return { key, value };
|
||||
}
|
||||
return { key: '', value: '' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit entityName rule
|
||||
* Grammar: entityName : IDENTIFIER | STRING | nodeIdWithLabel ;
|
||||
*/
|
||||
private visitEntityNameImpl(ctx: EntityNameContext): string {
|
||||
const identifier = ctx.IDENTIFIER?.();
|
||||
if (identifier) {
|
||||
return identifier.getText();
|
||||
}
|
||||
|
||||
const string = ctx.STRING?.();
|
||||
if (string) {
|
||||
const text = string.getText();
|
||||
// Remove quotes from string
|
||||
return text.slice(1, -1);
|
||||
}
|
||||
|
||||
const nodeIdWithLabel = ctx.nodeIdWithLabel?.();
|
||||
if (nodeIdWithLabel) {
|
||||
return this.visitNodeIdWithLabelImpl(nodeIdWithLabel);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit actorDeclaration rule
|
||||
* Grammar: actorDeclaration : 'actor' actorName ;
|
||||
*/
|
||||
private visitActorDeclarationImpl(ctx: ActorDeclarationContext): string {
|
||||
const actorName = ctx.actorName?.();
|
||||
if (actorName) {
|
||||
const actorResult = this.visitActorNameImpl(actorName);
|
||||
|
||||
// Add the actor if it doesn't already exist
|
||||
const existingActor = this.actors.find((actor) => actor.id === actorResult.name);
|
||||
if (!existingActor) {
|
||||
this.actors.push({
|
||||
id: actorResult.name,
|
||||
name: actorResult.name,
|
||||
metadata: actorResult.metadata,
|
||||
});
|
||||
}
|
||||
|
||||
return actorResult.name;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit nodeIdWithLabel rule
|
||||
* Grammar: nodeIdWithLabel : IDENTIFIER '(' nodeLabel ')' ;
|
||||
*/
|
||||
private visitNodeIdWithLabelImpl(ctx: NodeIdWithLabelContext): string {
|
||||
let nodeId = '';
|
||||
let nodeLabel = '';
|
||||
|
||||
const identifier = ctx.IDENTIFIER?.();
|
||||
if (identifier) {
|
||||
nodeId = identifier.getText();
|
||||
}
|
||||
|
||||
const nodeLabelCtx = ctx.nodeLabel?.();
|
||||
if (nodeLabelCtx) {
|
||||
nodeLabel = this.visitNodeLabelImpl(nodeLabelCtx);
|
||||
}
|
||||
|
||||
// Create or update the use case with nodeId and label
|
||||
const existingUseCase = this.useCases.find((uc) => uc.id === nodeLabel || uc.nodeId === nodeId);
|
||||
if (existingUseCase) {
|
||||
// Update existing use case with nodeId if not already set
|
||||
existingUseCase.nodeId ??= nodeId;
|
||||
} else {
|
||||
// Create new use case with nodeId and label
|
||||
this.useCases.push({
|
||||
id: nodeLabel,
|
||||
name: nodeLabel,
|
||||
nodeId: nodeId,
|
||||
});
|
||||
}
|
||||
|
||||
return nodeLabel; // Return the label as the entity name for relationships
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit nodeLabel rule
|
||||
* Grammar: nodeLabel : IDENTIFIER | STRING | nodeLabel IDENTIFIER | nodeLabel STRING ;
|
||||
*/
|
||||
private visitNodeLabelImpl(ctx: NodeLabelContext): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
// Handle recursive nodeLabel structure
|
||||
const nodeLabel = ctx.nodeLabel?.();
|
||||
if (nodeLabel) {
|
||||
parts.push(this.visitNodeLabelImpl(nodeLabel));
|
||||
}
|
||||
|
||||
const identifier = ctx.IDENTIFIER?.();
|
||||
if (identifier) {
|
||||
parts.push(identifier.getText());
|
||||
} else {
|
||||
const string = ctx.STRING?.();
|
||||
if (string) {
|
||||
const text = string.getText();
|
||||
// Remove quotes from string
|
||||
parts.push(text.slice(1, -1));
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit arrow rule
|
||||
* Grammar: arrow : SOLID_ARROW | BACK_ARROW | LINE_SOLID | labeledArrow ;
|
||||
*/
|
||||
private visitArrowImpl(ctx: ArrowContext): { arrowType: ArrowType; label?: string } {
|
||||
// Check if this is a labeled arrow
|
||||
if (ctx.labeledArrow()) {
|
||||
return this.visitLabeledArrowImpl(ctx.labeledArrow()!);
|
||||
}
|
||||
|
||||
// Regular arrow without label - determine type from token
|
||||
if (ctx.SOLID_ARROW()) {
|
||||
return { arrowType: ARROW_TYPE.SOLID_ARROW };
|
||||
} else if (ctx.BACK_ARROW()) {
|
||||
return { arrowType: ARROW_TYPE.BACK_ARROW };
|
||||
} else if (ctx.LINE_SOLID()) {
|
||||
return { arrowType: ARROW_TYPE.LINE_SOLID };
|
||||
}
|
||||
|
||||
// Fallback (should not happen with proper grammar)
|
||||
return { arrowType: ARROW_TYPE.SOLID_ARROW };
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit labeled arrow rule
|
||||
* Grammar: labeledArrow : LINE_SOLID edgeLabel SOLID_ARROW | BACK_ARROW edgeLabel LINE_SOLID | LINE_SOLID edgeLabel LINE_SOLID ;
|
||||
*/
|
||||
private visitLabeledArrowImpl(ctx: LabeledArrowContext): { arrowType: ArrowType; label: string } {
|
||||
const label = this.visitEdgeLabelImpl(ctx.edgeLabel());
|
||||
|
||||
// Determine arrow type based on the tokens present
|
||||
if (ctx.SOLID_ARROW()) {
|
||||
return { arrowType: ARROW_TYPE.SOLID_ARROW, label };
|
||||
} else if (ctx.BACK_ARROW()) {
|
||||
return { arrowType: ARROW_TYPE.BACK_ARROW, label };
|
||||
} else {
|
||||
return { arrowType: ARROW_TYPE.LINE_SOLID, label };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit edge label rule
|
||||
* Grammar: edgeLabel : IDENTIFIER | STRING ;
|
||||
*/
|
||||
private visitEdgeLabelImpl(ctx: EdgeLabelContext): string {
|
||||
const text = ctx.getText();
|
||||
// Remove quotes if it's a string
|
||||
if (
|
||||
(text.startsWith('"') && text.endsWith('"')) ||
|
||||
(text.startsWith("'") && text.endsWith("'"))
|
||||
) {
|
||||
return text.slice(1, -1);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parse result after visiting the diagram
|
||||
*/
|
||||
getParseResult(): UsecaseParseResult {
|
||||
return {
|
||||
actors: this.actors,
|
||||
useCases: this.useCases,
|
||||
systemBoundaries: this.systemBoundaries,
|
||||
relationships: this.relationships,
|
||||
};
|
||||
}
|
||||
}
|
@@ -1,8 +1,10 @@
|
||||
import type { LangiumParser, ParseResult } from 'langium';
|
||||
|
||||
import type { Info, Packet, Pie, Architecture, GitGraph, Radar, Treemap } from './index.js';
|
||||
import type { UsecaseParseResult } from './language/usecase/types.js';
|
||||
|
||||
export type DiagramAST = Info | Packet | Pie | Architecture | GitGraph | Radar;
|
||||
export type DiagramAST = Info | Packet | Pie | Architecture | GitGraph | Radar | UsecaseParseResult;
|
||||
export type LangiumDiagramAST = Info | Packet | Pie | Architecture | GitGraph | Radar;
|
||||
|
||||
const parsers: Record<string, LangiumParser> = {};
|
||||
const initializers = {
|
||||
@@ -41,6 +43,9 @@ const initializers = {
|
||||
const parser = createTreemapServices().Treemap.parser.LangiumParser;
|
||||
parsers.treemap = parser;
|
||||
},
|
||||
usecase: () => {
|
||||
// ANTLR-based parser - no Langium parser needed
|
||||
},
|
||||
} as const;
|
||||
|
||||
export async function parse(diagramType: 'info', text: string): Promise<Info>;
|
||||
@@ -50,7 +55,12 @@ export async function parse(diagramType: 'architecture', text: string): Promise<
|
||||
export async function parse(diagramType: 'gitGraph', text: string): Promise<GitGraph>;
|
||||
export async function parse(diagramType: 'radar', text: string): Promise<Radar>;
|
||||
export async function parse(diagramType: 'treemap', text: string): Promise<Treemap>;
|
||||
export async function parse(diagramType: 'usecase', text: string): Promise<UsecaseParseResult>;
|
||||
|
||||
export async function parse<T extends LangiumDiagramAST>(
|
||||
diagramType: Exclude<keyof typeof initializers, 'usecase'>,
|
||||
text: string
|
||||
): Promise<T>;
|
||||
export async function parse<T extends DiagramAST>(
|
||||
diagramType: keyof typeof initializers,
|
||||
text: string
|
||||
@@ -59,11 +69,19 @@ export async function parse<T extends DiagramAST>(
|
||||
if (!initializer) {
|
||||
throw new Error(`Unknown diagram type: ${diagramType}`);
|
||||
}
|
||||
|
||||
// Handle ANTLR-based parsers separately
|
||||
if (diagramType === 'usecase') {
|
||||
const { parseUsecaseWithAntlr } = await import('./language/usecase/index.js');
|
||||
return parseUsecaseWithAntlr(text) as T;
|
||||
}
|
||||
|
||||
if (!parsers[diagramType]) {
|
||||
await initializer();
|
||||
}
|
||||
const parser: LangiumParser = parsers[diagramType];
|
||||
const result: ParseResult<T> = parser.parse<T>(text);
|
||||
const result: ParseResult<T extends LangiumDiagramAST ? T : never> =
|
||||
parser.parse<T extends LangiumDiagramAST ? T : never>(text);
|
||||
if (result.lexerErrors.length > 0 || result.parserErrors.length > 0) {
|
||||
throw new MermaidParseError(result);
|
||||
}
|
||||
|
1612
packages/parser/tests/usecase.test.ts
Normal file
1612
packages/parser/tests/usecase.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user