From 38428114eec4d59919378772c53d232c1f68bcaf Mon Sep 17 00:00:00 2001 From: Ashish Jain Date: Fri, 19 Sep 2025 11:38:39 +0200 Subject: [PATCH] migrated class diagram antlr parser to new structure --- .../class/parser/antlr/ClassListener.ts | 266 ++++++++ .../class/parser/antlr/ClassParserCore.ts | 610 +++++++++++++++++ .../class/parser/antlr/ClassVisitor.ts | 303 +++++++++ .../class/parser/antlr/antlr-parser.ts | 621 ++---------------- 4 files changed, 1216 insertions(+), 584 deletions(-) create mode 100644 packages/mermaid/src/diagrams/class/parser/antlr/ClassListener.ts create mode 100644 packages/mermaid/src/diagrams/class/parser/antlr/ClassParserCore.ts create mode 100644 packages/mermaid/src/diagrams/class/parser/antlr/ClassVisitor.ts diff --git a/packages/mermaid/src/diagrams/class/parser/antlr/ClassListener.ts b/packages/mermaid/src/diagrams/class/parser/antlr/ClassListener.ts new file mode 100644 index 000000000..2975180ce --- /dev/null +++ b/packages/mermaid/src/diagrams/class/parser/antlr/ClassListener.ts @@ -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; + } + }; +} diff --git a/packages/mermaid/src/diagrams/class/parser/antlr/ClassParserCore.ts b/packages/mermaid/src/diagrams/class/parser/antlr/ClassParserCore.ts new file mode 100644 index 000000000..410a3b330 --- /dev/null +++ b/packages/mermaid/src/diagrams/class/parser/antlr/ClassParserCore.ts @@ -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; + +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(); + protected readonly memberLists = new WeakMap(); + 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, +}; diff --git a/packages/mermaid/src/diagrams/class/parser/antlr/ClassVisitor.ts b/packages/mermaid/src/diagrams/class/parser/antlr/ClassVisitor.ts new file mode 100644 index 000000000..a675cf4a9 --- /dev/null +++ b/packages/mermaid/src/diagrams/class/parser/antlr/ClassVisitor.ts @@ -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; + + constructor(db: ClassDbLike) { + super(db); + this.visitor = new ClassParserVisitor(); + + // 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'); + } +} diff --git a/packages/mermaid/src/diagrams/class/parser/antlr/antlr-parser.ts b/packages/mermaid/src/diagrams/class/parser/antlr/antlr-parser.ts index 25431ba17..33eb25cff 100644 --- a/packages/mermaid/src/diagrams/class/parser/antlr/antlr-parser.ts +++ b/packages/mermaid/src/diagrams/class/parser/antlr/antlr-parser.ts @@ -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; - -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(); - private readonly memberLists = new WeakMap(); - 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); } }