migrated class diagram antlr parser to new structure

This commit is contained in:
Ashish Jain
2025-09-19 11:38:39 +02:00
parent ac8d92a5a4
commit 38428114ee
4 changed files with 1216 additions and 584 deletions

View File

@@ -0,0 +1,266 @@
import type { ParseTreeListener } from 'antlr4ng';
import { ClassParserListener } from './generated/ClassParserListener.js';
import { ClassParserCore, type ClassDbLike } from './ClassParserCore.js';
import type {
ClassIdentifierContext,
ClassMembersContext,
ClassStatementContext,
NamespaceIdentifierContext,
NamespaceStatementContext,
RelationStatementContext,
NoteStatementContext,
AnnotationStatementContext,
MemberStatementContext,
ClassDefStatementContext,
StyleStatementContext,
CssClassStatementContext,
DirectionStatementContext,
AccTitleStatementContext,
AccDescrStatementContext,
AccDescrMultilineStatementContext,
CallbackStatementContext,
ClickStatementContext,
LinkStatementContext,
CallStatementContext,
} from './generated/ClassParser.js';
/**
* Class diagram listener implementation using the listener pattern
* Extends ClassParserCore for common parsing logic
*/
export class ClassListener extends ClassParserCore implements ParseTreeListener {
constructor(db: ClassDbLike) {
super(db);
}
// Standard ParseTreeListener methods
enterEveryRule = (_ctx: any) => {
// Optional: Add debug logging for rule entry
};
exitEveryRule = (_ctx: any) => {
// Optional: Add debug logging for rule exit
};
visitTerminal = (_node: any) => {
// Optional: Handle terminal nodes
};
visitErrorNode = (_node: any) => {
console.log('❌ ClassListener: Error node encountered');
// Throw error to match Jison parser behavior for syntax errors
throw new Error('Syntax error in class diagram');
};
// Listener method implementations that delegate to the core processing methods
enterNamespaceStatement = (_ctx: NamespaceStatementContext): void => {
console.log('🔧 ClassListener: Entering namespace statement');
try {
this.processNamespaceStatementEnter();
} catch (error) {
console.error('❌ ClassListener: Error entering namespace statement:', error);
throw error;
}
};
exitNamespaceIdentifier = (ctx: NamespaceIdentifierContext): void => {
console.log('🔧 ClassListener: Exiting namespace identifier');
try {
this.processNamespaceIdentifier(ctx);
} catch (error) {
console.error('❌ ClassListener: Error processing namespace identifier:', error);
throw error;
}
};
exitNamespaceStatement = (_ctx: NamespaceStatementContext): void => {
console.log('🔧 ClassListener: Exiting namespace statement');
try {
this.processNamespaceStatementExit();
} catch (error) {
console.error('❌ ClassListener: Error exiting namespace statement:', error);
throw error;
}
};
exitClassIdentifier = (ctx: ClassIdentifierContext): void => {
console.log('🔧 ClassListener: Exiting class identifier');
try {
this.processClassIdentifier(ctx);
} catch (error) {
console.error('❌ ClassListener: Error processing class identifier:', error);
throw error;
}
};
exitClassMembers = (ctx: ClassMembersContext): void => {
console.log('🔧 ClassListener: Exiting class members');
try {
this.processClassMembers(ctx);
} catch (error) {
console.error('❌ ClassListener: Error processing class members:', error);
throw error;
}
};
exitClassStatement = (ctx: ClassStatementContext): void => {
console.log('🔧 ClassListener: Exiting class statement');
try {
this.processClassStatement(ctx);
} catch (error) {
console.error('❌ ClassListener: Error processing class statement:', error);
throw error;
}
};
exitRelationStatement = (ctx: RelationStatementContext): void => {
console.log('🔧 ClassListener: Exiting relation statement');
try {
this.processRelationStatement(ctx);
} catch (error) {
console.error('❌ ClassListener: Error processing relation statement:', error);
throw error;
}
};
exitNoteStatement = (ctx: NoteStatementContext): void => {
console.log('🔧 ClassListener: Exiting note statement');
try {
this.processNoteStatement(ctx);
} catch (error) {
console.error('❌ ClassListener: Error processing note statement:', error);
throw error;
}
};
exitAnnotationStatement = (ctx: AnnotationStatementContext): void => {
console.log('🔧 ClassListener: Exiting annotation statement');
try {
this.processAnnotationStatement(ctx);
} catch (error) {
console.error('❌ ClassListener: Error processing annotation statement:', error);
throw error;
}
};
exitMemberStatement = (ctx: MemberStatementContext): void => {
console.log('🔧 ClassListener: Exiting member statement');
try {
this.processMemberStatement(ctx);
} catch (error) {
console.error('❌ ClassListener: Error processing member statement:', error);
throw error;
}
};
exitClassDefStatement = (ctx: ClassDefStatementContext): void => {
console.log('🔧 ClassListener: Exiting classDef statement');
try {
this.processClassDefStatement(ctx);
} catch (error) {
console.error('❌ ClassListener: Error processing classDef statement:', error);
throw error;
}
};
exitStyleStatement = (ctx: StyleStatementContext): void => {
console.log('🔧 ClassListener: Exiting style statement');
try {
this.processStyleStatement(ctx);
} catch (error) {
console.error('❌ ClassListener: Error processing style statement:', error);
throw error;
}
};
exitCssClassStatement = (ctx: CssClassStatementContext): void => {
console.log('🔧 ClassListener: Exiting cssClass statement');
try {
this.processCssClassStatement(ctx);
} catch (error) {
console.error('❌ ClassListener: Error processing cssClass statement:', error);
throw error;
}
};
exitDirectionStatement = (ctx: DirectionStatementContext): void => {
console.log('🔧 ClassListener: Exiting direction statement');
try {
this.processDirectionStatement(ctx);
} catch (error) {
console.error('❌ ClassListener: Error processing direction statement:', error);
throw error;
}
};
exitAccTitleStatement = (ctx: AccTitleStatementContext): void => {
console.log('🔧 ClassListener: Exiting accTitle statement');
try {
this.processAccTitleStatement(ctx);
} catch (error) {
console.error('❌ ClassListener: Error processing accTitle statement:', error);
throw error;
}
};
exitAccDescrStatement = (ctx: AccDescrStatementContext): void => {
console.log('🔧 ClassListener: Exiting accDescr statement');
try {
this.processAccDescrStatement(ctx);
} catch (error) {
console.error('❌ ClassListener: Error processing accDescr statement:', error);
throw error;
}
};
exitAccDescrMultilineStatement = (ctx: AccDescrMultilineStatementContext): void => {
console.log('🔧 ClassListener: Exiting accDescr multiline statement');
try {
this.processAccDescrMultilineStatement(ctx);
} catch (error) {
console.error('❌ ClassListener: Error processing accDescr multiline statement:', error);
throw error;
}
};
exitCallbackStatement = (ctx: CallbackStatementContext): void => {
console.log('🔧 ClassListener: Exiting callback statement');
try {
this.processCallbackStatement(ctx);
} catch (error) {
console.error('❌ ClassListener: Error processing callback statement:', error);
throw error;
}
};
exitClickStatement = (ctx: ClickStatementContext): void => {
console.log('🔧 ClassListener: Exiting click statement');
try {
this.processClickStatement(ctx);
} catch (error) {
console.error('❌ ClassListener: Error processing click statement:', error);
throw error;
}
};
exitLinkStatement = (ctx: LinkStatementContext): void => {
console.log('🔧 ClassListener: Exiting link statement');
try {
this.processLinkStatement(ctx);
} catch (error) {
console.error('❌ ClassListener: Error processing link statement:', error);
throw error;
}
};
exitCallStatement = (ctx: CallStatementContext): void => {
console.log('🔧 ClassListener: Exiting call statement');
try {
this.processCallStatement(ctx);
} catch (error) {
console.error('❌ ClassListener: Error processing call statement:', error);
throw error;
}
};
}

View File

@@ -0,0 +1,610 @@
import type {
ClassIdentifierContext,
ClassMembersContext,
ClassNameContext,
ClassNameSegmentContext,
ClassStatementContext,
NamespaceIdentifierContext,
RelationStatementContext,
NoteStatementContext,
AnnotationStatementContext,
MemberStatementContext,
ClassDefStatementContext,
StyleStatementContext,
CssClassStatementContext,
DirectionStatementContext,
AccTitleStatementContext,
AccDescrStatementContext,
AccDescrMultilineStatementContext,
CallbackStatementContext,
ClickStatementContext,
LinkStatementContext,
CallStatementContext,
CssClassRefContext,
StringLiteralContext,
} from './generated/ClassParser.js';
type ClassDbLike = Record<string, any>;
const stripQuotes = (value: string): string => {
const trimmed = value.trim();
if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
try {
return JSON.parse(trimmed.replace(/\r?\n/g, '\\n')) as string;
} catch {
return trimmed.slice(1, -1).replace(/\\"/g, '"');
}
}
return trimmed;
};
const stripBackticks = (value: string): string => {
const trimmed = value.trim();
if (trimmed.length >= 2 && trimmed.startsWith('`') && trimmed.endsWith('`')) {
return trimmed.slice(1, -1);
}
return trimmed;
};
const splitCommaSeparated = (text: string): string[] =>
text
.split(',')
.map((part) => part.trim())
.filter((part) => part.length > 0);
const getStringFromLiteral = (ctx: StringLiteralContext | undefined | null): string | undefined => {
if (!ctx) {
return undefined;
}
return stripQuotes(ctx.getText());
};
const getClassNameText = (ctx: ClassNameContext): string => {
const segments = ctx.classNameSegment();
const parts: string[] = [];
for (const segment of segments) {
parts.push(getClassNameSegmentText(segment));
}
return parts.join('.');
};
const getClassNameSegmentText = (ctx: ClassNameSegmentContext): string => {
if (ctx.BACKTICK_ID()) {
return stripBackticks(ctx.BACKTICK_ID()!.getText());
}
if (ctx.EDGE_STATE()) {
return ctx.EDGE_STATE()!.getText();
}
return ctx.getText();
};
const parseRelationArrow = (arrow: string, db: ClassDbLike) => {
const relation = {
type1: 'none',
type2: 'none',
lineType: db.lineType?.LINE ?? 0,
};
const trimmed = arrow.trim();
if (trimmed.includes('..')) {
relation.lineType = db.lineType?.DOTTED_LINE ?? relation.lineType;
}
const leftHeads: [string, keyof typeof db.relationType][] = [
['<|', 'EXTENSION'],
['()', 'LOLLIPOP'],
['o', 'AGGREGATION'],
['*', 'COMPOSITION'],
['<', 'DEPENDENCY'],
];
for (const [prefix, key] of leftHeads) {
if (trimmed.startsWith(prefix)) {
relation.type1 = db.relationType?.[key] ?? relation.type1;
break;
}
}
const rightHeads: [string, keyof typeof db.relationType][] = [
['|>', 'EXTENSION'],
['()', 'LOLLIPOP'],
['o', 'AGGREGATION'],
['*', 'COMPOSITION'],
['>', 'DEPENDENCY'],
];
for (const [suffix, key] of rightHeads) {
if (trimmed.endsWith(suffix)) {
relation.type2 = db.relationType?.[key] ?? relation.type2;
break;
}
}
return relation;
};
const parseStyleLine = (db: ClassDbLike, line: string) => {
const trimmed = line.trim();
const body = trimmed.slice('style'.length).trim();
if (!body) {
return;
}
const match = /^(\S+)(\s+.+)?$/.exec(body);
if (!match) {
return;
}
const classId = match[1];
const styleBody = match[2]?.trim() ?? '';
if (!styleBody) {
return;
}
const styles = splitCommaSeparated(styleBody);
if (styles.length) {
db.setCssStyle?.(classId, styles);
}
};
const parseClassDefLine = (db: ClassDbLike, line: string) => {
const trimmed = line.trim();
const body = trimmed.slice('classDef'.length).trim();
if (!body) {
return;
}
const match = /^(\S+)(\s+.+)?$/.exec(body);
if (!match) {
return;
}
const idPart = match[1];
const stylePart = match[2]?.trim() ?? '';
const ids = splitCommaSeparated(idPart);
const styles = stylePart ? splitCommaSeparated(stylePart) : [];
db.defineClass?.(ids, styles);
};
const parseCssClassLine = (db: ClassDbLike, line: string) => {
const trimmed = line.trim();
const body = trimmed.slice('cssClass'.length).trim();
if (!body) {
return;
}
const match = /^("[^"]*"|\S+)\s+(\S+)/.exec(body);
if (!match) {
return;
}
const idsRaw = stripQuotes(match[1]);
const className = match[2];
db.setCssClass?.(idsRaw, className);
};
const parseCallbackLine = (db: ClassDbLike, line: string) => {
const trimmed = line.trim();
const match = /^callback\s+(\S+)\s+("[^"]*")(?:\s+("[^"]*"))?\s*$/.exec(trimmed);
if (!match) {
return;
}
const target = match[1];
const fn = stripQuotes(match[2]);
const tooltip = match[3] ? stripQuotes(match[3]) : undefined;
db.setClickEvent?.(target, fn);
if (tooltip) {
db.setTooltip?.(target, tooltip);
}
};
const parseClickLine = (db: ClassDbLike, line: string) => {
const trimmed = line.trim();
const callMatch = /^click\s+(\S+)\s+call\s+([^(]+)\(([^)]*)\)(?:\s+("[^"]*"))?\s*$/.exec(trimmed);
if (callMatch) {
const target = callMatch[1];
const fnName = callMatch[2].trim();
const args = callMatch[3].trim();
const tooltip = callMatch[4] ? stripQuotes(callMatch[4]) : undefined;
if (args.length > 0) {
db.setClickEvent?.(target, fnName, args);
} else {
db.setClickEvent?.(target, fnName);
}
if (tooltip) {
db.setTooltip?.(target, tooltip);
}
return target;
}
const hrefMatch = /^click\s+(\S+)\s+href\s+("[^"]*")(?:\s+("[^"]*"))?(?:\s+(\S+))?\s*$/.exec(
trimmed
);
if (hrefMatch) {
const target = hrefMatch[1];
const url = stripQuotes(hrefMatch[2]);
const tooltip = hrefMatch[3] ? stripQuotes(hrefMatch[3]) : undefined;
const targetWindow = hrefMatch[4];
if (targetWindow) {
db.setLink?.(target, url, targetWindow);
} else {
db.setLink?.(target, url);
}
if (tooltip) {
db.setTooltip?.(target, tooltip);
}
return target;
}
const genericMatch = /^click\s+(\S+)\s+("[^"]*")(?:\s+("[^"]*"))?\s*$/.exec(trimmed);
if (genericMatch) {
const target = genericMatch[1];
const link = stripQuotes(genericMatch[2]);
const tooltip = genericMatch[3] ? stripQuotes(genericMatch[3]) : undefined;
db.setLink?.(target, link);
if (tooltip) {
db.setTooltip?.(target, tooltip);
}
return target;
}
return undefined;
};
const parseLinkLine = (db: ClassDbLike, line: string) => {
const trimmed = line.trim();
const match = /^link\s+(\S+)\s+("[^"]*")(?:\s+("[^"]*"))?(?:\s+(\S+))?\s*$/.exec(trimmed);
if (!match) {
return;
}
const target = match[1];
const href = stripQuotes(match[2]);
const tooltip = match[3] ? stripQuotes(match[3]) : undefined;
const targetWindow = match[4];
if (targetWindow) {
db.setLink?.(target, href, targetWindow);
} else {
db.setLink?.(target, href);
}
if (tooltip) {
db.setTooltip?.(target, tooltip);
}
};
const parseCallLine = (db: ClassDbLike, lastTarget: string | undefined, line: string) => {
if (!lastTarget) {
return;
}
const trimmed = line.trim();
const match = /^call\s+([^(]+)\(([^)]*)\)\s*("[^"]*")?\s*$/.exec(trimmed);
if (!match) {
return;
}
const fnName = match[1].trim();
const args = match[2].trim();
const tooltip = match[3] ? stripQuotes(match[3]) : undefined;
if (args.length > 0) {
db.setClickEvent?.(lastTarget, fnName, args);
} else {
db.setClickEvent?.(lastTarget, fnName);
}
if (tooltip) {
db.setTooltip?.(lastTarget, tooltip);
}
};
interface NamespaceFrame {
name?: string;
classes: string[];
}
/**
* Base class containing common parsing logic for class diagrams
* Used by both Visitor and Listener pattern implementations
*/
export abstract class ClassParserCore {
protected readonly classNames = new WeakMap<ClassIdentifierContext, string>();
protected readonly memberLists = new WeakMap<ClassMembersContext, string[]>();
protected readonly namespaceStack: NamespaceFrame[] = [];
protected lastClickTarget?: string;
constructor(protected readonly db: ClassDbLike) {}
protected recordClassInCurrentNamespace(name: string) {
const current = this.namespaceStack[this.namespaceStack.length - 1];
if (current?.name) {
current.classes.push(name);
}
}
protected resolveCssClassRef(ctx: CssClassRefContext): string | undefined {
if (ctx.className()) {
return getClassNameText(ctx.className()!);
}
if (ctx.IDENTIFIER()) {
return ctx.IDENTIFIER()!.getText();
}
return undefined;
}
// Processing methods that can be called by both Visitor and Listener patterns
processNamespaceStatementEnter(): void {
this.namespaceStack.push({ classes: [] });
}
processNamespaceIdentifier(ctx: NamespaceIdentifierContext): void {
const frame = this.namespaceStack[this.namespaceStack.length - 1];
if (!frame) {
return;
}
const classNameCtx = ctx.namespaceName()?.className();
if (!classNameCtx) {
return;
}
const name = getClassNameText(classNameCtx);
frame.name = name;
this.db.addNamespace?.(name);
}
processNamespaceStatementExit(): void {
const frame = this.namespaceStack.pop();
if (!frame?.name) {
return;
}
if (frame.classes.length) {
this.db.addClassesToNamespace?.(frame.name, frame.classes);
}
}
processClassIdentifier(ctx: ClassIdentifierContext): void {
const id = getClassNameText(ctx.className());
this.classNames.set(ctx, id);
this.db.addClass?.(id);
this.recordClassInCurrentNamespace(id);
const labelCtx = ctx.classLabel?.();
if (labelCtx) {
const label = getStringFromLiteral(labelCtx.stringLiteral());
if (label !== undefined) {
this.db.setClassLabel?.(id, label);
}
}
}
processClassMembers(ctx: ClassMembersContext): void {
const members: string[] = [];
for (const memberCtx of ctx.classMember() ?? []) {
if (memberCtx.MEMBER()) {
members.push(memberCtx.MEMBER()!.getText());
} else if (memberCtx.EDGE_STATE()) {
members.push(memberCtx.EDGE_STATE()!.getText());
}
}
members.reverse();
this.memberLists.set(ctx, members);
}
processClassStatement(ctx: ClassStatementContext): void {
const identifierCtx = ctx.classIdentifier();
if (!identifierCtx) {
return;
}
const classId = this.classNames.get(identifierCtx);
if (!classId) {
return;
}
const tailCtx = ctx.classStatementTail?.();
const cssRefCtx = tailCtx?.cssClassRef?.();
if (cssRefCtx) {
const cssTarget = this.resolveCssClassRef(cssRefCtx);
if (cssTarget) {
this.db.setCssClass?.(classId, cssTarget);
}
}
const memberContexts: ClassMembersContext[] = [];
const cm1 = tailCtx?.classMembers();
if (cm1) {
memberContexts.push(cm1);
}
const cssTailCtx = tailCtx?.classStatementCssTail?.();
const cm2 = cssTailCtx?.classMembers();
if (cm2) {
memberContexts.push(cm2);
}
for (const membersCtx of memberContexts) {
const members = this.memberLists.get(membersCtx) ?? [];
if (members.length) {
this.db.addMembers?.(classId, members);
}
}
}
processRelationStatement(ctx: RelationStatementContext): void {
const classNames = ctx.className();
if (classNames.length < 2) {
return;
}
const id1 = getClassNameText(classNames[0]);
const id2 = getClassNameText(classNames[classNames.length - 1]);
const arrow = ctx.relation()?.getText() ?? '';
const relation = parseRelationArrow(arrow, this.db);
let relationTitle1 = 'none';
let relationTitle2 = 'none';
const stringLiterals = ctx.stringLiteral();
if (stringLiterals.length === 1 && ctx.children) {
const stringCtx = stringLiterals[0];
const children = ctx.children as unknown[];
const stringIndex = children.indexOf(stringCtx);
const relationCtx = ctx.relation();
const relationIndex = relationCtx ? children.indexOf(relationCtx) : -1;
if (relationIndex >= 0 && stringIndex >= 0 && stringIndex < relationIndex) {
relationTitle1 = getStringFromLiteral(stringCtx) ?? 'none';
} else {
relationTitle2 = getStringFromLiteral(stringCtx) ?? 'none';
}
} else if (stringLiterals.length >= 2) {
relationTitle1 = getStringFromLiteral(stringLiterals[0]) ?? 'none';
relationTitle2 = getStringFromLiteral(stringLiterals[1]) ?? 'none';
}
let title = 'none';
const labelCtx = ctx.relationLabel?.();
if (labelCtx?.LABEL()) {
title = this.db.cleanupLabel?.(labelCtx.LABEL().getText()) ?? 'none';
}
this.db.addRelation?.({
id1,
id2,
relation,
relationTitle1,
relationTitle2,
title,
});
}
processNoteStatement(ctx: NoteStatementContext): void {
const noteCtx = ctx.noteBody();
const literalText = noteCtx?.getText?.();
const text = literalText !== undefined ? stripQuotes(literalText) : undefined;
if (text === undefined) {
return;
}
if (ctx.NOTE_FOR()) {
const className = getClassNameText(ctx.className()!);
this.db.addNote?.(text, className);
} else {
this.db.addNote?.(text);
}
}
processAnnotationStatement(ctx: AnnotationStatementContext): void {
const className = getClassNameText(ctx.className());
const nameCtx = ctx.annotationName();
let annotation: string | undefined;
if (nameCtx.IDENTIFIER()) {
annotation = nameCtx.IDENTIFIER()!.getText();
} else {
annotation = getStringFromLiteral(nameCtx.stringLiteral());
}
if (annotation !== undefined) {
this.db.addAnnotation?.(className, annotation);
}
}
processMemberStatement(ctx: MemberStatementContext): void {
const className = getClassNameText(ctx.className());
const labelToken = ctx.LABEL();
if (!labelToken) {
return;
}
const cleaned = this.db.cleanupLabel?.(labelToken.getText()) ?? labelToken.getText();
this.db.addMember?.(className, cleaned);
}
processClassDefStatement(ctx: ClassDefStatementContext): void {
const token = ctx.CLASSDEF_LINE()?.getSymbol()?.text;
if (token) {
parseClassDefLine(this.db, token);
}
}
processStyleStatement(ctx: StyleStatementContext): void {
const token = ctx.STYLE_LINE()?.getSymbol()?.text;
if (token) {
parseStyleLine(this.db, token);
}
}
processCssClassStatement(ctx: CssClassStatementContext): void {
const token = ctx.CSSCLASS_LINE()?.getSymbol()?.text;
if (token) {
parseCssClassLine(this.db, token);
}
}
processDirectionStatement(ctx: DirectionStatementContext): void {
if (ctx.DIRECTION_TB()) {
this.db.setDirection?.('TB');
} else if (ctx.DIRECTION_BT()) {
this.db.setDirection?.('BT');
} else if (ctx.DIRECTION_LR()) {
this.db.setDirection?.('LR');
} else if (ctx.DIRECTION_RL()) {
this.db.setDirection?.('RL');
}
}
processAccTitleStatement(ctx: AccTitleStatementContext): void {
const value = ctx.ACC_TITLE_VALUE()?.getText();
if (value !== undefined) {
this.db.setAccTitle?.(value.trim());
}
}
processAccDescrStatement(ctx: AccDescrStatementContext): void {
const value = ctx.ACC_DESCR_VALUE()?.getText();
if (value !== undefined) {
this.db.setAccDescription?.(value.trim());
}
}
processAccDescrMultilineStatement(ctx: AccDescrMultilineStatementContext): void {
const value = ctx.ACC_DESCR_MULTILINE_VALUE()?.getText();
if (value !== undefined) {
this.db.setAccDescription?.(value.trim());
}
}
processCallbackStatement(ctx: CallbackStatementContext): void {
const token = ctx.CALLBACK_LINE()?.getSymbol()?.text;
if (token) {
parseCallbackLine(this.db, token);
}
}
processClickStatement(ctx: ClickStatementContext): void {
const token = ctx.CLICK_LINE()?.getSymbol()?.text;
if (!token) {
return;
}
const target = parseClickLine(this.db, token);
if (target) {
this.lastClickTarget = target;
}
}
processLinkStatement(ctx: LinkStatementContext): void {
const token = ctx.LINK_LINE()?.getSymbol()?.text;
if (token) {
parseLinkLine(this.db, token);
}
}
processCallStatement(ctx: CallStatementContext): void {
const token = ctx.CALL_LINE()?.getSymbol()?.text;
if (token) {
parseCallLine(this.db, this.lastClickTarget, token);
}
}
}
// Export utility functions for use by other modules
export {
stripQuotes,
stripBackticks,
splitCommaSeparated,
getStringFromLiteral,
getClassNameText,
getClassNameSegmentText,
parseRelationArrow,
parseStyleLine,
parseClassDefLine,
parseCssClassLine,
parseCallbackLine,
parseClickLine,
parseLinkLine,
parseCallLine,
type ClassDbLike,
type NamespaceFrame,
};

View File

@@ -0,0 +1,303 @@
import { ClassParserVisitor } from './generated/ClassParserVisitor.js';
import { ClassParserCore, type ClassDbLike } from './ClassParserCore.js';
import type {
ClassIdentifierContext,
ClassMembersContext,
ClassStatementContext,
NamespaceIdentifierContext,
NamespaceStatementContext,
RelationStatementContext,
NoteStatementContext,
AnnotationStatementContext,
MemberStatementContext,
ClassDefStatementContext,
StyleStatementContext,
CssClassStatementContext,
DirectionStatementContext,
AccTitleStatementContext,
AccDescrStatementContext,
AccDescrMultilineStatementContext,
CallbackStatementContext,
ClickStatementContext,
LinkStatementContext,
CallStatementContext,
} from './generated/ClassParser.js';
/**
* Class diagram visitor implementation using the visitor pattern
* Extends ClassParserCore for common parsing logic
*/
export class ClassVisitor extends ClassParserCore {
private visitor: ClassParserVisitor<any>;
constructor(db: ClassDbLike) {
super(db);
this.visitor = new ClassParserVisitor<any>();
// Override visitor methods to call our processing methods
this.visitor.visitNamespaceStatement = this.visitNamespaceStatement.bind(this);
this.visitor.visitNamespaceIdentifier = this.visitNamespaceIdentifier.bind(this);
this.visitor.visitClassIdentifier = this.visitClassIdentifier.bind(this);
this.visitor.visitClassMembers = this.visitClassMembers.bind(this);
this.visitor.visitClassStatement = this.visitClassStatement.bind(this);
this.visitor.visitRelationStatement = this.visitRelationStatement.bind(this);
this.visitor.visitNoteStatement = this.visitNoteStatement.bind(this);
this.visitor.visitAnnotationStatement = this.visitAnnotationStatement.bind(this);
this.visitor.visitMemberStatement = this.visitMemberStatement.bind(this);
this.visitor.visitClassDefStatement = this.visitClassDefStatement.bind(this);
this.visitor.visitStyleStatement = this.visitStyleStatement.bind(this);
this.visitor.visitCssClassStatement = this.visitCssClassStatement.bind(this);
this.visitor.visitDirectionStatement = this.visitDirectionStatement.bind(this);
this.visitor.visitAccTitleStatement = this.visitAccTitleStatement.bind(this);
this.visitor.visitAccDescrStatement = this.visitAccDescrStatement.bind(this);
this.visitor.visitAccDescrMultilineStatement = this.visitAccDescrMultilineStatement.bind(this);
this.visitor.visitCallbackStatement = this.visitCallbackStatement.bind(this);
this.visitor.visitClickStatement = this.visitClickStatement.bind(this);
this.visitor.visitLinkStatement = this.visitLinkStatement.bind(this);
this.visitor.visitCallStatement = this.visitCallStatement.bind(this);
this.visitor.visitErrorNode = this.visitErrorNode.bind(this);
}
/**
* Visit the parse tree using the visitor pattern
*/
visit(tree: any): any {
return this.visitor.visit(tree);
}
// Visitor method implementations that delegate to the core processing methods
visitNamespaceStatement(ctx: NamespaceStatementContext): any {
console.log('🔧 ClassVisitor: Processing namespace statement');
try {
this.processNamespaceStatementEnter();
// Visit children first
const result = this.visitor.visitChildren?.(ctx);
this.processNamespaceStatementExit();
return result;
} catch (error) {
console.error('❌ ClassVisitor: Error processing namespace statement:', error);
throw error;
}
}
visitNamespaceIdentifier(ctx: NamespaceIdentifierContext): any {
console.log('🔧 ClassVisitor: Processing namespace identifier');
try {
this.processNamespaceIdentifier(ctx);
return this.visitor.visitChildren?.(ctx);
} catch (error) {
console.error('❌ ClassVisitor: Error processing namespace identifier:', error);
throw error;
}
}
visitClassIdentifier(ctx: ClassIdentifierContext): any {
console.log('🔧 ClassVisitor: Processing class identifier');
try {
this.processClassIdentifier(ctx);
return this.visitor.visitChildren?.(ctx);
} catch (error) {
console.error('❌ ClassVisitor: Error processing class identifier:', error);
throw error;
}
}
visitClassMembers(ctx: ClassMembersContext): any {
console.log('🔧 ClassVisitor: Processing class members');
try {
this.processClassMembers(ctx);
return this.visitor.visitChildren?.(ctx);
} catch (error) {
console.error('❌ ClassVisitor: Error processing class members:', error);
throw error;
}
}
visitClassStatement(ctx: ClassStatementContext): any {
console.log('🔧 ClassVisitor: Processing class statement');
try {
// Visit children first to populate member lists
const result = this.visitor.visitChildren?.(ctx);
this.processClassStatement(ctx);
return result;
} catch (error) {
console.error('❌ ClassVisitor: Error processing class statement:', error);
throw error;
}
}
visitRelationStatement(ctx: RelationStatementContext): any {
console.log('🔧 ClassVisitor: Processing relation statement');
try {
this.processRelationStatement(ctx);
return this.visitor.visitChildren?.(ctx);
} catch (error) {
console.error('❌ ClassVisitor: Error processing relation statement:', error);
throw error;
}
}
visitNoteStatement(ctx: NoteStatementContext): any {
console.log('🔧 ClassVisitor: Processing note statement');
try {
this.processNoteStatement(ctx);
return this.visitor.visitChildren?.(ctx);
} catch (error) {
console.error('❌ ClassVisitor: Error processing note statement:', error);
throw error;
}
}
visitAnnotationStatement(ctx: AnnotationStatementContext): any {
console.log('🔧 ClassVisitor: Processing annotation statement');
try {
this.processAnnotationStatement(ctx);
return this.visitor.visitChildren?.(ctx);
} catch (error) {
console.error('❌ ClassVisitor: Error processing annotation statement:', error);
throw error;
}
}
visitMemberStatement(ctx: MemberStatementContext): any {
console.log('🔧 ClassVisitor: Processing member statement');
try {
this.processMemberStatement(ctx);
return this.visitor.visitChildren?.(ctx);
} catch (error) {
console.error('❌ ClassVisitor: Error processing member statement:', error);
throw error;
}
}
visitClassDefStatement(ctx: ClassDefStatementContext): any {
console.log('🔧 ClassVisitor: Processing classDef statement');
try {
this.processClassDefStatement(ctx);
return this.visitor.visitChildren?.(ctx);
} catch (error) {
console.error('❌ ClassVisitor: Error processing classDef statement:', error);
throw error;
}
}
visitStyleStatement(ctx: StyleStatementContext): any {
console.log('🔧 ClassVisitor: Processing style statement');
try {
this.processStyleStatement(ctx);
return this.visitor.visitChildren?.(ctx);
} catch (error) {
console.error('❌ ClassVisitor: Error processing style statement:', error);
throw error;
}
}
visitCssClassStatement(ctx: CssClassStatementContext): any {
console.log('🔧 ClassVisitor: Processing cssClass statement');
try {
this.processCssClassStatement(ctx);
return this.visitor.visitChildren?.(ctx);
} catch (error) {
console.error('❌ ClassVisitor: Error processing cssClass statement:', error);
throw error;
}
}
visitDirectionStatement(ctx: DirectionStatementContext): any {
console.log('🔧 ClassVisitor: Processing direction statement');
try {
this.processDirectionStatement(ctx);
return this.visitor.visitChildren?.(ctx);
} catch (error) {
console.error('❌ ClassVisitor: Error processing direction statement:', error);
throw error;
}
}
visitAccTitleStatement(ctx: AccTitleStatementContext): any {
console.log('🔧 ClassVisitor: Processing accTitle statement');
try {
this.processAccTitleStatement(ctx);
return this.visitor.visitChildren?.(ctx);
} catch (error) {
console.error('❌ ClassVisitor: Error processing accTitle statement:', error);
throw error;
}
}
visitAccDescrStatement(ctx: AccDescrStatementContext): any {
console.log('🔧 ClassVisitor: Processing accDescr statement');
try {
this.processAccDescrStatement(ctx);
return this.visitor.visitChildren?.(ctx);
} catch (error) {
console.error('❌ ClassVisitor: Error processing accDescr statement:', error);
throw error;
}
}
visitAccDescrMultilineStatement(ctx: AccDescrMultilineStatementContext): any {
console.log('🔧 ClassVisitor: Processing accDescr multiline statement');
try {
this.processAccDescrMultilineStatement(ctx);
return this.visitor.visitChildren?.(ctx);
} catch (error) {
console.error('❌ ClassVisitor: Error processing accDescr multiline statement:', error);
throw error;
}
}
visitCallbackStatement(ctx: CallbackStatementContext): any {
console.log('🔧 ClassVisitor: Processing callback statement');
try {
this.processCallbackStatement(ctx);
return this.visitor.visitChildren?.(ctx);
} catch (error) {
console.error('❌ ClassVisitor: Error processing callback statement:', error);
throw error;
}
}
visitClickStatement(ctx: ClickStatementContext): any {
console.log('🔧 ClassVisitor: Processing click statement');
try {
this.processClickStatement(ctx);
return this.visitor.visitChildren?.(ctx);
} catch (error) {
console.error('❌ ClassVisitor: Error processing click statement:', error);
throw error;
}
}
visitLinkStatement(ctx: LinkStatementContext): any {
console.log('🔧 ClassVisitor: Processing link statement');
try {
this.processLinkStatement(ctx);
return this.visitor.visitChildren?.(ctx);
} catch (error) {
console.error('❌ ClassVisitor: Error processing link statement:', error);
throw error;
}
}
visitCallStatement(ctx: CallStatementContext): any {
console.log('🔧 ClassVisitor: Processing call statement');
try {
this.processCallStatement(ctx);
return this.visitor.visitChildren?.(ctx);
} catch (error) {
console.error('❌ ClassVisitor: Error processing call statement:', error);
throw error;
}
}
visitErrorNode(_node: any): any {
console.log('❌ ClassVisitor: Error node encountered');
// Throw error to match Jison parser behavior for syntax errors
throw new Error('Syntax error in class diagram');
}
}

View File

@@ -1,4 +1,3 @@
import type { ParseTreeListener } from 'antlr4ng';
import {
BailErrorStrategy,
CharStream,
@@ -8,596 +7,29 @@ import {
RecognitionException,
type Token,
} from 'antlr4ng';
import {
ClassParser,
type ClassIdentifierContext,
type ClassMembersContext,
type ClassNameContext,
type ClassNameSegmentContext,
type ClassStatementContext,
type NamespaceIdentifierContext,
type RelationStatementContext,
type NoteStatementContext,
type AnnotationStatementContext,
type MemberStatementContext,
type ClassDefStatementContext,
type StyleStatementContext,
type CssClassStatementContext,
type DirectionStatementContext,
type AccTitleStatementContext,
type AccDescrStatementContext,
type AccDescrMultilineStatementContext,
type CallbackStatementContext,
type ClickStatementContext,
type LinkStatementContext,
type CallStatementContext,
type CssClassRefContext,
type StringLiteralContext,
} from './generated/ClassParser.js';
import { ClassParserListener } from './generated/ClassParserListener.js';
import { ClassParser } from './generated/ClassParser.js';
import { ClassLexer } from './generated/ClassLexer.js';
import { ClassVisitor } from './ClassVisitor.js';
import { ClassListener } from './ClassListener.js';
import type { ClassDbLike } from './ClassParserCore.js';
type ClassDbLike = Record<string, any>;
const stripQuotes = (value: string): string => {
const trimmed = value.trim();
if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
try {
return JSON.parse(trimmed.replace(/\r?\n/g, '\\n')) as string;
} catch {
return trimmed.slice(1, -1).replace(/\\"/g, '"');
// Browser-safe environment variable access (same as sequence parser)
const getEnvVar = (name: string): string | undefined => {
try {
if (typeof process !== 'undefined' && process.env) {
return process.env[name];
}
}
return trimmed;
};
const stripBackticks = (value: string): string => {
const trimmed = value.trim();
if (trimmed.length >= 2 && trimmed.startsWith('`') && trimmed.endsWith('`')) {
return trimmed.slice(1, -1);
}
return trimmed;
};
const splitCommaSeparated = (text: string): string[] =>
text
.split(',')
.map((part) => part.trim())
.filter((part) => part.length > 0);
const getStringFromLiteral = (ctx: StringLiteralContext | undefined | null): string | undefined => {
if (!ctx) {
return undefined;
}
return stripQuotes(ctx.getText());
};
const getClassNameText = (ctx: ClassNameContext): string => {
const segments = ctx.classNameSegment();
const parts: string[] = [];
for (const segment of segments) {
parts.push(getClassNameSegmentText(segment));
}
return parts.join('.');
};
const getClassNameSegmentText = (ctx: ClassNameSegmentContext): string => {
if (ctx.BACKTICK_ID()) {
return stripBackticks(ctx.BACKTICK_ID()!.getText());
}
if (ctx.EDGE_STATE()) {
return ctx.EDGE_STATE()!.getText();
}
return ctx.getText();
};
const parseRelationArrow = (arrow: string, db: ClassDbLike) => {
const relation = {
type1: 'none',
type2: 'none',
lineType: db.lineType?.LINE ?? 0,
};
const trimmed = arrow.trim();
if (trimmed.includes('..')) {
relation.lineType = db.lineType?.DOTTED_LINE ?? relation.lineType;
} catch (_e) {
// process is not defined in browser, continue to browser checks
}
const leftHeads: [string, keyof typeof db.relationType][] = [
['<|', 'EXTENSION'],
['()', 'LOLLIPOP'],
['o', 'AGGREGATION'],
['*', 'COMPOSITION'],
['<', 'DEPENDENCY'],
];
for (const [prefix, key] of leftHeads) {
if (trimmed.startsWith(prefix)) {
relation.type1 = db.relationType?.[key] ?? relation.type1;
break;
}
// In browser, check for global variables
if (typeof window !== 'undefined' && (window as any).MERMAID_CONFIG) {
return (window as any).MERMAID_CONFIG[name];
}
const rightHeads: [string, keyof typeof db.relationType][] = [
['|>', 'EXTENSION'],
['()', 'LOLLIPOP'],
['o', 'AGGREGATION'],
['*', 'COMPOSITION'],
['>', 'DEPENDENCY'],
];
for (const [suffix, key] of rightHeads) {
if (trimmed.endsWith(suffix)) {
relation.type2 = db.relationType?.[key] ?? relation.type2;
break;
}
}
return relation;
};
const parseStyleLine = (db: ClassDbLike, line: string) => {
const trimmed = line.trim();
const body = trimmed.slice('style'.length).trim();
if (!body) {
return;
}
const match = /^(\S+)(\s+.+)?$/.exec(body);
if (!match) {
return;
}
const classId = match[1];
const styleBody = match[2]?.trim() ?? '';
if (!styleBody) {
return;
}
const styles = splitCommaSeparated(styleBody);
if (styles.length) {
db.setCssStyle?.(classId, styles);
}
};
const parseClassDefLine = (db: ClassDbLike, line: string) => {
const trimmed = line.trim();
const body = trimmed.slice('classDef'.length).trim();
if (!body) {
return;
}
const match = /^(\S+)(\s+.+)?$/.exec(body);
if (!match) {
return;
}
const idPart = match[1];
const stylePart = match[2]?.trim() ?? '';
const ids = splitCommaSeparated(idPart);
const styles = stylePart ? splitCommaSeparated(stylePart) : [];
db.defineClass?.(ids, styles);
};
const parseCssClassLine = (db: ClassDbLike, line: string) => {
const trimmed = line.trim();
const body = trimmed.slice('cssClass'.length).trim();
if (!body) {
return;
}
const match = /^("[^"]*"|\S+)\s+(\S+)/.exec(body);
if (!match) {
return;
}
const idsRaw = stripQuotes(match[1]);
const className = match[2];
db.setCssClass?.(idsRaw, className);
};
const parseCallbackLine = (db: ClassDbLike, line: string) => {
const trimmed = line.trim();
const match = /^callback\s+(\S+)\s+("[^"]*")(?:\s+("[^"]*"))?\s*$/.exec(trimmed);
if (!match) {
return;
}
const target = match[1];
const fn = stripQuotes(match[2]);
const tooltip = match[3] ? stripQuotes(match[3]) : undefined;
db.setClickEvent?.(target, fn);
if (tooltip) {
db.setTooltip?.(target, tooltip);
}
};
const parseClickLine = (db: ClassDbLike, line: string) => {
const trimmed = line.trim();
const callMatch = /^click\s+(\S+)\s+call\s+([^(]+)\(([^)]*)\)(?:\s+("[^"]*"))?\s*$/.exec(trimmed);
if (callMatch) {
const target = callMatch[1];
const fnName = callMatch[2].trim();
const args = callMatch[3].trim();
const tooltip = callMatch[4] ? stripQuotes(callMatch[4]) : undefined;
if (args.length > 0) {
db.setClickEvent?.(target, fnName, args);
} else {
db.setClickEvent?.(target, fnName);
}
if (tooltip) {
db.setTooltip?.(target, tooltip);
}
return target;
}
const hrefMatch = /^click\s+(\S+)\s+href\s+("[^"]*")(?:\s+("[^"]*"))?(?:\s+(\S+))?\s*$/.exec(
trimmed
);
if (hrefMatch) {
const target = hrefMatch[1];
const url = stripQuotes(hrefMatch[2]);
const tooltip = hrefMatch[3] ? stripQuotes(hrefMatch[3]) : undefined;
const targetWindow = hrefMatch[4];
if (targetWindow) {
db.setLink?.(target, url, targetWindow);
} else {
db.setLink?.(target, url);
}
if (tooltip) {
db.setTooltip?.(target, tooltip);
}
return target;
}
const genericMatch = /^click\s+(\S+)\s+("[^"]*")(?:\s+("[^"]*"))?\s*$/.exec(trimmed);
if (genericMatch) {
const target = genericMatch[1];
const link = stripQuotes(genericMatch[2]);
const tooltip = genericMatch[3] ? stripQuotes(genericMatch[3]) : undefined;
db.setLink?.(target, link);
if (tooltip) {
db.setTooltip?.(target, tooltip);
}
return target;
}
return undefined;
};
const parseLinkLine = (db: ClassDbLike, line: string) => {
const trimmed = line.trim();
const match = /^link\s+(\S+)\s+("[^"]*")(?:\s+("[^"]*"))?(?:\s+(\S+))?\s*$/.exec(trimmed);
if (!match) {
return;
}
const target = match[1];
const href = stripQuotes(match[2]);
const tooltip = match[3] ? stripQuotes(match[3]) : undefined;
const targetWindow = match[4];
if (targetWindow) {
db.setLink?.(target, href, targetWindow);
} else {
db.setLink?.(target, href);
}
if (tooltip) {
db.setTooltip?.(target, tooltip);
}
};
const parseCallLine = (db: ClassDbLike, lastTarget: string | undefined, line: string) => {
if (!lastTarget) {
return;
}
const trimmed = line.trim();
const match = /^call\s+([^(]+)\(([^)]*)\)\s*("[^"]*")?\s*$/.exec(trimmed);
if (!match) {
return;
}
const fnName = match[1].trim();
const args = match[2].trim();
const tooltip = match[3] ? stripQuotes(match[3]) : undefined;
if (args.length > 0) {
db.setClickEvent?.(lastTarget, fnName, args);
} else {
db.setClickEvent?.(lastTarget, fnName);
}
if (tooltip) {
db.setTooltip?.(lastTarget, tooltip);
}
};
interface NamespaceFrame {
name?: string;
classes: string[];
}
class ClassDiagramParseListener extends ClassParserListener implements ParseTreeListener {
private readonly classNames = new WeakMap<ClassIdentifierContext, string>();
private readonly memberLists = new WeakMap<ClassMembersContext, string[]>();
private readonly namespaceStack: NamespaceFrame[] = [];
private lastClickTarget?: string;
constructor(private readonly db: ClassDbLike) {
super();
}
private recordClassInCurrentNamespace(name: string) {
const current = this.namespaceStack[this.namespaceStack.length - 1];
if (current?.name) {
current.classes.push(name);
}
}
override enterNamespaceStatement = (): void => {
this.namespaceStack.push({ classes: [] });
};
override exitNamespaceIdentifier = (ctx: NamespaceIdentifierContext): void => {
const frame = this.namespaceStack[this.namespaceStack.length - 1];
if (!frame) {
return;
}
const classNameCtx = ctx.namespaceName()?.className();
if (!classNameCtx) {
return;
}
const name = getClassNameText(classNameCtx);
frame.name = name;
this.db.addNamespace?.(name);
};
override exitNamespaceStatement = (): void => {
const frame = this.namespaceStack.pop();
if (!frame?.name) {
return;
}
if (frame.classes.length) {
this.db.addClassesToNamespace?.(frame.name, frame.classes);
}
};
override exitClassIdentifier = (ctx: ClassIdentifierContext): void => {
const id = getClassNameText(ctx.className());
this.classNames.set(ctx, id);
this.db.addClass?.(id);
this.recordClassInCurrentNamespace(id);
const labelCtx = ctx.classLabel?.();
if (labelCtx) {
const label = getStringFromLiteral(labelCtx.stringLiteral());
if (label !== undefined) {
this.db.setClassLabel?.(id, label);
}
}
};
override exitClassMembers = (ctx: ClassMembersContext): void => {
const members: string[] = [];
for (const memberCtx of ctx.classMember() ?? []) {
if (memberCtx.MEMBER()) {
members.push(memberCtx.MEMBER()!.getText());
} else if (memberCtx.EDGE_STATE()) {
members.push(memberCtx.EDGE_STATE()!.getText());
}
}
members.reverse();
this.memberLists.set(ctx, members);
};
override exitClassStatement = (ctx: ClassStatementContext): void => {
const identifierCtx = ctx.classIdentifier();
if (!identifierCtx) {
return;
}
const classId = this.classNames.get(identifierCtx);
if (!classId) {
return;
}
const tailCtx = ctx.classStatementTail?.();
const cssRefCtx = tailCtx?.cssClassRef?.();
if (cssRefCtx) {
const cssTarget = this.resolveCssClassRef(cssRefCtx);
if (cssTarget) {
this.db.setCssClass?.(classId, cssTarget);
}
}
const memberContexts: ClassMembersContext[] = [];
const cm1 = tailCtx?.classMembers();
if (cm1) {
memberContexts.push(cm1);
}
const cssTailCtx = tailCtx?.classStatementCssTail?.();
const cm2 = cssTailCtx?.classMembers();
if (cm2) {
memberContexts.push(cm2);
}
for (const membersCtx of memberContexts) {
const members = this.memberLists.get(membersCtx) ?? [];
if (members.length) {
this.db.addMembers?.(classId, members);
}
}
};
private resolveCssClassRef(ctx: CssClassRefContext): string | undefined {
if (ctx.className()) {
return getClassNameText(ctx.className()!);
}
if (ctx.IDENTIFIER()) {
return ctx.IDENTIFIER()!.getText();
}
return undefined;
}
override exitRelationStatement = (ctx: RelationStatementContext): void => {
const classNames = ctx.className();
if (classNames.length < 2) {
return;
}
const id1 = getClassNameText(classNames[0]);
const id2 = getClassNameText(classNames[classNames.length - 1]);
const arrow = ctx.relation()?.getText() ?? '';
const relation = parseRelationArrow(arrow, this.db);
let relationTitle1 = 'none';
let relationTitle2 = 'none';
const stringLiterals = ctx.stringLiteral();
if (stringLiterals.length === 1 && ctx.children) {
const stringCtx = stringLiterals[0];
const children = ctx.children as unknown[];
const stringIndex = children.indexOf(stringCtx);
const relationCtx = ctx.relation();
const relationIndex = relationCtx ? children.indexOf(relationCtx) : -1;
if (relationIndex >= 0 && stringIndex >= 0 && stringIndex < relationIndex) {
relationTitle1 = getStringFromLiteral(stringCtx) ?? 'none';
} else {
relationTitle2 = getStringFromLiteral(stringCtx) ?? 'none';
}
} else if (stringLiterals.length >= 2) {
relationTitle1 = getStringFromLiteral(stringLiterals[0]) ?? 'none';
relationTitle2 = getStringFromLiteral(stringLiterals[1]) ?? 'none';
}
let title = 'none';
const labelCtx = ctx.relationLabel?.();
if (labelCtx?.LABEL()) {
title = this.db.cleanupLabel?.(labelCtx.LABEL().getText()) ?? 'none';
}
this.db.addRelation?.({
id1,
id2,
relation,
relationTitle1,
relationTitle2,
title,
});
};
override exitNoteStatement = (ctx: NoteStatementContext): void => {
const noteCtx = ctx.noteBody();
const literalText = noteCtx?.getText?.();
const text = literalText !== undefined ? stripQuotes(literalText) : undefined;
if (text === undefined) {
return;
}
if (ctx.NOTE_FOR()) {
const className = getClassNameText(ctx.className()!);
this.db.addNote?.(text, className);
} else {
this.db.addNote?.(text);
}
};
override exitAnnotationStatement = (ctx: AnnotationStatementContext): void => {
const className = getClassNameText(ctx.className());
const nameCtx = ctx.annotationName();
let annotation: string | undefined;
if (nameCtx.IDENTIFIER()) {
annotation = nameCtx.IDENTIFIER()!.getText();
} else {
annotation = getStringFromLiteral(nameCtx.stringLiteral());
}
if (annotation !== undefined) {
this.db.addAnnotation?.(className, annotation);
}
};
override exitMemberStatement = (ctx: MemberStatementContext): void => {
const className = getClassNameText(ctx.className());
const labelToken = ctx.LABEL();
if (!labelToken) {
return;
}
const cleaned = this.db.cleanupLabel?.(labelToken.getText()) ?? labelToken.getText();
this.db.addMember?.(className, cleaned);
};
override exitClassDefStatement = (ctx: ClassDefStatementContext): void => {
const token = ctx.CLASSDEF_LINE()?.getSymbol()?.text;
if (token) {
parseClassDefLine(this.db, token);
}
};
override exitStyleStatement = (ctx: StyleStatementContext): void => {
const token = ctx.STYLE_LINE()?.getSymbol()?.text;
if (token) {
parseStyleLine(this.db, token);
}
};
override exitCssClassStatement = (ctx: CssClassStatementContext): void => {
const token = ctx.CSSCLASS_LINE()?.getSymbol()?.text;
if (token) {
parseCssClassLine(this.db, token);
}
};
override exitDirectionStatement = (ctx: DirectionStatementContext): void => {
if (ctx.DIRECTION_TB()) {
this.db.setDirection?.('TB');
} else if (ctx.DIRECTION_BT()) {
this.db.setDirection?.('BT');
} else if (ctx.DIRECTION_LR()) {
this.db.setDirection?.('LR');
} else if (ctx.DIRECTION_RL()) {
this.db.setDirection?.('RL');
}
};
override exitAccTitleStatement = (ctx: AccTitleStatementContext): void => {
const value = ctx.ACC_TITLE_VALUE()?.getText();
if (value !== undefined) {
this.db.setAccTitle?.(value.trim());
}
};
override exitAccDescrStatement = (ctx: AccDescrStatementContext): void => {
const value = ctx.ACC_DESCR_VALUE()?.getText();
if (value !== undefined) {
this.db.setAccDescription?.(value.trim());
}
};
override exitAccDescrMultilineStatement = (ctx: AccDescrMultilineStatementContext): void => {
const value = ctx.ACC_DESCR_MULTILINE_VALUE()?.getText();
if (value !== undefined) {
this.db.setAccDescription?.(value.trim());
}
};
override exitCallbackStatement = (ctx: CallbackStatementContext): void => {
const token = ctx.CALLBACK_LINE()?.getSymbol()?.text;
if (token) {
parseCallbackLine(this.db, token);
}
};
override exitClickStatement = (ctx: ClickStatementContext): void => {
const token = ctx.CLICK_LINE()?.getSymbol()?.text;
if (!token) {
return;
}
const target = parseClickLine(this.db, token);
if (target) {
this.lastClickTarget = target;
}
};
override exitLinkStatement = (ctx: LinkStatementContext): void => {
const token = ctx.LINK_LINE()?.getSymbol()?.text;
if (token) {
parseLinkLine(this.db, token);
}
};
override exitCallStatement = (ctx: CallStatementContext): void => {
const token = ctx.CALL_LINE()?.getSymbol()?.text;
if (token) {
parseCallLine(this.db, this.lastClickTarget, token);
}
};
}
class ANTLRClassParser {
yy: ClassDbLike | null = null;
@@ -606,6 +38,11 @@ class ANTLRClassParser {
throw new Error('Class ANTLR parser missing yy (database).');
}
// eslint-disable-next-line no-console
console.log('🔧 ClassParser: USE_ANTLR_PARSER = true');
// eslint-disable-next-line no-console
console.log('🔧 ClassParser: Selected parser: ANTLR');
this.yy.clear?.();
const inputStream = CharStream.fromString(input);
@@ -631,10 +68,26 @@ class ANTLRClassParser {
try {
const tree = parser.start();
const listener = new ClassDiagramParseListener(this.yy);
ParseTreeWalker.DEFAULT.walk(listener, tree);
// 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';
// eslint-disable-next-line no-console
console.log('🔧 ClassParser: Pattern =', useVisitorPattern ? 'Visitor' : 'Listener');
if (useVisitorPattern) {
const visitor = new ClassVisitor(this.yy);
visitor.visit(tree);
} else {
const listener = new ClassListener(this.yy);
ParseTreeWalker.DEFAULT.walk(listener, tree);
}
return tree;
} catch (error) {
// eslint-disable-next-line no-console
console.error('❌ ANTLR Class Parser: Parse failed:', error);
throw this.transformParseError(error, parser);
}
}