mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-10-09 17:19:45 +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 {
|
import {
|
||||||
BailErrorStrategy,
|
BailErrorStrategy,
|
||||||
CharStream,
|
CharStream,
|
||||||
@@ -8,596 +7,29 @@ import {
|
|||||||
RecognitionException,
|
RecognitionException,
|
||||||
type Token,
|
type Token,
|
||||||
} from 'antlr4ng';
|
} from 'antlr4ng';
|
||||||
import {
|
import { ClassParser } from './generated/ClassParser.js';
|
||||||
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 { ClassLexer } from './generated/ClassLexer.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>;
|
// Browser-safe environment variable access (same as sequence parser)
|
||||||
|
const getEnvVar = (name: string): string | undefined => {
|
||||||
const stripQuotes = (value: string): string => {
|
try {
|
||||||
const trimmed = value.trim();
|
if (typeof process !== 'undefined' && process.env) {
|
||||||
if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
return process.env[name];
|
||||||
try {
|
|
||||||
return JSON.parse(trimmed.replace(/\r?\n/g, '\\n')) as string;
|
|
||||||
} catch {
|
|
||||||
return trimmed.slice(1, -1).replace(/\\"/g, '"');
|
|
||||||
}
|
}
|
||||||
}
|
} catch (_e) {
|
||||||
return trimmed;
|
// process is not defined in browser, continue to browser checks
|
||||||
};
|
|
||||||
|
|
||||||
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][] = [
|
// In browser, check for global variables
|
||||||
['<|', 'EXTENSION'],
|
if (typeof window !== 'undefined' && (window as any).MERMAID_CONFIG) {
|
||||||
['()', 'LOLLIPOP'],
|
return (window as any).MERMAID_CONFIG[name];
|
||||||
['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;
|
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 {
|
class ANTLRClassParser {
|
||||||
yy: ClassDbLike | null = null;
|
yy: ClassDbLike | null = null;
|
||||||
|
|
||||||
@@ -606,6 +38,11 @@ class ANTLRClassParser {
|
|||||||
throw new Error('Class ANTLR parser missing yy (database).');
|
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?.();
|
this.yy.clear?.();
|
||||||
|
|
||||||
const inputStream = CharStream.fromString(input);
|
const inputStream = CharStream.fromString(input);
|
||||||
@@ -631,10 +68,26 @@ class ANTLRClassParser {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const tree = parser.start();
|
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;
|
return tree;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('❌ ANTLR Class Parser: Parse failed:', error);
|
||||||
throw this.transformParseError(error, parser);
|
throw this.transformParseError(error, parser);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user