Compare commits

..

1 Commits

Author SHA1 Message Date
omkarht
cfed700a58 chore: modified the file structure to implement the new way of antlr parsing
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-11-14 18:24:37 +05:30
24 changed files with 2466 additions and 3744 deletions

View File

@@ -4,6 +4,7 @@ import cors from 'cors';
import { context } from 'esbuild';
import type { Request, Response } from 'express';
import express from 'express';
import { execSync } from 'child_process';
import { packageOptions } from '../.build/common.js';
import { generateLangium } from '../.build/generateLangium.js';
import { defaultOptions, getBuildConfig } from './util.js';
@@ -64,6 +65,28 @@ function eventsHandler(request: Request, response: Response) {
}
let timeoutID: NodeJS.Timeout | undefined = undefined;
let isGeneratingAntlr = false;
/**
* Generate ANTLR parser files from grammar files
*/
function generateAntlr(): void {
if (isGeneratingAntlr) {
console.log('⏳ ANTLR generation already in progress, skipping...');
return;
}
try {
isGeneratingAntlr = true;
console.log('🎯 ANTLR: Generating parser files...');
execSync('tsx scripts/antlr-generate.mts', { stdio: 'inherit' });
console.log('✅ ANTLR: Parser files generated successfully\n');
} catch (error) {
console.error('❌ ANTLR: Failed to generate parser files:', error);
} finally {
isGeneratingAntlr = false;
}
}
/**
* Debounce file change events to avoid rebuilding multiple times.
@@ -89,7 +112,7 @@ async function createServer() {
handleFileChange();
const app = express();
chokidar
.watch('**/src/**/*.{js,ts,langium,yaml,json}', {
.watch('**/src/**/*.{js,ts,g4,langium,yaml,json}', {
ignoreInitial: true,
ignored: [/node_modules/, /dist/, /docs/, /coverage/],
})
@@ -103,6 +126,9 @@ async function createServer() {
if (path.endsWith('.langium')) {
await generateLangium();
}
if (path.endsWith('.g4')) {
generateAntlr();
}
handleFileChange();
});

View File

@@ -15,8 +15,10 @@
"git graph"
],
"scripts": {
"build": "pnpm build:esbuild && pnpm build:types",
"build": "pnpm antlr:generate && pnpm build:esbuild && pnpm build:types",
"build:esbuild": "pnpm run -r clean && tsx .esbuild/build.ts",
"antlr:generate": "tsx scripts/antlr-generate.mts",
"antlr:watch": "tsx scripts/antlr-watch.mts",
"build:mermaid": "pnpm build:esbuild --mermaid",
"build:viz": "pnpm build:esbuild --visualize",
"build:types": "pnpm --filter mermaid types:build-config && tsx .build/types.ts",

View File

@@ -34,6 +34,8 @@
"scripts": {
"clean": "rimraf dist",
"dev": "pnpm -w dev",
"antlr:generate": "tsx ../../scripts/antlr-generate.mts",
"antlr:watch": "tsx ../../scripts/antlr-watch.mts",
"docs:code": "typedoc src/defaultConfig.ts src/config.ts src/mermaid.ts && prettier --write ./src/docs/config/setup",
"docs:build": "rimraf ../../docs && pnpm docs:code && pnpm docs:spellcheck && tsx scripts/docs.cli.mts",
"docs:verify": "pnpm docs:code && pnpm docs:spellcheck && tsx scripts/docs.cli.mts --verify",
@@ -71,6 +73,7 @@
"@iconify/utils": "^3.0.2",
"@mermaid-js/parser": "workspace:^",
"@types/d3": "^7.4.3",
"antlr4ng": "^3.0.7",
"cytoscape": "^3.33.1",
"cytoscape-cose-bilkent": "^4.1.0",
"cytoscape-fcose": "^2.2.0",

View File

@@ -0,0 +1,90 @@
import { BaseErrorListener } from 'antlr4ng';
import type { RecognitionException, Recognizer } from 'antlr4ng';
/**
* Custom error listener for ANTLR usecase parser
* Captures syntax errors and provides detailed error messages
*/
export class UsecaseErrorListener extends BaseErrorListener {
private errors: { line: number; column: number; message: string; offendingSymbol?: any }[] = [];
syntaxError(
_recognizer: Recognizer<any>,
offendingSymbol: any,
line: number,
charPositionInLine: number,
message: string,
_e: RecognitionException | null
): void {
this.errors.push({
line,
column: charPositionInLine,
message,
offendingSymbol,
});
}
reportAmbiguity(): void {
// Optional: handle ambiguity reports
}
reportAttemptingFullContext(): void {
// Optional: handle full context attempts
}
reportContextSensitivity(): void {
// Optional: handle context sensitivity reports
}
getErrors(): { line: number; column: number; message: string; offendingSymbol?: any }[] {
return this.errors;
}
hasErrors(): boolean {
return this.errors.length > 0;
}
clear(): void {
this.errors = [];
}
/**
* Create a detailed error with JISON-compatible hash property
*/
createDetailedError(): Error {
if (this.errors.length === 0) {
return new Error('Unknown parsing error');
}
const firstError = this.errors[0];
const message = `Parse error on line ${firstError.line}: ${firstError.message}`;
const error = new Error(message);
// Add hash property for JISON compatibility
Object.assign(error, {
hash: {
line: firstError.line,
loc: {
first_line: firstError.line,
last_line: firstError.line,
first_column: firstError.column,
last_column: firstError.column,
},
text: firstError.offendingSymbol?.text ?? '',
token: firstError.offendingSymbol?.text ?? '',
expected: [],
},
});
return error;
}
/**
* Get all error messages as a single string
*/
getErrorMessages(): string {
return this.errors
.map((error) => `Line ${error.line}:${error.column} - ${error.message}`)
.join('\n');
}
}

View File

@@ -0,0 +1,65 @@
lexer grammar UsecaseLexer;
// Keywords
ACTOR: 'actor';
SYSTEM_BOUNDARY: 'systemBoundary';
END: 'end';
DIRECTION: 'direction';
CLASS_DEF: 'classDef';
CLASS: 'class';
STYLE: 'style';
USECASE: 'usecase';
// Direction keywords
TB: 'TB';
TD: 'TD';
BT: 'BT';
RL: 'RL';
LR: 'LR';
// System boundary types
PACKAGE: 'package';
RECT: 'rect';
TYPE: 'type';
// Arrow types (order matters - longer patterns first)
SOLID_ARROW: '-->';
BACK_ARROW: '<--';
CIRCLE_ARROW: '--o';
CIRCLE_ARROW_REVERSED: 'o--';
CROSS_ARROW: '--x';
CROSS_ARROW_REVERSED: 'x--';
LINE_SOLID: '--';
// Symbols
COMMA: ',';
AT: '@';
LBRACE: '{';
RBRACE: '}';
COLON: ':';
LPAREN: '(';
RPAREN: ')';
CLASS_SEPARATOR: ':::';
// Hash color (must come before HASH to avoid conflicts)
HASH_COLOR: '#' [a-fA-F0-9]+;
// Number with optional unit
NUMBER: [0-9]+ ('.' [0-9]+)? ([a-zA-Z]+)?;
// Identifier
IDENTIFIER: [a-zA-Z_][a-zA-Z0-9_]*;
// String literals
STRING: '"' (~["\r\n])* '"' | '\'' (~['\r\n])* '\'';
// These tokens are defined last so they have lowest priority
// This ensures arrow tokens like '-->' are matched before DASH
DASH: '-';
DOT: '.';
PERCENT: '%';
// Whitespace and newlines
NEWLINE: [\r\n]+;
WS: [ \t]+ -> skip;

View File

@@ -0,0 +1,429 @@
import type { ParseTreeListener } from 'antlr4ng';
import { UsecaseParserCore } from './UsecaseParserCore.js';
import { log } from '../../../../logger.js';
import type { UsecaseDB } from '../../usecaseTypes.js';
/**
* Listener implementation that builds the usecase model
* Extends the core logic to ensure consistency with Visitor pattern
*/
export class UsecaseListener extends UsecaseParserCore implements ParseTreeListener {
constructor(db: UsecaseDB) {
super(db);
log.debug('👂 UsecaseListener: Constructor called');
}
// Standard ParseTreeListener methods
enterEveryRule = (ctx: any) => {
if (this.getEnvVar('ANTLR_DEBUG') === 'true') {
const ruleName = ctx.constructor.name;
log.debug('🔍 UsecaseListener: Entering rule:', ruleName);
}
};
exitEveryRule = (ctx: any) => {
if (this.getEnvVar('ANTLR_DEBUG') === 'true') {
const ruleName = ctx.constructor.name;
log.debug('🔍 UsecaseListener: Exiting rule:', ruleName);
}
};
visitTerminal = (_node: any) => {
// Optional: Handle terminal nodes
};
visitErrorNode = (_node: any) => {
log.debug('❌ UsecaseListener: Error node encountered');
};
// Actor statement
exitActorName = (ctx: any) => {
let actorName = '';
if (ctx.IDENTIFIER()) {
actorName = ctx.IDENTIFIER().getText();
} else if (ctx.STRING()) {
actorName = this.extractString(ctx.STRING().getText());
}
const actorId = this.generateId(actorName);
// Process metadata if present
let metadata: Record<string, string> | undefined;
if (ctx.metadata()) {
metadata = this.extractMetadata(ctx.metadata());
}
this.processActorStatement(actorId, actorName, metadata);
};
// Relationship statement
exitRelationshipStatement = (ctx: any) => {
let from = '';
let to = '';
let arrowType = 0;
let label: string | undefined;
// Get entity names
const entityNames = ctx.entityName();
if (entityNames && entityNames.length >= 2) {
from = this.extractEntityName(entityNames[0]);
to = this.extractEntityName(entityNames[1]);
} else if (ctx.actorDeclaration()) {
from = this.extractActorDeclaration(ctx.actorDeclaration());
if (entityNames && entityNames.length >= 1) {
to = this.extractEntityName(entityNames[0]);
}
}
// Get arrow type
const arrow = ctx.arrow();
if (arrow) {
const arrowResult = this.extractArrow(arrow);
arrowType = arrowResult.type;
label = arrowResult.label;
}
this.processRelationship(from, to, arrowType, label);
};
// System boundary statement
enterSystemBoundaryStatement = (ctx: any) => {
const boundaryName = ctx.systemBoundaryName();
let boundaryId = '';
let boundaryNameText = '';
if (boundaryName) {
if (boundaryName.IDENTIFIER()) {
boundaryNameText = boundaryName.IDENTIFIER().getText();
} else if (boundaryName.STRING()) {
boundaryNameText = this.extractString(boundaryName.STRING().getText());
}
boundaryId = this.generateId(boundaryNameText);
}
this.processSystemBoundaryStart(boundaryId, boundaryNameText);
};
exitSystemBoundaryStatement = (_ctx: any) => {
this.processSystemBoundaryEnd();
};
exitUsecaseInBoundary = (ctx: any) => {
let useCaseId = '';
let useCaseName = '';
let classes: string[] | undefined;
if (ctx.usecaseWithClass()) {
const withClass = ctx.usecaseWithClass();
if (withClass.IDENTIFIER()) {
const identifiers = withClass.IDENTIFIER();
if (Array.isArray(identifiers) && identifiers.length >= 2) {
useCaseId = identifiers[0].getText();
useCaseName = useCaseId;
classes = [identifiers[1].getText()];
}
} else if (withClass.STRING()) {
useCaseName = this.extractString(withClass.STRING().getText());
useCaseId = this.generateId(useCaseName);
const identifiers = withClass.IDENTIFIER();
if (identifiers) {
classes = [identifiers.getText()];
}
}
} else if (ctx.IDENTIFIER()) {
useCaseId = ctx.IDENTIFIER().getText();
useCaseName = useCaseId;
} else if (ctx.STRING()) {
useCaseName = this.extractString(ctx.STRING().getText());
useCaseId = this.generateId(useCaseName);
}
if (useCaseId && useCaseName) {
this.processUseCaseStatement(useCaseId, useCaseName, undefined, classes);
}
};
// System boundary type statement
exitSystemBoundaryTypeStatement = (ctx: any) => {
const boundaryName = ctx.systemBoundaryName();
let boundaryId = '';
if (boundaryName) {
if (boundaryName.IDENTIFIER()) {
boundaryId = boundaryName.IDENTIFIER().getText();
} else if (boundaryName.STRING()) {
boundaryId = this.generateId(this.extractString(boundaryName.STRING().getText()));
}
}
const typeContent = ctx.systemBoundaryTypeContent();
if (typeContent) {
const properties = typeContent.systemBoundaryTypeProperty();
const props = Array.isArray(properties) ? properties : [properties];
for (const prop of props) {
const type = prop.systemBoundaryType();
if (type) {
let typeValue: 'package' | 'rect' = 'rect';
if (type.PACKAGE()) {
typeValue = 'package';
} else if (type.RECT()) {
typeValue = 'rect';
}
this.processSystemBoundaryType(boundaryId, typeValue);
}
}
}
};
// Direction statement
exitDirectionStatement = (ctx: any) => {
const direction = ctx.direction();
if (direction) {
let directionText = '';
if (direction.TB()) {
directionText = 'TB';
} else if (direction.TD()) {
directionText = 'TD';
} else if (direction.BT()) {
directionText = 'BT';
} else if (direction.RL()) {
directionText = 'RL';
} else if (direction.LR()) {
directionText = 'LR';
}
this.processDirectionStatement(directionText);
}
};
// Class definition statement
exitClassDefStatement = (ctx: any) => {
let classId = '';
if (ctx.IDENTIFIER()) {
classId = ctx.IDENTIFIER().getText();
}
const styles: string[] = [];
const stylesOpt = ctx.stylesOpt();
if (stylesOpt) {
this.collectStyles(stylesOpt, styles);
}
this.processClassDefStatement(classId, styles);
};
// Class statement
exitClassStatement = (ctx: any) => {
const nodeList = ctx.nodeList();
const nodeIds: string[] = [];
if (nodeList) {
const identifiers = nodeList.IDENTIFIER();
const ids = Array.isArray(identifiers) ? identifiers : [identifiers];
for (const id of ids) {
nodeIds.push(id.getText());
}
}
let classId = '';
const identifiers = ctx.IDENTIFIER();
if (identifiers) {
const ids = Array.isArray(identifiers) ? identifiers : [identifiers];
if (ids.length > 0) {
classId = ids[ids.length - 1].getText();
}
}
this.processClassStatement(nodeIds, classId);
};
// Style statement
exitStyleStatement = (ctx: any) => {
let nodeId = '';
if (ctx.IDENTIFIER()) {
nodeId = ctx.IDENTIFIER().getText();
}
const styles: string[] = [];
const stylesOpt = ctx.stylesOpt();
if (stylesOpt) {
this.collectStyles(stylesOpt, styles);
}
this.processStyleStatement(nodeId, styles);
};
// Usecase statement
exitUsecaseStatement = (ctx: any) => {
const entityName = ctx.entityName();
if (entityName) {
const useCaseId = this.extractEntityName(entityName);
this.processUseCaseStatement(useCaseId, useCaseId);
}
};
// Helper methods
private extractMetadata(ctx: any): Record<string, string> {
const metadata: Record<string, string> = {};
const content = ctx.metadataContent();
if (content) {
const properties = content.metadataProperty();
const props = Array.isArray(properties) ? properties : [properties];
for (const prop of props) {
const strings = prop.STRING();
if (strings && strings.length >= 2) {
const key = this.extractString(strings[0].getText());
const value = this.extractString(strings[1].getText());
metadata[key] = value;
}
}
}
return metadata;
}
private extractEntityName(ctx: any): string {
if (ctx.nodeIdWithLabel()) {
const nodeId = ctx.nodeIdWithLabel();
if (nodeId.IDENTIFIER()) {
return nodeId.IDENTIFIER().getText();
}
}
if (ctx.IDENTIFIER()) {
const identifiers = ctx.IDENTIFIER();
if (Array.isArray(identifiers) && identifiers.length >= 2) {
return identifiers[0].getText();
}
return identifiers.getText ? identifiers.getText() : identifiers[0].getText();
}
if (ctx.STRING()) {
const strings = ctx.STRING();
const text = strings.getText ? strings.getText() : strings[0].getText();
return this.extractString(text);
}
return '';
}
private extractActorDeclaration(ctx: any): string {
const actorName = ctx.actorName();
if (actorName) {
if (actorName.IDENTIFIER()) {
return actorName.IDENTIFIER().getText();
} else if (actorName.STRING()) {
return this.extractString(actorName.STRING().getText());
}
}
return '';
}
private extractArrow(ctx: any): { type: number; label?: string } {
let arrowText = '';
let label: string | undefined;
if (ctx.labeledArrow()) {
const labeledArrow = ctx.labeledArrow();
const edgeLabel = labeledArrow.edgeLabel();
if (edgeLabel) {
if (edgeLabel.IDENTIFIER()) {
label = edgeLabel.IDENTIFIER().getText();
} else if (edgeLabel.STRING()) {
label = this.extractString(edgeLabel.STRING().getText());
}
}
if (labeledArrow.SOLID_ARROW()) {
arrowText = '-->';
} else if (labeledArrow.BACK_ARROW()) {
arrowText = '<--';
} else if (labeledArrow.CIRCLE_ARROW()) {
arrowText = '--o';
} else if (labeledArrow.CROSS_ARROW()) {
arrowText = '--x';
} else if (labeledArrow.CIRCLE_ARROW_REVERSED()) {
arrowText = 'o--';
} else if (labeledArrow.CROSS_ARROW_REVERSED()) {
arrowText = 'x--';
} else {
arrowText = '--';
}
} else {
if (ctx.SOLID_ARROW()) {
arrowText = '-->';
} else if (ctx.BACK_ARROW()) {
arrowText = '<--';
} else if (ctx.LINE_SOLID()) {
arrowText = '--';
} else if (ctx.CIRCLE_ARROW()) {
arrowText = '--o';
} else if (ctx.CROSS_ARROW()) {
arrowText = '--x';
} else if (ctx.CIRCLE_ARROW_REVERSED()) {
arrowText = 'o--';
} else if (ctx.CROSS_ARROW_REVERSED()) {
arrowText = 'x--';
}
}
return {
type: this.parseArrowType(arrowText),
label,
};
}
private collectStyles(ctx: any, styles: string[]): void {
if (!ctx) {
return;
}
const styleComponents = this.getAllStyleComponents(ctx);
for (const component of styleComponents) {
styles.push(component.getText());
}
}
private getAllStyleComponents(ctx: any): any[] {
const components: any[] = [];
if (ctx.style) {
const styleCtx = ctx.style();
if (styleCtx) {
this.collectStyleComponents(styleCtx, components);
}
}
if (ctx.stylesOpt) {
const stylesOptList = Array.isArray(ctx.stylesOpt()) ? ctx.stylesOpt() : [ctx.stylesOpt()];
for (const opt of stylesOptList) {
if (opt) {
this.collectStyleComponents(opt, components);
}
}
}
return components;
}
private collectStyleComponents(ctx: any, components: any[]): void {
if (!ctx) {
return;
}
if (ctx.styleComponent) {
const comp = ctx.styleComponent();
if (comp) {
components.push(comp);
}
}
if (ctx.style) {
const styleCtx = ctx.style();
if (styleCtx) {
this.collectStyleComponents(styleCtx, components);
}
}
}
}

View File

@@ -1,10 +1,13 @@
grammar Usecase;
parser grammar UsecaseParser;
// Parser rules
usecaseDiagram
: 'usecase' NEWLINE* statement* EOF
;
options {
tokenVocab = UsecaseLexer;
}
// Entry point
start: USECASE NEWLINE* statement* EOF;
// Statement types
statement
: actorStatement
| relationshipStatement
@@ -18,16 +21,18 @@ statement
| NEWLINE
;
// Usecase statement (standalone entity)
usecaseStatement
: entityName NEWLINE*
;
// Actor statement
actorStatement
: 'actor' actorList NEWLINE*
: ACTOR actorList NEWLINE*
;
actorList
: actorName (',' actorName)*
: actorName (COMMA actorName)*
;
actorName
@@ -35,26 +40,26 @@ actorName
;
metadata
: '@' '{' metadataContent '}'
: AT LBRACE metadataContent RBRACE
;
metadataContent
: metadataProperty (',' metadataProperty)*
: metadataProperty (COMMA metadataProperty)*
;
metadataProperty
: STRING ':' STRING
: STRING COLON STRING
;
// Relationship statement
relationshipStatement
: entityName arrow entityName NEWLINE*
| actorDeclaration arrow entityName NEWLINE*
;
// System boundary statement
systemBoundaryStatement
: 'systemBoundary' systemBoundaryName NEWLINE* systemBoundaryContent* 'end' NEWLINE*
: SYSTEM_BOUNDARY systemBoundaryName NEWLINE* systemBoundaryContent* END NEWLINE*
;
systemBoundaryName
@@ -78,23 +83,25 @@ usecaseWithClass
| STRING CLASS_SEPARATOR IDENTIFIER
;
// System boundary type statement
systemBoundaryTypeStatement
: systemBoundaryName '@' '{' systemBoundaryTypeContent '}' NEWLINE*
: systemBoundaryName AT LBRACE systemBoundaryTypeContent RBRACE NEWLINE*
;
systemBoundaryTypeContent
: systemBoundaryTypeProperty (',' systemBoundaryTypeProperty)*
: systemBoundaryTypeProperty (COMMA systemBoundaryTypeProperty)*
;
systemBoundaryTypeProperty
: 'type' ':' systemBoundaryType
: TYPE COLON systemBoundaryType
;
systemBoundaryType
: 'package'
| 'rect'
: PACKAGE
| RECT
;
// Entity name (node reference)
entityName
: IDENTIFIER CLASS_SEPARATOR IDENTIFIER
| STRING CLASS_SEPARATOR IDENTIFIER
@@ -103,12 +110,14 @@ entityName
| nodeIdWithLabel
;
// Actor declaration (inline)
actorDeclaration
: 'actor' actorName
: ACTOR actorName
;
// Node with label
nodeIdWithLabel
: IDENTIFIER '(' nodeLabel ')'
: IDENTIFIER LPAREN nodeLabel RPAREN
;
nodeLabel
@@ -118,6 +127,7 @@ nodeLabel
| nodeLabel STRING
;
// Arrow types
arrow
: SOLID_ARROW
| BACK_ARROW
@@ -144,20 +154,22 @@ edgeLabel
| STRING
;
// Direction statement
directionStatement
: 'direction' direction NEWLINE*
: DIRECTION direction NEWLINE*
;
direction
: 'TB'
| 'TD'
| 'BT'
| 'RL'
| 'LR'
: TB
| TD
| BT
| RL
| LR
;
// Class definition statement
classDefStatement
: 'classDef' IDENTIFIER stylesOpt NEWLINE*
: CLASS_DEF IDENTIFIER stylesOpt NEWLINE*
;
stylesOpt
@@ -181,105 +193,18 @@ styleComponent
| PERCENT
;
// Class statement
classStatement
: 'class' nodeList IDENTIFIER NEWLINE*
: CLASS nodeList IDENTIFIER NEWLINE*
;
// Style statement
styleStatement
: 'style' IDENTIFIER stylesOpt NEWLINE*
: STYLE IDENTIFIER stylesOpt NEWLINE*
;
// Node list
nodeList
: IDENTIFIER (',' IDENTIFIER)*
: IDENTIFIER (COMMA IDENTIFIER)*
;
// Lexer rules
SOLID_ARROW
: '-->'
;
BACK_ARROW
: '<--'
;
CIRCLE_ARROW
: '--o'
;
CIRCLE_ARROW_REVERSED
: 'o--'
;
CROSS_ARROW
: '--x'
;
CROSS_ARROW_REVERSED
: 'x--'
;
LINE_SOLID
: '--'
;
COMMA
: ','
;
AT
: '@'
;
LBRACE
: '{'
;
RBRACE
: '}'
;
COLON
: ':'
;
CLASS_SEPARATOR
: ':::'
;
IDENTIFIER
: [a-zA-Z_][a-zA-Z0-9_]*
;
STRING
: '"' (~["\r\n])* '"'
| '\'' (~['\r\n])* '\''
;
HASH_COLOR
: '#' [a-fA-F0-9]+
;
NUMBER
: [0-9]+ ('.' [0-9]+)? ([a-zA-Z]+)?
;
// These tokens are defined last so they have lowest priority
// This ensures arrow tokens like '-->' are matched before DASH
DASH
: '-'
;
DOT
: '.'
;
PERCENT
: '%'
;
NEWLINE
: [\r\n]+
;
WS
: [ \t]+ -> skip
;

View File

@@ -0,0 +1,292 @@
import type {
UsecaseDB,
Actor,
UseCase,
SystemBoundary,
Relationship,
ClassDef,
ArrowType,
} from '../../usecaseTypes.js';
import { ARROW_TYPE } from '../../usecaseTypes.js';
import { log } from '../../../../logger.js';
/**
* Core shared logic for both Listener and Visitor patterns
* Contains all the proven parsing logic for usecase diagrams
*/
export class UsecaseParserCore {
protected db: UsecaseDB;
protected relationshipCounter = 0;
protected currentSystemBoundary: string | null = null;
protected currentSystemBoundaryUseCases: string[] = [];
constructor(db: UsecaseDB) {
this.db = db;
}
/**
* Browser-safe environment variable access
*/
protected getEnvVar(name: string): string | undefined {
try {
if (typeof process !== 'undefined' && process.env) {
return process.env[name];
}
} catch (_e) {
// process is not defined in browser
}
// Browser fallback
if (typeof window !== 'undefined' && (window as any).MERMAID_CONFIG) {
return (window as any).MERMAID_CONFIG[name];
}
return undefined;
}
/**
* Process actor statement
*/
protected processActorStatement(
actorId: string,
actorName: string,
metadata?: Record<string, string>
): void {
const actor: Actor = {
id: actorId,
name: actorName,
metadata,
};
this.db.addActor(actor);
log.debug(`Processed actor: ${actorId} (${actorName})`);
}
/**
* Process use case statement
*/
protected processUseCaseStatement(
useCaseId: string,
useCaseName: string,
nodeId?: string,
classes?: string[]
): void {
const useCase: UseCase = {
id: useCaseId,
name: useCaseName,
nodeId,
classes,
systemBoundary: this.currentSystemBoundary ?? undefined,
};
this.db.addUseCase(useCase);
// Add to current system boundary if we're inside one
if (this.currentSystemBoundary) {
this.currentSystemBoundaryUseCases.push(useCaseId);
}
log.debug(`Processed use case: ${useCaseId} (${useCaseName})`);
}
/**
* Process relationship statement
*/
protected processRelationship(from: string, to: string, arrowType: number, label?: string): void {
// Generate IDs for checking if entities exist
const fromId = this.generateId(from);
const toId = this.generateId(to);
// Ensure entities exist - if they're not actors, create them as use cases
if (!this.db.getActor(fromId) && !this.db.getUseCase(fromId)) {
this.db.addUseCase({ id: fromId, name: from });
log.debug(`Auto-created use case: ${fromId} (${from})`);
}
if (!this.db.getActor(toId) && !this.db.getUseCase(toId)) {
this.db.addUseCase({ id: toId, name: to });
log.debug(`Auto-created use case: ${toId} (${to})`);
}
const relationshipId = `rel_${this.relationshipCounter++}`;
// Determine relationship type based on arrow type and label
let type: 'association' | 'include' | 'extend' = 'association';
if (label) {
const lowerLabel = label.toLowerCase();
if (lowerLabel.includes('include')) {
type = 'include';
} else if (lowerLabel.includes('extend')) {
type = 'extend';
}
}
const relationship: Relationship = {
id: relationshipId,
from: fromId,
to: toId,
type,
arrowType: arrowType as ArrowType,
label,
};
this.db.addRelationship(relationship);
log.debug(`Processed relationship: ${fromId} -> ${toId} (${type})`);
}
/**
* Process system boundary start
*/
protected processSystemBoundaryStart(boundaryId: string, boundaryName: string): void {
this.currentSystemBoundary = boundaryId;
this.currentSystemBoundaryUseCases = [];
log.debug(`Started system boundary: ${boundaryId} (${boundaryName})`);
}
/**
* Process system boundary end
*/
protected processSystemBoundaryEnd(): void {
if (this.currentSystemBoundary) {
const systemBoundary: SystemBoundary = {
id: this.currentSystemBoundary,
name: this.currentSystemBoundary,
useCases: [...this.currentSystemBoundaryUseCases],
};
this.db.addSystemBoundary(systemBoundary);
log.debug(`Ended system boundary: ${this.currentSystemBoundary}`);
this.currentSystemBoundary = null;
this.currentSystemBoundaryUseCases = [];
}
}
/**
* Process system boundary type
*/
protected processSystemBoundaryType(boundaryId: string, type: 'package' | 'rect'): void {
const boundary = this.db.getSystemBoundary(boundaryId);
if (boundary) {
boundary.type = type;
log.debug(`Set system boundary type: ${boundaryId} -> ${type}`);
}
}
/**
* Process direction statement
*/
protected processDirectionStatement(direction: string): void {
const normalizedDirection = this.normalizeDirection(direction);
this.db.setDirection(normalizedDirection as any);
log.debug(`Set direction: ${normalizedDirection}`);
}
/**
* Normalize direction
*/
protected normalizeDirection(dir: string): string {
switch (dir) {
case 'TD':
return 'TB';
default:
return dir;
}
}
/**
* Process class definition statement
*/
protected processClassDefStatement(classId: string, styles: string[]): void {
const classDef: ClassDef = {
id: classId,
styles,
};
this.db.addClassDef(classDef);
log.debug(`Processed class definition: ${classId}`);
}
/**
* Process class statement (apply class to nodes)
*/
protected processClassStatement(nodeIds: string[], classId: string): void {
for (const nodeId of nodeIds) {
const useCase = this.db.getUseCase(nodeId);
if (useCase) {
if (!useCase.classes) {
useCase.classes = [];
}
if (!useCase.classes.includes(classId)) {
useCase.classes.push(classId);
}
log.debug(`Applied class ${classId} to use case ${nodeId}`);
}
}
}
/**
* Process style statement (apply styles directly to node)
*/
protected processStyleStatement(nodeId: string, styles: string[]): void {
const useCase = this.db.getUseCase(nodeId);
if (useCase) {
useCase.styles = styles;
log.debug(`Applied styles to use case ${nodeId}`);
}
const actor = this.db.getActor(nodeId);
if (actor) {
actor.styles = styles;
log.debug(`Applied styles to actor ${nodeId}`);
}
}
/**
* Extract text from string (remove quotes)
*/
protected extractString(text: string): string {
if (!text) {
return '';
}
// Remove surrounding quotes
if (
(text.startsWith('"') && text.endsWith('"')) ||
(text.startsWith("'") && text.endsWith("'"))
) {
return text.slice(1, -1);
}
return text;
}
/**
* Parse arrow type from token
*/
protected parseArrowType(arrowText: string): number {
switch (arrowText) {
case '-->':
return ARROW_TYPE.SOLID_ARROW;
case '<--':
return ARROW_TYPE.BACK_ARROW;
case '--':
return ARROW_TYPE.LINE_SOLID;
case '--o':
return ARROW_TYPE.CIRCLE_ARROW;
case '--x':
return ARROW_TYPE.CROSS_ARROW;
case 'o--':
return ARROW_TYPE.CIRCLE_ARROW_REVERSED;
case 'x--':
return ARROW_TYPE.CROSS_ARROW_REVERSED;
default:
return ARROW_TYPE.SOLID_ARROW;
}
}
/**
* Generate unique ID from name
*/
protected generateId(name: string): string {
return name.replace(/\W/g, '_');
}
}

View File

@@ -0,0 +1,533 @@
import { UsecaseParserCore } from './UsecaseParserCore.js';
import { log } from '../../../../logger.js';
import type { UsecaseDB } from '../../usecaseTypes.js';
/**
* Visitor implementation that builds the usecase model
* Uses the same core logic as the Listener for consistency
*/
export class UsecaseVisitor extends UsecaseParserCore {
private visitCount = 0;
constructor(db: UsecaseDB) {
super(db);
if (this.getEnvVar('ANTLR_DEBUG') === 'true') {
log.debug('🎯 UsecaseVisitor: Constructor called');
}
}
// Default visitor methods
visit(tree: any): any {
const shouldLog = this.getEnvVar('ANTLR_DEBUG') === 'true';
this.visitCount++;
if (shouldLog) {
log.debug(`🔍 UsecaseVisitor: Visiting node type: ${tree.constructor.name}`);
}
let result;
try {
result = tree.accept(this);
if (shouldLog) {
log.debug(`✅ UsecaseVisitor: Successfully visited ${tree.constructor.name}`);
}
} catch (error) {
log.error(`❌ UsecaseVisitor: Error visiting ${tree.constructor.name}:`, error);
throw error;
}
return result;
}
visitChildren(node: any): any {
if (!node) {
return null;
}
let result = null;
const n = node.getChildCount();
for (let i = 0; i < n; i++) {
const child = node.getChild(i);
if (child) {
const childResult = child.accept(this);
if (childResult !== null) {
result = childResult;
}
}
}
return result;
}
visitTerminal(_node: any): any {
return null;
}
visitErrorNode(_node: any): any {
log.error('❌ UsecaseVisitor: Error node encountered');
return null;
}
// Start rule
visitStart(ctx: any): any {
return this.visitChildren(ctx);
}
// Statement rule
visitStatement(ctx: any): any {
return this.visitChildren(ctx);
}
// Actor statement
visitActorStatement(ctx: any): any {
const actorList = ctx.actorList();
if (actorList) {
this.visitActorList(actorList);
}
return null;
}
visitActorList(ctx: any): any {
const actorNames = ctx.actorName();
if (actorNames) {
const names = Array.isArray(actorNames) ? actorNames : [actorNames];
for (const actorName of names) {
this.visitActorName(actorName);
}
}
return null;
}
visitActorName(ctx: any): any {
let actorName = '';
const identifier = ctx.IDENTIFIER();
if (identifier) {
actorName = identifier.getText();
} else {
const stringToken = ctx.STRING();
if (stringToken) {
actorName = this.extractString(stringToken.getText());
}
}
const actorId = this.generateId(actorName);
// Process metadata if present
let metadata: Record<string, string> | undefined;
const metadataCtx = ctx.metadata();
if (metadataCtx) {
metadata = this.visitMetadata(metadataCtx);
}
this.processActorStatement(actorId, actorName, metadata);
return null;
}
visitMetadata(ctx: any): Record<string, string> {
const metadata: Record<string, string> = {};
const content = ctx.metadataContent();
if (content) {
const properties = content.metadataProperty();
const props = Array.isArray(properties) ? properties : [properties];
for (const prop of props) {
const strings = prop.STRING();
if (strings && strings.length >= 2) {
const key = this.extractString(strings[0].getText());
const value = this.extractString(strings[1].getText());
metadata[key] = value;
}
}
}
return metadata;
}
// Relationship statement
visitRelationshipStatement(ctx: any): any {
let from = '';
let to = '';
let arrowType = 0;
let label: string | undefined;
// Get entity names
const entityNames = ctx.entityName();
if (entityNames && entityNames.length >= 2) {
from = this.visitEntityName(entityNames[0]);
to = this.visitEntityName(entityNames[1]);
} else if (ctx.actorDeclaration()) {
from = this.visitActorDeclaration(ctx.actorDeclaration());
if (entityNames && entityNames.length >= 1) {
to = this.visitEntityName(entityNames[0]);
}
}
// Get arrow type
const arrow = ctx.arrow();
if (arrow) {
const arrowResult = this.visitArrow(arrow);
arrowType = arrowResult.type;
label = arrowResult.label;
}
this.processRelationship(from, to, arrowType, label);
return null;
}
visitEntityName(ctx: any): string {
if (!ctx) {
return '';
}
const nodeIdWithLabel = ctx.nodeIdWithLabel();
if (nodeIdWithLabel) {
return this.visitNodeIdWithLabel(nodeIdWithLabel);
}
const identifiers = ctx.IDENTIFIER();
if (identifiers) {
if (Array.isArray(identifiers) && identifiers.length >= 2) {
// Has class separator (:::)
return identifiers[0].getText();
} else if (Array.isArray(identifiers) && identifiers.length === 1) {
return identifiers[0].getText();
}
}
const stringToken = ctx.STRING();
if (stringToken) {
return this.extractString(stringToken.getText());
}
return '';
}
visitActorDeclaration(ctx: any): string {
const actorName = ctx.actorName();
if (actorName) {
return this.visitActorName(actorName);
}
return '';
}
visitNodeIdWithLabel(ctx: any): string {
if (ctx.IDENTIFIER()) {
return ctx.IDENTIFIER().getText();
}
return '';
}
visitArrow(ctx: any): { type: number; label?: string } {
let arrowText = '';
let label: string | undefined;
if (ctx.labeledArrow()) {
const labeledArrow = ctx.labeledArrow();
const edgeLabel = labeledArrow.edgeLabel();
if (edgeLabel) {
if (edgeLabel.IDENTIFIER()) {
label = edgeLabel.IDENTIFIER().getText();
} else if (edgeLabel.STRING()) {
label = this.extractString(edgeLabel.STRING().getText());
}
}
// Determine arrow type from labeled arrow structure
if (labeledArrow.SOLID_ARROW()) {
arrowText = '-->';
} else if (labeledArrow.BACK_ARROW()) {
arrowText = '<--';
} else if (labeledArrow.CIRCLE_ARROW()) {
arrowText = '--o';
} else if (labeledArrow.CROSS_ARROW()) {
arrowText = '--x';
} else if (labeledArrow.CIRCLE_ARROW_REVERSED()) {
arrowText = 'o--';
} else if (labeledArrow.CROSS_ARROW_REVERSED()) {
arrowText = 'x--';
} else {
arrowText = '--';
}
} else {
// Simple arrow
if (ctx.SOLID_ARROW()) {
arrowText = '-->';
} else if (ctx.BACK_ARROW()) {
arrowText = '<--';
} else if (ctx.LINE_SOLID()) {
arrowText = '--';
} else if (ctx.CIRCLE_ARROW()) {
arrowText = '--o';
} else if (ctx.CROSS_ARROW()) {
arrowText = '--x';
} else if (ctx.CIRCLE_ARROW_REVERSED()) {
arrowText = 'o--';
} else if (ctx.CROSS_ARROW_REVERSED()) {
arrowText = 'x--';
}
}
return {
type: this.parseArrowType(arrowText),
label,
};
}
// System boundary statement
visitSystemBoundaryStatement(ctx: any): any {
const boundaryName = ctx.systemBoundaryName();
let boundaryId = '';
let boundaryNameText = '';
if (boundaryName) {
if (boundaryName.IDENTIFIER()) {
boundaryNameText = boundaryName.IDENTIFIER().getText();
} else if (boundaryName.STRING()) {
boundaryNameText = this.extractString(boundaryName.STRING().getText());
}
boundaryId = this.generateId(boundaryNameText);
}
this.processSystemBoundaryStart(boundaryId, boundaryNameText);
// Visit boundary content
const contents = ctx.systemBoundaryContent();
if (contents) {
const contentList = Array.isArray(contents) ? contents : [contents];
for (const content of contentList) {
this.visitSystemBoundaryContent(content);
}
}
this.processSystemBoundaryEnd();
return null;
}
visitSystemBoundaryContent(ctx: any): any {
const usecaseInBoundary = ctx.usecaseInBoundary();
if (usecaseInBoundary) {
this.visitUsecaseInBoundary(usecaseInBoundary);
}
return null;
}
visitUsecaseInBoundary(ctx: any): any {
let useCaseId = '';
let useCaseName = '';
let classes: string[] | undefined;
if (ctx.usecaseWithClass()) {
const withClass = ctx.usecaseWithClass();
if (withClass.IDENTIFIER()) {
const identifiers = withClass.IDENTIFIER();
if (Array.isArray(identifiers) && identifiers.length >= 2) {
useCaseId = identifiers[0].getText();
useCaseName = useCaseId;
classes = [identifiers[1].getText()];
}
} else if (withClass.STRING()) {
useCaseName = this.extractString(withClass.STRING().getText());
useCaseId = this.generateId(useCaseName);
const identifiers = withClass.IDENTIFIER();
if (identifiers) {
classes = [identifiers.getText()];
}
}
} else if (ctx.IDENTIFIER()) {
useCaseId = ctx.IDENTIFIER().getText();
useCaseName = useCaseId;
} else if (ctx.STRING()) {
useCaseName = this.extractString(ctx.STRING().getText());
useCaseId = this.generateId(useCaseName);
}
if (useCaseId && useCaseName) {
this.processUseCaseStatement(useCaseId, useCaseName, undefined, classes);
}
return null;
}
// System boundary type statement
visitSystemBoundaryTypeStatement(ctx: any): any {
const boundaryName = ctx.systemBoundaryName();
let boundaryId = '';
if (boundaryName) {
if (boundaryName.IDENTIFIER()) {
boundaryId = boundaryName.IDENTIFIER().getText();
} else if (boundaryName.STRING()) {
boundaryId = this.generateId(this.extractString(boundaryName.STRING().getText()));
}
}
const typeContent = ctx.systemBoundaryTypeContent();
if (typeContent) {
const properties = typeContent.systemBoundaryTypeProperty();
const props = Array.isArray(properties) ? properties : [properties];
for (const prop of props) {
const type = prop.systemBoundaryType();
if (type) {
let typeValue: 'package' | 'rect' = 'rect';
if (type.PACKAGE()) {
typeValue = 'package';
} else if (type.RECT()) {
typeValue = 'rect';
}
this.processSystemBoundaryType(boundaryId, typeValue);
}
}
}
return null;
}
// Direction statement
visitDirectionStatement(ctx: any): any {
const direction = ctx.direction();
if (direction) {
let directionText = '';
if (direction.TB()) {
directionText = 'TB';
} else if (direction.TD()) {
directionText = 'TD';
} else if (direction.BT()) {
directionText = 'BT';
} else if (direction.RL()) {
directionText = 'RL';
} else if (direction.LR()) {
directionText = 'LR';
}
this.processDirectionStatement(directionText);
}
return null;
}
// Class definition statement
visitClassDefStatement(ctx: any): any {
let classId = '';
if (ctx.IDENTIFIER()) {
classId = ctx.IDENTIFIER().getText();
}
const styles: string[] = [];
const stylesOpt = ctx.stylesOpt();
if (stylesOpt) {
this.collectStyles(stylesOpt, styles);
}
this.processClassDefStatement(classId, styles);
return null;
}
// Class statement
visitClassStatement(ctx: any): any {
const nodeList = ctx.nodeList();
const nodeIds: string[] = [];
if (nodeList) {
const identifiers = nodeList.IDENTIFIER();
const ids = Array.isArray(identifiers) ? identifiers : [identifiers];
for (const id of ids) {
nodeIds.push(id.getText());
}
}
let classId = '';
const identifiers = ctx.IDENTIFIER();
if (identifiers) {
const ids = Array.isArray(identifiers) ? identifiers : [identifiers];
if (ids.length > 0) {
classId = ids[ids.length - 1].getText();
}
}
this.processClassStatement(nodeIds, classId);
return null;
}
// Style statement
visitStyleStatement(ctx: any): any {
let nodeId = '';
if (ctx.IDENTIFIER()) {
nodeId = ctx.IDENTIFIER().getText();
}
const styles: string[] = [];
const stylesOpt = ctx.stylesOpt();
if (stylesOpt) {
this.collectStyles(stylesOpt, styles);
}
this.processStyleStatement(nodeId, styles);
return null;
}
// Usecase statement
visitUsecaseStatement(ctx: any): any {
const entityName = ctx.entityName();
if (entityName) {
const useCaseId = this.visitEntityName(entityName);
this.processUseCaseStatement(useCaseId, useCaseId);
}
return null;
}
// Helper method to collect styles
private collectStyles(ctx: any, styles: string[]): void {
if (!ctx) {
return;
}
// Visit all style components
const styleComponents = this.getAllStyleComponents(ctx);
for (const component of styleComponents) {
styles.push(component.getText());
}
}
private getAllStyleComponents(ctx: any): any[] {
const components: any[] = [];
if (ctx.style) {
const styleCtx = ctx.style();
if (styleCtx) {
this.collectStyleComponents(styleCtx, components);
}
}
if (ctx.stylesOpt) {
const stylesOptList = Array.isArray(ctx.stylesOpt()) ? ctx.stylesOpt() : [ctx.stylesOpt()];
for (const opt of stylesOptList) {
if (opt) {
this.collectStyleComponents(opt, components);
}
}
}
return components;
}
private collectStyleComponents(ctx: any, components: any[]): void {
if (!ctx) {
return;
}
if (ctx.styleComponent) {
const comp = ctx.styleComponent();
if (comp) {
components.push(comp);
}
}
if (ctx.style) {
const styleCtx = ctx.style();
if (styleCtx) {
this.collectStyleComponents(styleCtx, components);
}
}
}
}

View File

@@ -0,0 +1,181 @@
/**
* ANTLR-based Usecase Diagram Parser
*
* This is a proper ANTLR implementation using antlr-ng generated parser code.
* It provides the same interface as the existing parser for 100% compatibility.
*
* Follows the same structure as flowchart and sequence ANTLR parsers with both
* listener and visitor pattern support.
*/
import { CharStream, CommonTokenStream, ParseTreeWalker } from 'antlr4ng';
import { UsecaseLexer } from './generated/UsecaseLexer.js';
import { UsecaseParser } from './generated/UsecaseParser.js';
import { UsecaseListener } from './UsecaseListener.js';
import { UsecaseVisitor } from './UsecaseVisitor.js';
import { UsecaseErrorListener } from './UsecaseErrorListener.js';
import type { UsecaseDB } from '../../usecaseTypes.js';
import { log } from '../../../../logger.js';
/**
* Main ANTLR parser class that provides the same interface as the existing parser
*/
export class ANTLRUsecaseParser {
yy: UsecaseDB | null;
constructor() {
this.yy = null;
}
/**
* Parse usecase diagram input using ANTLR
*
* @param input - The usecase diagram text to parse
* @returns The database instance populated with parsed data
*/
parse(input: string): UsecaseDB {
if (!this.yy) {
throw new Error('Usecase ANTLR parser missing yy (database).');
}
const startTime = performance.now();
// Get environment variable helper
const getEnvVar = (name: string): string | undefined => {
try {
if (typeof process !== 'undefined' && process.env) {
return process.env[name];
}
} catch (_e) {
// process is not defined in browser
}
// Browser fallback
if (typeof window !== 'undefined' && (window as any).MERMAID_CONFIG) {
return (window as any).MERMAID_CONFIG[name];
}
return undefined;
};
const shouldLog = getEnvVar('ANTLR_DEBUG') === 'true';
if (shouldLog) {
log.debug('🎯 ANTLR Usecase Parser: Starting parse');
log.debug(`📝 Input length: ${input.length} characters`);
}
try {
// Reset database state
if (this.yy.clear) {
this.yy.clear();
}
// Create input stream and lexer
const inputStream = CharStream.fromString(input);
const lexer = new UsecaseLexer(inputStream);
// Add custom error listener to lexer
const lexerErrorListener = new UsecaseErrorListener();
lexer.removeErrorListeners();
lexer.addErrorListener(lexerErrorListener);
const tokenStream = new CommonTokenStream(lexer);
// Create parser
const parser = new UsecaseParser(tokenStream);
// Add custom error listener to parser
const parserErrorListener = new UsecaseErrorListener();
parser.removeErrorListeners();
parser.addErrorListener(parserErrorListener);
// Generate parse tree
if (shouldLog) {
log.debug('🌳 ANTLR Usecase Parser: Starting parse tree generation');
}
const tree = parser.start();
// Check for syntax errors
if (lexerErrorListener.hasErrors()) {
throw lexerErrorListener.createDetailedError();
}
if (parserErrorListener.hasErrors()) {
throw parserErrorListener.createDetailedError();
}
if (shouldLog) {
log.debug('✅ ANTLR Usecase Parser: Parse tree generated successfully');
}
// Check if we should use Visitor or Listener pattern
// Default to Visitor pattern (true) unless explicitly set to false
const useVisitorPattern = getEnvVar('USE_ANTLR_VISITOR') !== 'false';
if (shouldLog) {
log.debug('🔧 Usecase Parser: Pattern =', useVisitorPattern ? 'Visitor' : 'Listener');
}
if (useVisitorPattern) {
const visitor = new UsecaseVisitor(this.yy);
try {
visitor.visit(tree);
if (shouldLog) {
log.debug('✅ ANTLR Usecase Parser: Visitor completed successfully');
}
} catch (error) {
log.error(
'❌ ANTLR Usecase Parser: Visitor failed:',
error instanceof Error ? error.message : String(error)
);
log.error(
'❌ ANTLR Usecase Parser: Visitor stack:',
error instanceof Error ? error.stack : undefined
);
throw error;
}
} else {
const listener = new UsecaseListener(this.yy);
try {
ParseTreeWalker.DEFAULT.walk(listener, tree);
if (shouldLog) {
log.debug('✅ ANTLR Usecase Parser: Listener completed successfully');
}
} catch (error) {
log.error(
'❌ ANTLR Usecase Parser: Listener failed:',
error instanceof Error ? error.message : String(error)
);
log.error(
'❌ ANTLR Usecase Parser: Listener stack:',
error instanceof Error ? error.stack : undefined
);
throw error;
}
}
const totalTime = performance.now() - startTime;
if (shouldLog) {
log.debug(`⏱️ Total parse time: ${totalTime.toFixed(2)}ms`);
log.debug('✅ ANTLR Usecase Parser: Parse completed successfully');
}
return this.yy;
} catch (error) {
const totalTime = performance.now() - startTime;
log.error(`❌ ANTLR usecase parsing error after ${totalTime.toFixed(2)}ms:`, error);
log.error('📝 Input that caused error (first 500 chars):', input.substring(0, 500));
throw error;
}
}
// Provide the same interface as existing parser
setYY(yy: UsecaseDB) {
this.yy = yy;
}
}
// Export for compatibility with existing code
export const parser = new ANTLRUsecaseParser();
export default parser;

View File

@@ -1,8 +1,7 @@
import { vi } from 'vitest';
import { setSiteConfig } from '../../diagram-api/diagramAPI.js';
import mermaidAPI from '../../mermaidAPI.js';
import { vi, describe, it, expect, beforeEach, beforeAll } from 'vitest';
import { Diagram } from '../../Diagram.js';
import { addDiagrams } from '../../diagram-api/diagram-orchestration.js';
import { db } from './usecaseDb.js';
beforeAll(async () => {
// Is required to load the useCase diagram
@@ -70,29 +69,433 @@ vi.mock('d3', () => {
addDiagrams();
/**
* @param conf - Configuration object
* @param key - Configuration key
* @param value - Configuration value
*/
function addConf(conf: any, key: any, value: any) {
if (value !== undefined) {
conf[key] = value;
}
return conf;
}
describe('UseCase diagram with ANTLR parser', () => {
it('should parse actors and use cases correctly', async () => {
const diagram = await Diagram.fromText(
`usecase
actor Developer1
actor Developer2
Developer1 --> a("Login System")
Developer2 --> b(Authentication)`
);
beforeEach(() => {
db.clear();
});
expect(diagram).toBeDefined();
expect(diagram.type).toBe('usecase');
describe('when parsing basic actors', () => {
it('should parse a single actor', async () => {
const diagram = await Diagram.fromText(
`usecase
actor User`
);
expect(diagram).toBeDefined();
expect(diagram.type).toBe('usecase');
const actors = db.getActors();
expect(actors.size).toBe(1);
expect(actors.has('User')).toBe(true);
expect(actors.get('User')?.name).toBe('User');
});
it('should parse multiple actors', async () => {
await Diagram.fromText(
`usecase
actor User
actor Admin
actor Guest`
);
const actors = db.getActors();
expect(actors.size).toBe(3);
expect(actors.has('User')).toBe(true);
expect(actors.has('Admin')).toBe(true);
expect(actors.has('Guest')).toBe(true);
});
it('should parse actor with simple name', async () => {
await Diagram.fromText(
`usecase
actor SystemUser`
);
const actors = db.getActors();
expect(actors.size).toBe(1);
expect(actors.has('SystemUser')).toBe(true);
});
});
describe('when parsing use cases', () => {
it('should parse use cases from relationships', async () => {
await Diagram.fromText(
`usecase
actor User
User --> Login`
);
const useCases = db.getUseCases();
expect(useCases.size).toBe(1);
expect(useCases.has('Login')).toBe(true);
});
it('should parse multiple use cases from relationships', async () => {
await Diagram.fromText(
`usecase
actor User
User --> Login
User --> Logout
User --> Register`
);
const useCases = db.getUseCases();
expect(useCases.size).toBe(3);
expect(useCases.has('Login')).toBe(true);
expect(useCases.has('Logout')).toBe(true);
expect(useCases.has('Register')).toBe(true);
});
it('should parse use case from relationship', async () => {
await Diagram.fromText(
`usecase
actor User
User --> UserLoginProcess`
);
const useCases = db.getUseCases();
expect(useCases.size).toBe(1);
expect(useCases.has('UserLoginProcess')).toBe(true);
});
it('should parse use cases with quoted names', async () => {
await Diagram.fromText(
`usecase
actor "Customer Service"
actor "System Administrator"
"Customer Service" --> "Handle Tickets"
"System Administrator" --> "Manage System"`
);
const actors = db.getActors();
expect(actors.size).toBe(2);
// IDs are generated with underscores replacing spaces
expect(actors.has('Customer_Service')).toBe(true);
expect(actors.has('System_Administrator')).toBe(true);
// But names should preserve the original text
expect(actors.get('Customer_Service')?.name).toBe('Customer Service');
expect(actors.get('System_Administrator')?.name).toBe('System Administrator');
const useCases = db.getUseCases();
expect(useCases.size).toBe(2);
expect(useCases.has('Handle_Tickets')).toBe(true);
expect(useCases.has('Manage_System')).toBe(true);
expect(useCases.get('Handle_Tickets')?.name).toBe('Handle Tickets');
expect(useCases.get('Manage_System')?.name).toBe('Manage System');
});
});
describe('when parsing relationships', () => {
it('should parse actor to use case relationship', async () => {
await Diagram.fromText(
`usecase
actor User
User --> Login`
);
const relationships = db.getRelationships();
expect(relationships.length).toBe(1);
expect(relationships[0].from).toBe('User');
expect(relationships[0].to).toBe('Login');
expect(relationships[0].type).toBe('association');
});
it('should parse multiple relationships', async () => {
await Diagram.fromText(
`usecase
actor User
User --> Login
User --> Logout`
);
const relationships = db.getRelationships();
expect(relationships.length).toBe(2);
expect(relationships[0].from).toBe('User');
expect(relationships[0].to).toBe('Login');
expect(relationships[1].from).toBe('User');
expect(relationships[1].to).toBe('Logout');
});
it('should parse relationship with label', async () => {
await Diagram.fromText(
`usecase
actor Developer
Developer --important--> WriteCode`
);
const relationships = db.getRelationships();
expect(relationships.length).toBe(1);
expect(relationships[0].label).toBe('important');
});
it('should parse different arrow types', async () => {
await Diagram.fromText(
`usecase
actor User
actor Admin
User --> Login
Admin <-- Logout
User -- ViewData`
);
const relationships = db.getRelationships();
expect(relationships.length).toBe(3);
});
});
describe('when parsing system boundaries', () => {
it('should parse a system boundary', async () => {
await Diagram.fromText(
`usecase
actor Admin, User
systemBoundary "Authentication"
Login
Logout
end
Admin --> Login
User --> Login`
);
const boundaries = db.getSystemBoundaries();
expect(boundaries.size).toBeGreaterThan(0);
});
it('should parse use cases within system boundary', async () => {
await Diagram.fromText(
`usecase
actor User
systemBoundary "Authentication System"
Login
Logout
end
User --> Login`
);
const useCases = db.getUseCases();
expect(useCases.size).toBe(2);
expect(useCases.has('Login')).toBe(true);
expect(useCases.has('Logout')).toBe(true);
});
});
describe('when parsing direction', () => {
it('should handle TB direction', async () => {
await Diagram.fromText(
`usecase
direction TB
actor User`
);
expect(db.getDirection()).toBe('TB');
});
it('should handle LR direction', async () => {
await Diagram.fromText(
`usecase
direction LR
actor User`
);
expect(db.getDirection()).toBe('LR');
});
it('should normalize TD to TB', async () => {
await Diagram.fromText(
`usecase
direction TD
actor User`
);
expect(db.getDirection()).toBe('TB');
});
});
describe('when parsing actor metadata', () => {
it('should parse actor with metadata', async () => {
await Diagram.fromText(
`usecase
actor User@{ "type" : "primary", "icon" : "user" }
User --> Login`
);
const actors = db.getActors();
expect(actors.size).toBe(1);
const user = actors.get('User');
expect(user).toBeDefined();
expect(user?.metadata).toBeDefined();
});
it('should parse multiple actors with different metadata', async () => {
await Diagram.fromText(
`usecase
actor User@{ "type" : "primary", "icon" : "user" }
actor Admin@{ "type" : "secondary", "icon" : "admin" }
User --> Login
Admin --> ManageUsers`
);
const actors = db.getActors();
expect(actors.size).toBe(2);
});
});
describe('when parsing complex diagrams', () => {
it('should parse a complete authentication system', async () => {
await Diagram.fromText(
`usecase
actor User
actor Admin
systemBoundary "Authentication System"
Login
Logout
Register
ResetPassword
end
User --> Login
User --> Register
User --> Logout
Admin --> Login`
);
const actors = db.getActors();
const useCases = db.getUseCases();
const relationships = db.getRelationships();
expect(actors.size).toBe(2);
expect(useCases.size).toBe(4);
expect(relationships.length).toBe(4);
});
it('should parse diagram with multiple arrow types', async () => {
await Diagram.fromText(
`usecase
actor User
actor Admin
User --> Login
Admin <-- Logout
User -- ViewData`
);
const relationships = db.getRelationships();
expect(relationships.length).toBe(3);
});
it('should handle use case creation from relationships', async () => {
await Diagram.fromText(
`usecase
actor Developer
Developer --> LoginSystem
Developer --> Authentication`
);
const useCases = db.getUseCases();
expect(useCases.size).toBe(2);
expect(useCases.has('LoginSystem')).toBe(true);
expect(useCases.has('Authentication')).toBe(true);
});
});
describe('when parsing class definitions', () => {
it('should handle classDef', async () => {
await Diagram.fromText(
`usecase
actor User
User --> Login
classDef important fill:#f96,stroke:#333,stroke-width:4px
class Login important`
);
const classDefs = db.getClassDefs();
expect(classDefs.size).toBeGreaterThan(0);
});
});
describe('database methods', () => {
it('should clear all data', async () => {
await Diagram.fromText(
`usecase
actor User
User --> Login`
);
expect(db.getActors().size).toBe(1);
expect(db.getUseCases().size).toBe(1);
expect(db.getRelationships().length).toBe(1);
db.clear();
expect(db.getActors().size).toBe(0);
expect(db.getUseCases().size).toBe(0);
expect(db.getRelationships().length).toBe(0);
});
it('should get specific actor by id', async () => {
await Diagram.fromText(
`usecase
actor User
actor Admin`
);
const user = db.getActor('User');
expect(user).toBeDefined();
expect(user?.id).toBe('User');
expect(user?.name).toBe('User');
});
it('should get specific use case by id', async () => {
await Diagram.fromText(
`usecase
actor User
User --> Login
User --> Logout`
);
const login = db.getUseCase('Login');
expect(login).toBeDefined();
expect(login?.id).toBe('Login');
expect(login?.name).toBe('Login');
});
it('should return undefined for non-existent actor', () => {
const actor = db.getActor('NonExistent');
expect(actor).toBeUndefined();
});
it('should return undefined for non-existent use case', () => {
const useCase = db.getUseCase('NonExistent');
expect(useCase).toBeUndefined();
});
});
describe('getData method', () => {
it('should convert diagram data to LayoutData format', async () => {
await Diagram.fromText(
`usecase
actor User
User --> Login`
);
const data = db.getData();
expect(data).toBeDefined();
expect(data.nodes).toBeDefined();
expect(data.edges).toBeDefined();
expect(data.nodes.length).toBe(2);
expect(data.edges.length).toBe(1);
expect(data.type).toBe('usecase');
});
it('should include direction in layout data', async () => {
await Diagram.fromText(
`usecase
direction LR
actor User`
);
const data = db.getData();
expect(data.direction).toBe('LR');
});
});
});

View File

@@ -1,160 +1,30 @@
// Import ANTLR parser from the parser package
import { parse } from '@mermaid-js/parser';
// Import local ANTLR parser
import { log } from '../../logger.js';
import type { ParserDefinition } from '../../diagram-api/types.js';
import { populateCommonDb } from '../common/populateCommonDb.js';
import type {
UsecaseDB,
Actor,
UseCase,
SystemBoundary,
Relationship,
ArrowType,
ClassDef,
} from './usecaseTypes.js';
import { db } from './usecaseDb.js';
// ANTLR parser result interface
interface UsecaseParseResult {
actors: { id: string; name: string; metadata?: Record<string, string>; styles?: string[] }[];
useCases: {
id: string;
name: string;
nodeId?: string;
systemBoundary?: string;
classes?: string[];
styles?: string[];
}[];
systemBoundaries: {
id: string;
name: string;
useCases: string[];
type?: 'package' | 'rect';
styles?: string[];
}[];
relationships: {
id: string;
from: string;
to: string;
type: 'association' | 'include' | 'extend';
arrowType: number;
label?: string;
}[];
classDefs?: Map<string, { id: string; styles: string[] }>;
direction?: string;
accDescr?: string;
accTitle?: string;
title?: string;
}
// Import local ANTLR parser implementation
import antlrParser from './parser/antlr/antlr-parser.js';
/**
* Parse usecase diagram using ANTLR parser
* Parse usecase diagram using local ANTLR parser
*/
const parseUsecaseWithAntlr = async (input: string): Promise<UsecaseParseResult> => {
// Use the ANTLR parser from @mermaid-js/parser
const result = (await parse('usecase', input)) as UsecaseParseResult;
return result;
};
const parseUsecaseWithLocalAntlr = (input: string) => {
// Set the database instance
antlrParser.yy = db;
/**
* Populate the database with parsed ANTLR results
*/
const populateDb = (ast: UsecaseParseResult, db: UsecaseDB) => {
// Clear existing data
db.clear();
// Add actors (ANTLR result already has id, name, and metadata)
ast.actors.forEach((actorData) => {
const actor: Actor = {
id: actorData.id,
name: actorData.name,
metadata: actorData.metadata,
styles: actorData.styles,
};
db.addActor(actor);
});
// Add use cases (ANTLR result already has id, name, nodeId, systemBoundary, and classes)
ast.useCases.forEach((useCaseData) => {
const useCase: UseCase = {
id: useCaseData.id,
name: useCaseData.name,
nodeId: useCaseData.nodeId,
systemBoundary: useCaseData.systemBoundary,
classes: useCaseData.classes,
styles: useCaseData.styles,
};
db.addUseCase(useCase);
});
// Add system boundaries
if (ast.systemBoundaries) {
ast.systemBoundaries.forEach((boundaryData) => {
const systemBoundary: SystemBoundary = {
id: boundaryData.id,
name: boundaryData.name,
useCases: boundaryData.useCases,
type: boundaryData.type || 'rect', // default to 'rect' if not specified
styles: boundaryData.styles,
};
db.addSystemBoundary(systemBoundary);
});
}
// Add relationships (ANTLR result already has proper structure)
ast.relationships.forEach((relationshipData) => {
const relationship: Relationship = {
id: relationshipData.id,
from: relationshipData.from,
to: relationshipData.to,
type: relationshipData.type,
arrowType: relationshipData.arrowType as ArrowType,
label: relationshipData.label,
};
db.addRelationship(relationship);
});
// Add class definitions
if (ast.classDefs) {
ast.classDefs.forEach((classDefData) => {
const classDef: ClassDef = {
id: classDefData.id,
styles: classDefData.styles,
};
db.addClassDef(classDef);
});
}
// Set direction if provided
if (ast.direction) {
db.setDirection(ast.direction as any);
}
log.debug('Populated usecase database:', {
actors: ast.actors.length,
useCases: ast.useCases.length,
relationships: ast.relationships.length,
classDefs: ast.classDefs?.size ?? 0,
direction: ast.direction,
});
// Parse and return the populated database
return antlrParser.parse(input);
};
export const parser: ParserDefinition = {
parse: async (input: string): Promise<void> => {
log.debug('Parsing usecase diagram with ANTLR:', input);
parse: (input: string): void => {
log.debug('Parsing usecase diagram with local ANTLR parser:', input);
try {
// Use our ANTLR parser
const ast: UsecaseParseResult = await parseUsecaseWithAntlr(input);
log.debug('ANTLR parsing result:', ast);
// Populate common database fields
populateCommonDb(ast as any, db);
// Populate the database with validation
populateDb(ast, db);
log.debug('Usecase diagram parsing completed successfully');
// Use local ANTLR parser
parseUsecaseWithLocalAntlr(input);
log.debug('ANTLR parsing completed successfully');
} catch (error) {
log.error('Error parsing usecase diagram:', error);

View File

@@ -1,14 +1,5 @@
{
"projectName": "Mermaid",
"grammars": [
{
"id": "usecase",
"grammar": "src/language/usecase/Usecase.g4",
"outputDir": "src/language/usecase/generated",
"language": "TypeScript",
"generateVisitor": true,
"generateListener": true
}
],
"grammars": [],
"mode": "production"
}

View File

@@ -45,4 +45,3 @@ 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';

View File

@@ -1,4 +0,0 @@
export * from './module.js';
export * from './types.js';
export * from './parser.js';
export * from './visitor.js';

View File

@@ -1,50 +0,0 @@
/**
* 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;
}

View File

@@ -1,194 +0,0 @@
/**
* 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);
}

View File

@@ -1,85 +0,0 @@
/**
* 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, // --
CIRCLE_ARROW: 3, // --o
CROSS_ARROW: 4, // --x
CIRCLE_ARROW_REVERSED: 5, // o--
CROSS_ARROW_REVERSED: 6, // x--
} 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;
styles?: string[]; // Direct CSS styles applied to this actor
}
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
classes?: string[]; // CSS classes applied to this use case
styles?: string[]; // Direct CSS styles applied to this use case
}
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')
styles?: string[]; // Direct CSS styles applied to this system boundary
}
export interface Relationship {
id: string;
from: string;
to: string;
type: 'association' | 'include' | 'extend';
arrowType: ArrowType;
label?: string;
}
export interface ClassDef {
id: string;
styles: string[];
}
export interface UsecaseParseResult {
actors: Actor[];
useCases: UseCase[];
systemBoundaries: SystemBoundary[];
relationships: Relationship[];
classDefs?: Map<string, ClassDef>;
direction?: string;
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;
}

View File

@@ -1,894 +0,0 @@
/**
* 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,
UsecaseWithClassContext,
UsecaseStatementContext,
ActorNameContext,
ActorDeclarationContext,
NodeIdWithLabelContext,
NodeLabelContext,
MetadataContext,
MetadataContentContext,
MetadataPropertyContext,
EntityNameContext,
ArrowContext,
LabeledArrowContext,
EdgeLabelContext,
DirectionStatementContext,
DirectionContext,
ClassDefStatementContext,
ClassStatementContext,
NodeListContext,
} from './generated/UsecaseParser.js';
import { ARROW_TYPE } from './types.js';
import type {
Actor,
UseCase,
SystemBoundary,
Relationship,
UsecaseParseResult,
ArrowType,
ClassDef,
} from './types.js';
export class UsecaseAntlrVisitor extends UsecaseVisitor<void> {
private actors: Actor[] = [];
private useCases: UseCase[] = [];
private systemBoundaries: SystemBoundary[] = [];
private relationships: Relationship[] = [];
private relationshipCounter = 0;
private direction = 'TB'; // Default direction
private classDefs = new Map<string, ClassDef>();
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.visitDirectionStatement = this.visitDirectionStatementImpl.bind(this);
this.visitClassDefStatement = this.visitClassDefStatementImpl.bind(this);
this.visitClassStatement = this.visitClassStatementImpl.bind(this);
this.visitStyleStatement = this.visitStyleStatementImpl.bind(this);
this.visitUsecaseStatement = this.visitUsecaseStatementImpl.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;
this.direction = 'TB'; // Reset direction to default
// 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 | directionStatement | classDefStatement | classStatement | usecaseStatement | 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()!);
} else if (ctx.directionStatement?.()) {
this.visitDirectionStatementImpl(ctx.directionStatement()!);
} else if (ctx.classDefStatement?.()) {
const classDefStmt = ctx.classDefStatement();
if (classDefStmt) {
this.visitClassDefStatementImpl(classDefStmt);
}
} else if (ctx.classStatement?.()) {
const classStmt = ctx.classStatement();
if (classStmt) {
this.visitClassStatementImpl(classStmt);
}
} else if (ctx.styleStatement?.()) {
this.visitStyleStatementImpl(ctx.styleStatement());
} else if (ctx.usecaseStatement?.()) {
const usecaseStmt = ctx.usecaseStatement();
if (usecaseStmt) {
this.visitUsecaseStatementImpl(usecaseStmt);
}
}
// 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 : usecaseWithClass | IDENTIFIER | STRING ;
*/
private visitUsecaseInBoundaryImpl(ctx: UsecaseInBoundaryContext): string {
// Check for usecaseWithClass (e.g., "debugging:::case1")
const usecaseWithClass = ctx.usecaseWithClass?.();
if (usecaseWithClass) {
return this.visitUsecaseWithClassImpl(usecaseWithClass);
}
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 usecaseWithClass rule
* Grammar: usecaseWithClass : IDENTIFIER CLASS_SEPARATOR IDENTIFIER | STRING CLASS_SEPARATOR IDENTIFIER ;
*/
private visitUsecaseWithClassImpl(ctx: UsecaseWithClassContext): string {
let usecaseName = '';
let className = '';
const identifier0 = ctx.IDENTIFIER(0);
const identifier1 = ctx.IDENTIFIER(1);
const string = ctx.STRING();
if (identifier0 && identifier1) {
// IDENTIFIER:::IDENTIFIER
usecaseName = identifier0.getText();
className = identifier1.getText();
} else if (string && identifier0) {
// STRING:::IDENTIFIER
const text = string.getText();
usecaseName = text.slice(1, -1); // Remove quotes
className = identifier0.getText();
}
// Apply class to the use case
if (usecaseName && className) {
this.applyClassToEntity(usecaseName, className);
}
return usecaseName;
}
/**
* 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 CLASS_SEPARATOR IDENTIFIER | STRING CLASS_SEPARATOR IDENTIFIER | IDENTIFIER | STRING | nodeIdWithLabel ;
*/
private visitEntityNameImpl(ctx: EntityNameContext): string {
const classSeparator = ctx.CLASS_SEPARATOR?.();
// Check for class application syntax (e.g., "debugging:::case1")
if (classSeparator) {
let entityName = '';
let className = '';
const identifier0 = ctx.IDENTIFIER(0);
const identifier1 = ctx.IDENTIFIER(1);
const string0 = ctx.STRING();
if (identifier0 && identifier1) {
// IDENTIFIER:::IDENTIFIER
entityName = identifier0.getText();
className = identifier1.getText();
} else if (string0 && identifier0) {
// STRING:::IDENTIFIER
const text = string0.getText();
entityName = text.slice(1, -1); // Remove quotes
className = identifier0.getText();
}
// Apply class to the entity
if (entityName && className) {
this.applyClassToEntity(entityName, className);
}
return entityName;
}
// Regular entity name without class
const identifier = ctx.IDENTIFIER(0);
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 '';
}
/**
* Apply a class to an entity (use case)
*/
private applyClassToEntity(entityName: string, className: string): void {
// Find or create the use case
let useCase = this.useCases.find((uc) => uc.id === entityName);
if (!useCase) {
useCase = {
id: entityName,
name: entityName,
classes: [],
};
this.useCases.push(useCase);
}
// Add the class if not already present
if (!useCase.classes) {
useCase.classes = [];
}
if (!useCase.classes.includes(className)) {
useCase.classes.push(className);
}
}
/**
* 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 | CIRCLE_ARROW | CROSS_ARROW | CIRCLE_ARROW_REVERSED | CROSS_ARROW_REVERSED | 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 };
} else if (ctx.CIRCLE_ARROW()) {
return { arrowType: ARROW_TYPE.CIRCLE_ARROW };
} else if (ctx.CROSS_ARROW()) {
return { arrowType: ARROW_TYPE.CROSS_ARROW };
} else if (ctx.CIRCLE_ARROW_REVERSED()) {
return { arrowType: ARROW_TYPE.CIRCLE_ARROW_REVERSED };
} else if (ctx.CROSS_ARROW_REVERSED()) {
return { arrowType: ARROW_TYPE.CROSS_ARROW_REVERSED };
}
// 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 | LINE_SOLID edgeLabel CIRCLE_ARROW | LINE_SOLID edgeLabel CROSS_ARROW | CIRCLE_ARROW_REVERSED edgeLabel LINE_SOLID | CROSS_ARROW_REVERSED 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 if (ctx.CIRCLE_ARROW()) {
return { arrowType: ARROW_TYPE.CIRCLE_ARROW, label };
} else if (ctx.CROSS_ARROW()) {
return { arrowType: ARROW_TYPE.CROSS_ARROW, label };
} else if (ctx.CIRCLE_ARROW_REVERSED()) {
return { arrowType: ARROW_TYPE.CIRCLE_ARROW_REVERSED, label };
} else if (ctx.CROSS_ARROW_REVERSED()) {
return { arrowType: ARROW_TYPE.CROSS_ARROW_REVERSED, 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;
}
/**
* Visit directionStatement rule
* Grammar: directionStatement : 'direction' direction NEWLINE* ;
*/
visitDirectionStatementImpl(ctx: DirectionStatementContext): void {
const directionCtx = ctx.direction?.();
if (directionCtx) {
this.direction = this.visitDirectionImpl(directionCtx);
}
}
/**
* Visit direction rule
* Grammar: direction : 'TB' | 'TD' | 'BT' | 'RL' | 'LR' ;
*/
private visitDirectionImpl(ctx: DirectionContext): string {
const text = ctx.getText();
// Normalize TD to TB (same as flowchart)
if (text === 'TD') {
return 'TB';
}
return text;
}
/**
* Visit classDefStatement rule
* Grammar: classDefStatement : 'classDef' IDENTIFIER stylesOpt NEWLINE* ;
*/
visitClassDefStatementImpl(ctx: ClassDefStatementContext): void {
const className = ctx.IDENTIFIER().getText();
const stylesOptCtx = ctx.stylesOpt();
// Get all style properties as an array of strings
const styles = this.visitStylesOptImpl(stylesOptCtx);
this.classDefs.set(className, {
id: className,
styles,
});
}
/**
* Visit stylesOpt rule
* Grammar: stylesOpt : style | stylesOpt COMMA style ;
* Returns an array of style strings like ['stroke:#f00', 'fill:#ff0']
*/
private visitStylesOptImpl(ctx: any): string[] {
const styles: string[] = [];
// Check if this is a recursive stylesOpt (stylesOpt COMMA style)
const stylesOptCtx = ctx.stylesOpt?.();
if (stylesOptCtx) {
styles.push(...this.visitStylesOptImpl(stylesOptCtx));
}
// Get the style context
const styleCtx = ctx.style();
if (styleCtx) {
const styleText = this.visitStyleImpl(styleCtx);
styles.push(styleText);
}
return styles;
}
/**
* Visit style rule
* Grammar: style : styleComponent | style styleComponent ;
* Returns a single style string like 'stroke:#f00'
*/
private visitStyleImpl(ctx: any): string {
// Get all text from the style context
return ctx.getText();
}
/**
* Visit classStatement rule
* Grammar: classStatement : 'class' nodeList IDENTIFIER NEWLINE* ;
*/
visitClassStatementImpl(ctx: ClassStatementContext): void {
const nodeIds = this.visitNodeListImpl(ctx.nodeList());
const className = ctx.IDENTIFIER().getText();
// Apply class to each node
nodeIds.forEach((nodeId) => {
this.applyClassToEntity(nodeId, className);
});
}
/**
* Visit styleStatement rule
* Grammar: styleStatement : 'style' IDENTIFIER stylesOpt NEWLINE* ;
*/
visitStyleStatementImpl(ctx: any): void {
const nodeId = ctx.IDENTIFIER().getText();
const stylesOptCtx = ctx.stylesOpt();
// Get all style properties as an array of strings
const styles = this.visitStylesOptImpl(stylesOptCtx);
// Apply styles directly to the entity
let entity = this.useCases.find((uc) => uc.id === nodeId);
if (!entity) {
entity = this.actors.find((a) => a.id === nodeId);
}
if (!entity) {
entity = this.systemBoundaries.find((sb) => sb.id === nodeId);
}
if (entity) {
// Initialize styles array if it doesn't exist
if (!entity.styles) {
entity.styles = [];
}
// Add the new styles
entity.styles.push(...styles);
}
}
/**
* Visit nodeList rule
* Grammar: nodeList : IDENTIFIER (',' IDENTIFIER)* ;
*/
private visitNodeListImpl(ctx: NodeListContext): string[] {
const identifiers = ctx.IDENTIFIER();
return identifiers.map((id) => id.getText());
}
/**
* Visit usecaseStatement rule
* Grammar: usecaseStatement : entityName NEWLINE* ;
*/
visitUsecaseStatementImpl(ctx: UsecaseStatementContext): void {
const entityName = this.visitEntityNameImpl(ctx.entityName());
// Create a standalone use case if it doesn't already exist
if (!this.useCases.some((uc) => uc.id === entityName)) {
this.useCases.push({
id: entityName,
name: entityName,
});
}
}
/**
* Get the parse result after visiting the diagram
*/
getParseResult(): UsecaseParseResult {
return {
actors: this.actors,
useCases: this.useCases,
systemBoundaries: this.systemBoundaries,
relationships: this.relationships,
classDefs: this.classDefs,
direction: this.direction,
};
}
}

View File

@@ -1,10 +1,9 @@
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 | UsecaseParseResult;
export type LangiumDiagramAST = Info | Packet | Pie | Architecture | GitGraph | Radar;
export type DiagramAST = Info | Packet | Pie | Architecture | GitGraph | Radar | Treemap;
export type LangiumDiagramAST = Info | Packet | Pie | Architecture | GitGraph | Radar | Treemap;
const parsers: Record<string, LangiumParser> = {};
const initializers = {
@@ -43,9 +42,6 @@ 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>;
@@ -55,13 +51,8 @@ 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
): Promise<T> {
@@ -70,18 +61,11 @@ export async function parse<T extends DiagramAST>(
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 extends LangiumDiagramAST ? T : never> =
parser.parse<T extends LangiumDiagramAST ? T : never>(text);
const result: ParseResult<T> = parser.parse<T>(text);
if (result.lexerErrors.length > 0 || result.parserErrors.length > 0) {
throw new MermaidParseError(result);
}

File diff suppressed because it is too large Load Diff

250
pnpm-lock.yaml generated
View File

@@ -235,6 +235,9 @@ importers:
'@types/d3':
specifier: ^7.4.3
version: 7.4.3
antlr4ng:
specifier: ^3.0.7
version: 3.0.16
cytoscape:
specifier: ^3.33.1
version: 3.33.1
@@ -533,67 +536,6 @@ importers:
specifier: ^7.3.0
version: 7.3.0
packages/mermaid/src/vitepress:
dependencies:
'@mdi/font':
specifier: ^7.4.47
version: 7.4.47
'@vueuse/core':
specifier: ^13.1.0
version: 13.9.0(vue@3.5.21(typescript@5.9.2))
font-awesome:
specifier: ^4.7.0
version: 4.7.0
jiti:
specifier: ^2.4.2
version: 2.5.1
mermaid:
specifier: workspace:^
version: link:../..
vue:
specifier: ^3.4.38
version: 3.5.21(typescript@5.9.2)
devDependencies:
'@iconify-json/carbon':
specifier: ^1.1.37
version: 1.2.13
'@unocss/reset':
specifier: ^66.0.0
version: 66.5.1
'@vite-pwa/vitepress':
specifier: ^1.0.0
version: 1.0.0(vite-plugin-pwa@1.0.3(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0))
'@vitejs/plugin-vue':
specifier: ^6.0.0
version: 6.0.1(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))
fast-glob:
specifier: ^3.3.3
version: 3.3.3
https-localhost:
specifier: ^4.7.1
version: 4.7.1
pathe:
specifier: ^2.0.3
version: 2.0.3
unocss:
specifier: ^66.4.2
version: 66.5.1(postcss@8.5.6)(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))
unplugin-vue-components:
specifier: ^28.4.0
version: 28.8.0(@babel/parser@7.28.4)(vue@3.5.21(typescript@5.9.2))
vite:
specifier: ^6.1.1
version: 6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1)
vite-plugin-pwa:
specifier: ^1.0.0
version: 1.0.3(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0)
vitepress:
specifier: 1.6.3
version: 1.6.3(@algolia/client-search@5.37.0)(@types/node@22.18.6)(axios@1.12.2)(postcss@8.5.6)(search-insights@2.17.3)(terser@5.44.0)(typescript@5.9.2)
workbox-window:
specifier: ^7.3.0
version: 7.3.0
packages/parser:
dependencies:
antlr4ng:
@@ -9519,46 +9461,6 @@ packages:
terser:
optional: true
vite@6.3.6:
resolution: {integrity: sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
peerDependencies:
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
jiti: '>=1.21.0'
less: '*'
lightningcss: ^1.21.0
sass: '*'
sass-embedded: '*'
stylus: '*'
sugarss: '*'
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
'@types/node':
optional: true
jiti:
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
sass-embedded:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
tsx:
optional: true
yaml:
optional: true
vite@7.1.5:
resolution: {integrity: sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -9607,18 +9509,6 @@ packages:
vitepress: ^1.0.0-rc.35
vue: '3'
vitepress@1.6.3:
resolution: {integrity: sha512-fCkfdOk8yRZT8GD9BFqusW3+GggWYZ/rYncOfmgcDtP3ualNHCAg+Robxp2/6xfH1WwPHtGpPwv7mbA3qomtBw==}
hasBin: true
peerDependencies:
markdown-it-mathjax3: ^4
postcss: ^8
peerDependenciesMeta:
markdown-it-mathjax3:
optional: true
postcss:
optional: true
vitepress@1.6.4:
resolution: {integrity: sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==}
hasBin: true
@@ -13374,14 +13264,6 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
'@unocss/astro@66.5.1(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))':
dependencies:
'@unocss/core': 66.5.1
'@unocss/reset': 66.5.1
'@unocss/vite': 66.5.1(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))
optionalDependencies:
vite: 6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1)
'@unocss/astro@66.5.1(vite@7.1.5(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))':
dependencies:
'@unocss/core': 66.5.1
@@ -13518,19 +13400,6 @@ snapshots:
dependencies:
'@unocss/core': 66.5.1
'@unocss/vite@66.5.1(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))':
dependencies:
'@jridgewell/remapping': 2.3.5
'@unocss/config': 66.5.1
'@unocss/core': 66.5.1
'@unocss/inspector': 66.5.1
chokidar: 3.6.0
magic-string: 0.30.19
pathe: 2.0.3
tinyglobby: 0.2.15
unplugin-utils: 0.3.0
vite: 6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1)
'@unocss/vite@66.5.1(vite@7.1.5(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))':
dependencies:
'@jridgewell/remapping': 2.3.5
@@ -13603,10 +13472,6 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true
'@vite-pwa/vitepress@1.0.0(vite-plugin-pwa@1.0.3(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0))':
dependencies:
vite-plugin-pwa: 1.0.3(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0)
'@vite-pwa/vitepress@1.0.0(vite-plugin-pwa@1.0.3(vite@7.1.5(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0))':
dependencies:
vite-plugin-pwa: 1.0.3(vite@7.1.5(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0)
@@ -13621,12 +13486,6 @@ snapshots:
vite: 5.4.20(@types/node@22.18.6)(terser@5.44.0)
vue: 3.5.21(typescript@5.9.2)
'@vitejs/plugin-vue@6.0.1(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))':
dependencies:
'@rolldown/pluginutils': 1.0.0-beta.29
vite: 6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1)
vue: 3.5.21(typescript@5.9.2)
'@vitejs/plugin-vue@6.0.1(vite@7.1.5(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))':
dependencies:
'@rolldown/pluginutils': 1.0.0-beta.29
@@ -20608,33 +20467,6 @@ snapshots:
universalify@2.0.1: {}
unocss@66.5.1(postcss@8.5.6)(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1)):
dependencies:
'@unocss/astro': 66.5.1(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))
'@unocss/cli': 66.5.1
'@unocss/core': 66.5.1
'@unocss/postcss': 66.5.1(postcss@8.5.6)
'@unocss/preset-attributify': 66.5.1
'@unocss/preset-icons': 66.5.1
'@unocss/preset-mini': 66.5.1
'@unocss/preset-tagify': 66.5.1
'@unocss/preset-typography': 66.5.1
'@unocss/preset-uno': 66.5.1
'@unocss/preset-web-fonts': 66.5.1
'@unocss/preset-wind': 66.5.1
'@unocss/preset-wind3': 66.5.1
'@unocss/preset-wind4': 66.5.1
'@unocss/transformer-attributify-jsx': 66.5.1
'@unocss/transformer-compile-class': 66.5.1
'@unocss/transformer-directives': 66.5.1
'@unocss/transformer-variant-group': 66.5.1
'@unocss/vite': 66.5.1(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))
optionalDependencies:
vite: 6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1)
transitivePeerDependencies:
- postcss
- supports-color
unocss@66.5.1(postcss@8.5.6)(vite@7.1.5(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1)):
dependencies:
'@unocss/astro': 66.5.1(vite@7.1.5(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))
@@ -20806,17 +20638,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
vite-plugin-pwa@1.0.3(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0):
dependencies:
debug: 4.4.3(supports-color@8.1.1)
pretty-bytes: 6.1.1
tinyglobby: 0.2.15
vite: 6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1)
workbox-build: 7.3.0(@types/babel__core@7.20.5)
workbox-window: 7.3.0
transitivePeerDependencies:
- supports-color
vite-plugin-pwa@1.0.3(vite@7.1.5(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0):
dependencies:
debug: 4.4.3(supports-color@8.1.1)
@@ -20838,22 +20659,6 @@ snapshots:
fsevents: 2.3.3
terser: 5.44.0
vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1):
dependencies:
esbuild: 0.25.10
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
rollup: 4.50.2
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 22.18.6
fsevents: 2.3.3
jiti: 2.5.1
terser: 5.44.0
tsx: 4.20.5
yaml: 2.8.1
vite@7.1.5(@types/node@22.18.6)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1):
dependencies:
esbuild: 0.25.10
@@ -20880,55 +20685,6 @@ snapshots:
vitepress: 1.6.4(@algolia/client-search@5.37.0)(@types/node@22.18.6)(axios@1.12.2)(postcss@8.5.6)(search-insights@2.17.3)(terser@5.44.0)(typescript@5.7.3)
vue: 3.5.21(typescript@5.7.3)
vitepress@1.6.3(@algolia/client-search@5.37.0)(@types/node@22.18.6)(axios@1.12.2)(postcss@8.5.6)(search-insights@2.17.3)(terser@5.44.0)(typescript@5.9.2):
dependencies:
'@docsearch/css': 3.8.2
'@docsearch/js': 3.8.2(@algolia/client-search@5.37.0)(search-insights@2.17.3)
'@iconify-json/simple-icons': 1.2.52
'@shikijs/core': 2.5.0
'@shikijs/transformers': 2.5.0
'@shikijs/types': 2.5.0
'@types/markdown-it': 14.1.2
'@vitejs/plugin-vue': 5.2.4(vite@5.4.20(@types/node@22.18.6)(terser@5.44.0))(vue@3.5.21(typescript@5.9.2))
'@vue/devtools-api': 7.7.7
'@vue/shared': 3.5.21
'@vueuse/core': 12.8.2(typescript@5.9.2)
'@vueuse/integrations': 12.8.2(axios@1.12.2)(focus-trap@7.6.5)(typescript@5.9.2)
focus-trap: 7.6.5
mark.js: 8.11.1
minisearch: 7.1.2
shiki: 2.5.0
vite: 5.4.20(@types/node@22.18.6)(terser@5.44.0)
vue: 3.5.21(typescript@5.9.2)
optionalDependencies:
postcss: 8.5.6
transitivePeerDependencies:
- '@algolia/client-search'
- '@types/node'
- '@types/react'
- async-validator
- axios
- change-case
- drauu
- fuse.js
- idb-keyval
- jwt-decode
- less
- lightningcss
- nprogress
- qrcode
- react
- react-dom
- sass
- sass-embedded
- search-insights
- sortablejs
- stylus
- sugarss
- terser
- typescript
- universal-cookie
vitepress@1.6.4(@algolia/client-search@5.37.0)(@types/node@22.18.6)(axios@1.12.2)(postcss@8.5.6)(search-insights@2.17.3)(terser@5.44.0)(typescript@5.7.3):
dependencies:
'@docsearch/css': 3.8.2

230
scripts/antlr-generate.mts Normal file
View File

@@ -0,0 +1,230 @@
#!/usr/bin/env tsx
/* eslint-disable no-console */
/* cspell:disable */
import { execSync } from 'child_process';
import { existsSync, mkdirSync, readdirSync, statSync } from 'fs';
import { join, dirname, basename } from 'path';
/**
* Generic ANTLR generator script that finds all .g4 files and generates parsers
* Automatically creates generated folders and runs antlr4ng for each diagram type
*/
interface GrammarInfo {
lexerFile: string;
parserFile: string;
outputDir: string;
diagramType: string;
}
/**
* Recursively find all .g4 files in a directory
*/
function findG4Files(dir: string): string[] {
const files: string[] = [];
if (!existsSync(dir)) {
return files;
}
const entries = readdirSync(dir);
for (const entry of entries) {
const fullPath = join(dir, entry);
const stat = statSync(fullPath);
if (stat.isDirectory()) {
files.push(...findG4Files(fullPath));
} else if (entry.endsWith('.g4')) {
files.push(fullPath);
}
}
return files;
}
/**
* Find all ANTLR grammar files in the diagrams directory
*/
function findGrammarFiles(): GrammarInfo[] {
const grammarFiles: GrammarInfo[] = [];
// Determine the correct path based on current working directory
const cwd = process.cwd();
let diagramsPath: string;
if (cwd.endsWith('/packages/mermaid')) {
// Running from mermaid package directory
diagramsPath = 'src/diagrams';
} else {
// Running from project root
diagramsPath = 'packages/mermaid/src/diagrams';
}
// Find all .g4 files
const g4Files = findG4Files(diagramsPath);
// Group by directory (each diagram should have a Lexer and Parser pair)
const grammarDirs = new Map<string, string[]>();
for (const file of g4Files) {
const dir = dirname(file);
if (!grammarDirs.has(dir)) {
grammarDirs.set(dir, []);
}
grammarDirs.get(dir)!.push(file);
}
// Process each directory
for (const [dir, files] of grammarDirs) {
const lexerFile = files.find((f) => f.includes('Lexer.g4'));
const parserFile = files.find((f) => f.includes('Parser.g4'));
if (lexerFile && parserFile) {
// Extract diagram type from path
const pathParts = dir.split('/');
const diagramIndex = pathParts.indexOf('diagrams');
const diagramType = diagramIndex >= 0 ? pathParts[diagramIndex + 1] : 'unknown';
grammarFiles.push({
lexerFile,
parserFile,
outputDir: join(dir, 'generated'),
diagramType,
});
} else {
console.warn(`⚠️ Incomplete grammar pair in ${dir}:`);
console.warn(` Lexer: ${lexerFile ?? 'MISSING'}`);
console.warn(` Parser: ${parserFile ?? 'MISSING'}`);
}
}
return grammarFiles;
}
/**
* Clean the generated directory
*/
function cleanGeneratedDir(outputDir: string): void {
try {
execSync(`rimraf "${outputDir}"`, { stdio: 'inherit' });
console.log(`🧹 Cleaned: ${outputDir}`);
} catch (error) {
console.warn(`⚠️ Failed to clean ${outputDir}:`, error);
}
}
/**
* Create the generated directory if it doesn't exist
*/
function ensureGeneratedDir(outputDir: string): void {
if (!existsSync(outputDir)) {
mkdirSync(outputDir, { recursive: true });
console.log(`📁 Created: ${outputDir}`);
}
}
/**
* Generate ANTLR files for a grammar pair
*/
function generateAntlrFiles(grammar: GrammarInfo): void {
const { lexerFile, parserFile, outputDir, diagramType } = grammar;
console.log(`\n🎯 Generating ANTLR files for ${diagramType} diagram...`);
console.log(` Lexer: ${basename(lexerFile)}`);
console.log(` Parser: ${basename(parserFile)}`);
console.log(` Output: ${outputDir}`);
try {
// Clean and create output directory
cleanGeneratedDir(outputDir);
ensureGeneratedDir(outputDir);
// Determine common header lib path for imported grammars
const cwd = process.cwd();
const commonLibPath = cwd.endsWith('/packages/mermaid')
? 'src/diagrams/common/parser/antlr'
: 'packages/mermaid/src/diagrams/common/parser/antlr';
// TODO: Use JS api instead of CLI
// Generate ANTLR files
const command = [
'pnpm',
'dlx',
'antlr-ng',
'-Dlanguage=TypeScript',
'-l',
'-v',
`--lib "${commonLibPath}"`,
`-o "${outputDir}"`,
`"${lexerFile}"`,
`"${parserFile}"`,
].join(' ');
console.log(` Command: ${command}`);
execSync(command, { stdio: 'inherit' });
console.log(`✅ Successfully generated ANTLR files for ${diagramType}`);
} catch (error) {
console.error(`❌ Failed to generate ANTLR files for ${diagramType}:`, error);
throw error;
}
}
/**
* Main function
*/
export function generateAntlr(): void {
console.log('🚀 ANTLR Generator - Finding and generating all grammar files...\n');
try {
// Find all grammar files
const grammarFiles = findGrammarFiles();
if (grammarFiles.length === 0) {
console.log(' No ANTLR grammar files found.');
return;
}
console.log(`📋 Found ${grammarFiles.length} diagram(s) with ANTLR grammars:`);
for (const grammar of grammarFiles) {
console.log(`${grammar.diagramType}`);
}
// Generate files for each grammar
let successCount = 0;
let failureCount = 0;
for (const grammar of grammarFiles) {
try {
generateAntlrFiles(grammar);
successCount++;
} catch (error) {
failureCount++;
console.error(`Failed to process ${grammar.diagramType}:`, error);
}
}
// Summary
console.log('\n📊 Generation Summary:');
console.log(` ✅ Successful: ${successCount}`);
console.log(` ❌ Failed: ${failureCount}`);
console.log(` 📁 Total: ${grammarFiles.length}`);
if (failureCount > 0) {
console.error('\n❌ Some ANTLR generations failed. Check the errors above.');
process.exit(1);
} else {
console.log('\n🎉 All ANTLR files generated successfully!');
}
} catch (error) {
console.error('❌ Fatal error during ANTLR generation:', error);
process.exit(1);
}
}
// Run the script
if (import.meta.url === `file://${process.argv[1]}`) {
generateAntlr();
}

119
scripts/antlr-watch.mts Executable file
View File

@@ -0,0 +1,119 @@
#!/usr/bin/env tsx
/* eslint-disable no-console */
import chokidar from 'chokidar';
import { execSync } from 'child_process';
/**
* ANTLR Watch Script
*
* This script generates ANTLR files and then watches for changes to .g4 grammar files,
* automatically regenerating the corresponding parsers when changes are detected.
*
* Features:
* - Initial generation of all ANTLR files
* - Watch .g4 files for changes
* - Debounced regeneration to avoid multiple builds
* - Clear logging and progress reporting
* - Graceful shutdown handling
*/
let isGenerating = false;
let timeoutID: NodeJS.Timeout | undefined = undefined;
/**
* Generate ANTLR parser files from grammar files
*/
function generateAntlr(): void {
if (isGenerating) {
console.log('⏳ ANTLR generation already in progress, skipping...');
return;
}
try {
isGenerating = true;
console.log('🎯 ANTLR: Generating parser files...');
execSync('tsx scripts/antlr-generate.mts', { stdio: 'inherit' });
console.log('✅ ANTLR: Parser files generated successfully\n');
} catch (error) {
console.error('❌ ANTLR: Failed to generate parser files:', error);
} finally {
isGenerating = false;
}
}
/**
* Handle file change events with debouncing
*/
function handleFileChange(path: string): void {
if (timeoutID !== undefined) {
clearTimeout(timeoutID);
}
console.log(`🎯 Grammar file changed: ${path}`);
// Debounce file changes to avoid multiple regenerations
timeoutID = setTimeout(() => {
console.log('🔄 Regenerating ANTLR files...\n');
generateAntlr();
timeoutID = undefined;
}, 500); // 500ms debounce
}
/**
* Setup graceful shutdown
*/
function setupGracefulShutdown(): void {
const shutdown = () => {
console.log('\n🛑 Shutting down ANTLR watch...');
if (timeoutID) {
clearTimeout(timeoutID);
}
process.exit(0);
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
}
/**
* Main function
*/
function main(): void {
console.log('🚀 ANTLR Watch - Generate and watch grammar files for changes\n');
// Setup graceful shutdown
setupGracefulShutdown();
// Initial generation
generateAntlr();
// Setup file watcher
console.log('👀 Watching for .g4 file changes...');
console.log('📁 Pattern: **/src/**/parser/antlr/*.g4');
console.log('🛑 Press Ctrl+C to stop watching\n');
const watcher = chokidar.watch('**/src/**/parser/antlr/*.g4', {
ignoreInitial: true,
ignored: [/node_modules/, /dist/, /docs/, /coverage/],
persistent: true,
});
watcher
.on('change', handleFileChange)
.on('add', handleFileChange)
.on('error', (error) => {
console.error('❌ Watcher error:', error);
})
.on('ready', () => {
console.log('✅ Watcher ready - monitoring grammar files for changes...\n');
});
// Keep the process alive
process.stdin.resume();
}
// Run the script
if (import.meta.url === `file://${process.argv[1]}`) {
main();
}