mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-10-08 16:49:38 +02:00
migrated class diagram antlr parser to new structure
This commit is contained in:
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
@@ -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,
|
||||
};
|
303
packages/mermaid/src/diagrams/class/parser/antlr/ClassVisitor.ts
Normal file
303
packages/mermaid/src/diagrams/class/parser/antlr/ClassVisitor.ts
Normal 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');
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user