Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion .esbuild/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
Expand All @@ -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/],
})
Expand All @@ -103,6 +126,9 @@ async function createServer() {
if (path.endsWith('.langium')) {
await generateLangium();
}
if (path.endsWith('.g4')) {
generateAntlr();
}
handleFileChange();
});

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions packages/mermaid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
}
}
65 changes: 65 additions & 0 deletions packages/mermaid/src/diagrams/usecase/parser/antlr/UsecaseLexer.g4
Original file line number Diff line number Diff line change
@@ -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;

Loading
Loading