mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-12-12 23:44:14 +01:00
Compare commits
1 Commits
feat/useca
...
feat/useca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfed700a58 |
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
;
|
||||
@@ -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, '_');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from './module.js';
|
||||
export * from './types.js';
|
||||
export * from './parser.js';
|
||||
export * from './visitor.js';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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
250
pnpm-lock.yaml
generated
@@ -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
230
scripts/antlr-generate.mts
Normal 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
119
scripts/antlr-watch.mts
Executable 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();
|
||||
}
|
||||
Reference in New Issue
Block a user