diff --git a/packages/mermaid/package.json b/packages/mermaid/package.json index 01fa3d1b1..7ad031a81 100644 --- a/packages/mermaid/package.json +++ b/packages/mermaid/package.json @@ -50,6 +50,8 @@ "checkCircle": "npx madge --circular ./src", "antlr:sequence:clean": "rimraf src/diagrams/sequence/parser/antlr/generated", "antlr:sequence": "pnpm run antlr:sequence:clean && antlr4ng -Dlanguage=TypeScript -Xexact-output-dir -o src/diagrams/sequence/parser/antlr/generated src/diagrams/sequence/parser/antlr/SequenceLexer.g4 src/diagrams/sequence/parser/antlr/SequenceParser.g4", + "antlr:class:clean": "rimraf src/diagrams/class/parser/antlr/generated", + "antlr:class": "pnpm run antlr:class:clean && antlr4ng -Dlanguage=TypeScript -Xexact-output-dir -o src/diagrams/class/parser/antlr/generated src/diagrams/class/parser/antlr/ClassLexer.g4 src/diagrams/class/parser/antlr/ClassParser.g4", "prepublishOnly": "pnpm docs:verify-version" }, "repository": { diff --git a/packages/mermaid/src/diagrams/class/ANTLR_MIGRATION.md b/packages/mermaid/src/diagrams/class/ANTLR_MIGRATION.md new file mode 100644 index 000000000..7ae91f0e4 --- /dev/null +++ b/packages/mermaid/src/diagrams/class/ANTLR_MIGRATION.md @@ -0,0 +1,147 @@ +## ANTLR migration plan for Class Diagrams (parity with Sequence) + +This guide summarizes how to migrate the Class diagram parser from Jison to ANTLR (antlr4ng), following the approach used for Sequence diagrams. The goal is full feature parity and 100% test pass rate, while keeping the Jison implementation as the reference until the ANTLR path is green. + +### Objectives + +- Keep the existing Jison parser as the authoritative reference until parity is achieved +- Add an ANTLR parser behind a runtime flag (`USE_ANTLR_PARSER=true`), mirroring Sequence +- Achieve 100% test compatibility with the current Jison behavior, including error cases +- Keep the public DB and rendering contracts unchanged + +--- + +## 1) Prep and references + +- Use the Sequence migration as a template for structure, scripts, and patterns: + - antlr4ng grammar files: `SequenceLexer.g4`, `SequenceParser.g4` + - wrapper: `antlr-parser.ts` providing a Jison-compatible `parse()` and `yy` + - generation script: `pnpm --filter mermaid run antlr:sequence` +- For Class diagrams, identify analogous files: + - Jison grammar: `packages/mermaid/src/diagrams/class/parser/classDiagram.jison` + - DB: `packages/mermaid/src/diagrams/class/classDb.ts` + - Tests: `packages/mermaid/src/diagrams/class/classDiagram.spec.js` +- Confirm Class diagram features in the Jison grammar and tests: classes, interfaces, enums, relationships (e.g., `--`, `*--`, `o--`, `<|--`, `--|>`), visibility markers (`+`, `-`, `#`, `~`), generics (``, nested), static/abstract indicators, fields/properties, methods (with parameters and return types), stereotypes (`<< >>`), notes, direction, style/config lines, and titles/accessibility lines if supported. + +--- + +## 2) Create ANTLR grammars + +- Create `ClassLexer.g4` and `ClassParser.g4` under `packages/mermaid/src/diagrams/class/parser/antlr/` +- Lexer design guidelines (mirror Sequence approach): + - Implement stateful lexing with modes to replicate Jison behavior (e.g., default, line/rest-of-line, config/title/acc modes if used) + - Ensure token precedence resolves conflicts between relation arrows and generics (`<|--` vs ``). Prefer longest-match arrow tokens and handle generics in parser context + - Accept identifiers that include special characters that Jison allowed (quotes, underscores, digits, unicode as applicable) + - Provide tokens for core keywords and symbols: `class`, `interface`, `enum`, relationship operators, visibility markers, `<< >>` stereotypes, `{ }` blocks, `:` type separators, `,` parameter separators, `[` `]` arrays, `<` `>` generics + - Reuse common tokens shared across diagrams where appropriate (e.g., `TITLE`, `ACC_...`) if Class supports them +- Parser design guidelines: + - Follow the Jison grammar structure closely to minimize semantic drift + - Allow the final statement in the file to omit a trailing newline (to avoid EOF vs NEWLINE mismatches) + - Keep non-ambiguous rules for: + - Class declarations and bodies (members split into fields/properties vs methods) + - Modifiers (visibility, static, abstract) + - Types (simple, namespaced, generic with nesting) + - Relationships with labels (left->right/right->left forms) and multiplicities + - Stereotypes and notes + - Optional global lines (title, accTitle, accDescr) if supported by class diagrams + +--- + +## 3) Add the wrapper and flag switch + +- Add `packages/mermaid/src/diagrams/class/parser/antlr/antlr-parser.ts`: + - Export an object `{ parse, parser, yy }` that mirrors the Jison parser shape + - `parse(input)` should: + - `this.yy.clear()` to reset DB (same as Sequence) + - Build ANTLR's lexer/parser, set `BailErrorStrategy` to fail-fast on syntax errors + - Walk the tree with a listener that calls classDb methods + - Implement no-op bodies for `visitTerminal`, `visitErrorNode`, `enterEveryRule`, `exitEveryRule` (required by ParseTreeWalker) + - Avoid `require()`; import from `antlr4ng` + - Use minimal `any`; when casting is unavoidable, add clear comments +- Add `packages/mermaid/src/diagrams/class/parser/classParser.ts` similar to Sequence `sequenceParser.ts`: + - Import both the Jison parser and the ANTLR wrapper + - Gate on `process.env.USE_ANTLR_PARSER === 'true'` + - Normalize whitespace if Jison relies on specific newlines (keep parity with Sequence patterns) + +--- + +## 4) Implement the listener (semantic actions) + +Map parsed constructs to classDb calls. Typical handlers include: + +- Class-like declarations + - `db.addClass(id, { type: 'class'|'interface'|'enum', ... })` + - `db.addClassMember(id, member)` for fields/properties/methods (capture visibility, static/abstract, types, params) + - Stereotypes, annotations, notes: `db.addAnnotation(...)`, `db.addNote(...)` if applicable +- Relationships + - Parse arrow/operator to relation type; map to db constants (composition/aggregation/inheritance/realization/association) + - `db.addRelation(lhs, rhs, { type, label, multiplicity })` +- Title/Accessibility (if supported in Class diagrams) + - `db.setDiagramTitle(...)`, `db.setAccTitle(...)`, `db.setAccDescription(...)` +- Styles/Directives/Config lines as supported by the Jison grammar + +Error handling: + +- Use BailErrorStrategy; let invalid constructs throw where Jison tests expect failure +- For robustness parity, only swallow exceptions in places where Jison tolerated malformed content without aborting + +--- + +## 5) Scripts and generation + +- Add package scripts similar to Sequence in `packages/mermaid/package.json`: + - `antlr:class:clean`: remove generated TS + - `antlr:class`: run antlr4ng to generate TS into `parser/antlr/generated` +- Example command (once scripts exist): + - `pnpm --filter mermaid run antlr:class` + +--- + +## 6) Tests (Vitest) + +- Run existing Class tests with the ANTLR parser enabled: + - `USE_ANTLR_PARSER=true pnpm vitest packages/mermaid/src/diagrams/class/classDiagram.spec.js --run` +- Start by making a small focused subset pass, then expand to the full suite +- Add targeted tests for areas where the ANTLR grammar needs extra coverage (e.g., nested generics, tricky arrow/operator precedence, stereotypes, notes) +- Keep test expectations identical to Jison’s behavior; only adjust if Jison’s behavior was explicitly flaky and already tolerated in the repo + +--- + +## 7) Linting and quality + +- Satisfy ESLint rules enforced in the repo: + - Prefer imports over `require()`; no empty methods, avoid untyped `any` where reasonable + - If `@ts-ignore` is necessary, include a descriptive reason (≥10 chars) +- Provide minimal types for listener contexts where helpful; keep casts localized and commented +- Prefix diagnostic debug logs with the project’s preferred prefix if temporary logging is needed (and clean up before commit) + +--- + +## 8) Common pitfalls and tips + +- NEWLINE vs EOF: allow the last statement without a trailing newline to prevent InputMismatch +- Token conflicts: order matters; ensure relationship operators (e.g., `<|--`, `--|>`, `*--`, `o--`) win over generic `<`/`>` in the right contexts +- Identifiers: match Jison’s permissiveness (quoted names, digits where allowed) and avoid over-greedy tokens that eat operators +- Listener resilience: ensure classes and endpoints exist before adding relations (create implicitly if Jison did so) +- Error parity: do not swallow exceptions for cases where tests expect failure + +--- + +## 9) Rollout checklist + +- [ ] Grammar compiles and generated files are committed +- [ ] `USE_ANTLR_PARSER=true` passes all Class diagram tests +- [ ] Sequence and other diagram suites remain green +- [ ] No new ESLint errors; warnings minimized +- [ ] PR includes notes on parity and how to run the ANTLR tests + +--- + +## 10) Quick command reference + +- Generate ANTLR targets (after adding scripts): + - `pnpm --filter mermaid run antlr:class` +- Run Class tests with ANTLR parser: + - `USE_ANTLR_PARSER=true pnpm vitest packages/mermaid/src/diagrams/class/classDiagram.spec.js --run` +- Run a single test: + - `USE_ANTLR_PARSER=true pnpm vitest packages/mermaid/src/diagrams/class/classDiagram.spec.js -t "some test name" --run` diff --git a/packages/mermaid/src/diagrams/class/classDiagram-styles.spec.js b/packages/mermaid/src/diagrams/class/classDiagram-styles.spec.js index 71f322478..8a4d214ff 100644 --- a/packages/mermaid/src/diagrams/class/classDiagram-styles.spec.js +++ b/packages/mermaid/src/diagrams/class/classDiagram-styles.spec.js @@ -1,4 +1,4 @@ -import { parser } from './parser/classDiagram.jison'; +import { parser } from './parser/classParser.ts'; import { ClassDB } from './classDb.js'; describe('class diagram, ', function () { diff --git a/packages/mermaid/src/diagrams/class/classDiagram-v2.ts b/packages/mermaid/src/diagrams/class/classDiagram-v2.ts index 9111fe658..9b4ec9b91 100644 --- a/packages/mermaid/src/diagrams/class/classDiagram-v2.ts +++ b/packages/mermaid/src/diagrams/class/classDiagram-v2.ts @@ -1,6 +1,6 @@ import type { DiagramDefinition } from '../../diagram-api/types.js'; // @ts-ignore: JISON doesn't support types -import parser from './parser/classDiagram.jison'; +import parser from './parser/classParser.ts'; import { ClassDB } from './classDb.js'; import styles from './styles.js'; import renderer from './classRenderer-v3-unified.js'; diff --git a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts index aa5e514e0..bea8bdc8c 100644 --- a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts +++ b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/unbound-method -- Broken for Vitest mocks, see https://github.com/vitest-dev/eslint-plugin-vitest/pull/286 */ -// @ts-expect-error Jison doesn't export types -import { parser } from './parser/classDiagram.jison'; +// @ts-expect-error Parser exposes mutable yy property without typings +import { parser } from './parser/classParser.ts'; import { ClassDB } from './classDb.js'; import { vi, describe, it, expect } from 'vitest'; import type { ClassMap, NamespaceNode } from './classTypes.js'; diff --git a/packages/mermaid/src/diagrams/class/classDiagram.ts b/packages/mermaid/src/diagrams/class/classDiagram.ts index 9111fe658..9b4ec9b91 100644 --- a/packages/mermaid/src/diagrams/class/classDiagram.ts +++ b/packages/mermaid/src/diagrams/class/classDiagram.ts @@ -1,6 +1,6 @@ import type { DiagramDefinition } from '../../diagram-api/types.js'; // @ts-ignore: JISON doesn't support types -import parser from './parser/classDiagram.jison'; +import parser from './parser/classParser.ts'; import { ClassDB } from './classDb.js'; import styles from './styles.js'; import renderer from './classRenderer-v3-unified.js'; diff --git a/packages/mermaid/src/diagrams/class/parser/antlr/ClassLexer.g4 b/packages/mermaid/src/diagrams/class/parser/antlr/ClassLexer.g4 new file mode 100644 index 000000000..8e51dc24b --- /dev/null +++ b/packages/mermaid/src/diagrams/class/parser/antlr/ClassLexer.g4 @@ -0,0 +1,229 @@ +lexer grammar ClassLexer; + +tokens { + ACC_TITLE_VALUE, + ACC_DESCR_VALUE, + ACC_DESCR_MULTILINE_VALUE, + ACC_DESCR_MULTI_END, + OPEN_IN_STRUCT, + MEMBER +} + +@members { + private pendingClassBody = false; + private pendingNamespaceBody = false; + + private clearPendingScopes(): void { + this.pendingClassBody = false; + this.pendingNamespaceBody = false; + } +} + +// Common fragments +fragment WS_INLINE: [ \t]+; +fragment DIGIT: [0-9]; +fragment LETTER: [A-Za-z_]; +fragment IDENT_PART: [A-Za-z0-9_\-]; +fragment NOT_DQUOTE: ~[""]; + + +// Comments and whitespace +COMMENT: '%%' ~[\r\n]* -> skip; +NEWLINE: ('\r'? '\n')+ { this.clearPendingScopes(); }; +WS: [ \t]+ -> skip; + +// Diagram title declaration +CLASS_DIAGRAM_V2: 'classDiagram-v2' -> type(CLASS_DIAGRAM); +CLASS_DIAGRAM: 'classDiagram'; + +// Directions +DIRECTION_TB: 'direction' WS_INLINE+ 'TB'; +DIRECTION_BT: 'direction' WS_INLINE+ 'BT'; +DIRECTION_LR: 'direction' WS_INLINE+ 'LR'; +DIRECTION_RL: 'direction' WS_INLINE+ 'RL'; + +// Accessibility tokens +ACC_TITLE: 'accTitle' WS_INLINE* ':' WS_INLINE* -> pushMode(ACC_TITLE_MODE); +ACC_DESCR: 'accDescr' WS_INLINE* ':' WS_INLINE* -> pushMode(ACC_DESCR_MODE); +ACC_DESCR_MULTI: 'accDescr' WS_INLINE* '{' -> pushMode(ACC_DESCR_MULTILINE_MODE); + +// Statements captured as raw lines for semantic handling in listener +STYLE_LINE: 'style' WS_INLINE+ ~[\r\n]*; +CLASSDEF_LINE: 'classDef' ~[\r\n]*; +CSSCLASS_LINE: 'cssClass' ~[\r\n]*; +CALLBACK_LINE: 'callback' ~[\r\n]*; +CLICK_LINE: 'click' ~[\r\n]*; +LINK_LINE: 'link' ~[\r\n]*; +CALL_LINE: 'call' ~[\r\n]*; + +// Notes +NOTE_FOR: 'note' WS_INLINE+ 'for'; +NOTE: 'note'; + +// Keywords that affect block handling +CLASS: 'class' { this.pendingClassBody = true; }; +NAMESPACE: 'namespace' { this.pendingNamespaceBody = true; }; + +// Structural tokens +STYLE_SEPARATOR: ':::'; +ANNOTATION_START: '<<'; +ANNOTATION_END: '>>'; +LBRACKET: '['; +RBRACKET: ']'; +COMMA: ','; +DOT: '.'; +EDGE_STATE: '[*]'; +GENERIC: '~' (~[~\r\n])+ '~'; +// Match strings without escape semantics to mirror Jison behavior +// Allow any chars except an unescaped closing double-quote; permit newlines +STRING: '"' NOT_DQUOTE* '"'; +BACKTICK_ID: '`' (~[`])* '`'; +LABEL: ':' (~[':\r\n;])*; + +RELATION_ARROW + : (LEFT_HEAD)? LINE_BODY (RIGHT_HEAD)? + ; +fragment LEFT_HEAD + : '<|' + | '<' + | 'o' + | '*' + | '()' + ; +fragment RIGHT_HEAD + : '|>' + | '>' + | 'o' + | '*' + | '()' + ; +fragment LINE_BODY + : '--' + | '..' + ; + +// Identifiers and numbers +IDENTIFIER + : (LETTER | DIGIT) IDENT_PART* + ; +NUMBER: DIGIT+; +PLUS: '+'; +MINUS: '-'; +HASH: '#'; +PERCENT: '%'; +STAR: '*'; +SLASH: '/'; +LPAREN: '('; +RPAREN: ')'; + +// Structural braces with mode management +STRUCT_START + : '{' + { + if (this.pendingClassBody) { + this.pendingClassBody = false; + this.pushMode(ClassLexer.CLASS_BODY); + } else { + if (this.pendingNamespaceBody) { + this.pendingNamespaceBody = false; + } + this.pushMode(ClassLexer.BLOCK); + } + } + ; + +STRUCT_END: '}' { /* default mode only */ }; + +// Default fallback (should not normally trigger) +UNKNOWN: .; + +// ===== Mode: ACC_TITLE ===== +mode ACC_TITLE_MODE; +ACC_TITLE_MODE_WS: [ \t]+ -> skip; +ACC_TITLE_VALUE: ~[\r\n;#]+ -> type(ACC_TITLE_VALUE), popMode; +ACC_TITLE_MODE_NEWLINE: ('\r'? '\n')+ { this.popMode(); this.clearPendingScopes(); } -> type(NEWLINE); + +// ===== Mode: ACC_DESCR ===== +mode ACC_DESCR_MODE; +ACC_DESCR_MODE_WS: [ \t]+ -> skip; +ACC_DESCR_VALUE: ~[\r\n;#]+ -> type(ACC_DESCR_VALUE), popMode; +ACC_DESCR_MODE_NEWLINE: ('\r'? '\n')+ { this.popMode(); this.clearPendingScopes(); } -> type(NEWLINE); + +// ===== Mode: ACC_DESCR_MULTILINE ===== +mode ACC_DESCR_MULTILINE_MODE; +ACC_DESCR_MULTILINE_VALUE: (~[}])+ -> type(ACC_DESCR_MULTILINE_VALUE); +ACC_DESCR_MULTI_END: '}' -> popMode, type(ACC_DESCR_MULTI_END); + +// ===== Mode: CLASS_BODY ===== +mode CLASS_BODY; +CLASS_BODY_WS: [ \t]+ -> skip; +CLASS_BODY_COMMENT: '%%' ~[\r\n]* -> skip; +CLASS_BODY_NEWLINE: ('\r'? '\n')+ -> type(NEWLINE); +CLASS_BODY_STRUCT_END: '}' -> popMode, type(STRUCT_END); +CLASS_BODY_OPEN_BRACE: '{' -> type(OPEN_IN_STRUCT); +CLASS_BODY_EDGE_STATE: '[*]' -> type(EDGE_STATE); +CLASS_BODY_MEMBER: ~[{}\r\n]+ -> type(MEMBER); + +// ===== Mode: BLOCK ===== +mode BLOCK; +BLOCK_WS: [ \t]+ -> skip; +BLOCK_COMMENT: '%%' ~[\r\n]* -> skip; +BLOCK_NEWLINE: ('\r'? '\n')+ -> type(NEWLINE); +BLOCK_CLASS: 'class' { this.pendingClassBody = true; } -> type(CLASS); +BLOCK_NAMESPACE: 'namespace' { this.pendingNamespaceBody = true; } -> type(NAMESPACE); +BLOCK_STYLE_LINE: 'style' WS_INLINE+ ~[\r\n]* -> type(STYLE_LINE); +BLOCK_CLASSDEF_LINE: 'classDef' ~[\r\n]* -> type(CLASSDEF_LINE); +BLOCK_CSSCLASS_LINE: 'cssClass' ~[\r\n]* -> type(CSSCLASS_LINE); +BLOCK_CALLBACK_LINE: 'callback' ~[\r\n]* -> type(CALLBACK_LINE); +BLOCK_CLICK_LINE: 'click' ~[\r\n]* -> type(CLICK_LINE); +BLOCK_LINK_LINE: 'link' ~[\r\n]* -> type(LINK_LINE); +BLOCK_CALL_LINE: 'call' ~[\r\n]* -> type(CALL_LINE); +BLOCK_NOTE_FOR: 'note' WS_INLINE+ 'for' -> type(NOTE_FOR); +BLOCK_NOTE: 'note' -> type(NOTE); +BLOCK_ACC_TITLE: 'accTitle' WS_INLINE* ':' WS_INLINE* -> type(ACC_TITLE), pushMode(ACC_TITLE_MODE); +BLOCK_ACC_DESCR: 'accDescr' WS_INLINE* ':' WS_INLINE* -> type(ACC_DESCR), pushMode(ACC_DESCR_MODE); +BLOCK_ACC_DESCR_MULTI: 'accDescr' WS_INLINE* '{' -> type(ACC_DESCR_MULTI), pushMode(ACC_DESCR_MULTILINE_MODE); +BLOCK_STRUCT_START + : '{' + { + if (this.pendingClassBody) { + this.pendingClassBody = false; + this.pushMode(ClassLexer.CLASS_BODY); + } else { + if (this.pendingNamespaceBody) { + this.pendingNamespaceBody = false; + } + this.pushMode(ClassLexer.BLOCK); + } + } + -> type(STRUCT_START) + ; +BLOCK_STRUCT_END: '}' -> popMode, type(STRUCT_END); +BLOCK_STYLE_SEPARATOR: ':::' -> type(STYLE_SEPARATOR); +BLOCK_ANNOTATION_START: '<<' -> type(ANNOTATION_START); +BLOCK_ANNOTATION_END: '>>' -> type(ANNOTATION_END); +BLOCK_LBRACKET: '[' -> type(LBRACKET); +BLOCK_RBRACKET: ']' -> type(RBRACKET); +BLOCK_COMMA: ',' -> type(COMMA); +BLOCK_DOT: '.' -> type(DOT); +BLOCK_EDGE_STATE: '[*]' -> type(EDGE_STATE); +BLOCK_GENERIC: '~' (~[~\r\n])+ '~' -> type(GENERIC); +// Mirror Jison: no escape semantics inside strings in BLOCK mode as well +BLOCK_STRING: '"' NOT_DQUOTE* '"' -> type(STRING); +BLOCK_BACKTICK_ID: '`' (~[`])* '`' -> type(BACKTICK_ID); +BLOCK_LABEL: ':' (~[':\r\n;])* -> type(LABEL); +BLOCK_RELATION_ARROW + : (LEFT_HEAD)? LINE_BODY (RIGHT_HEAD)? + -> type(RELATION_ARROW) + ; +BLOCK_IDENTIFIER: (LETTER | DIGIT) IDENT_PART* -> type(IDENTIFIER); +BLOCK_NUMBER: DIGIT+ -> type(NUMBER); +BLOCK_PLUS: '+' -> type(PLUS); +BLOCK_MINUS: '-' -> type(MINUS); +BLOCK_HASH: '#' -> type(HASH); +BLOCK_PERCENT: '%' -> type(PERCENT); +BLOCK_STAR: '*' -> type(STAR); +BLOCK_SLASH: '/' -> type(SLASH); +BLOCK_LPAREN: '(' -> type(LPAREN); +BLOCK_RPAREN: ')' -> type(RPAREN); +BLOCK_UNKNOWN: . -> type(UNKNOWN); diff --git a/packages/mermaid/src/diagrams/class/parser/antlr/ClassParser.g4 b/packages/mermaid/src/diagrams/class/parser/antlr/ClassParser.g4 new file mode 100644 index 000000000..a37d04665 --- /dev/null +++ b/packages/mermaid/src/diagrams/class/parser/antlr/ClassParser.g4 @@ -0,0 +1,204 @@ +parser grammar ClassParser; + +options { + tokenVocab = ClassLexer; +} + +start + : (NEWLINE)* classDiagramSection EOF + ; + +classDiagramSection + : CLASS_DIAGRAM (NEWLINE)+ document + ; + +document + : (line)* statement? + ; + +line + : statement? NEWLINE + ; + +statement + : classStatement + | namespaceStatement + | relationStatement + | noteStatement + | annotationStatement + | memberStatement + | classDefStatement + | styleStatement + | cssClassStatement + | directionStatement + | accTitleStatement + | accDescrStatement + | accDescrMultilineStatement + | callbackStatement + | clickStatement + | linkStatement + | callStatement + ; + +classStatement + : classIdentifier classStatementTail? + ; + +classStatementTail + : STRUCT_START classMembers? STRUCT_END + | STYLE_SEPARATOR cssClassRef classStatementCssTail? + ; + +classStatementCssTail + : STRUCT_START classMembers? STRUCT_END + ; + +classIdentifier + : CLASS className classLabel? + ; + +classLabel + : LBRACKET stringLiteral RBRACKET + ; + +cssClassRef + : className + | IDENTIFIER + ; + +classMembers + : (NEWLINE | classMember)* + ; + +classMember + : MEMBER + | EDGE_STATE + ; + +namespaceStatement + : namespaceIdentifier namespaceBlock + ; + +namespaceIdentifier + : NAMESPACE namespaceName + ; + +namespaceName + : className + ; + +namespaceBlock + : STRUCT_START (NEWLINE)* namespaceBody? STRUCT_END + ; + +namespaceBody + : namespaceLine+ + ; + +namespaceLine + : (classStatement | namespaceStatement)? NEWLINE + | classStatement + | namespaceStatement + ; + +relationStatement + : className relation className relationLabel? + | className stringLiteral relation className relationLabel? + | className relation stringLiteral className relationLabel? + | className stringLiteral relation stringLiteral className relationLabel? + ; + +relation + : RELATION_ARROW + ; + +relationLabel + : LABEL + ; + +noteStatement + : NOTE_FOR className noteBody + | NOTE noteBody + ; + +noteBody + : stringLiteral + ; + +annotationStatement + : ANNOTATION_START annotationName ANNOTATION_END className + ; + +annotationName + : IDENTIFIER + | stringLiteral + ; + +memberStatement + : className LABEL + ; + +classDefStatement + : CLASSDEF_LINE + ; + +styleStatement + : STYLE_LINE + ; + +cssClassStatement + : CSSCLASS_LINE + ; + +directionStatement + : DIRECTION_TB + | DIRECTION_BT + | DIRECTION_LR + | DIRECTION_RL + ; + +accTitleStatement + : ACC_TITLE ACC_TITLE_VALUE + ; + +accDescrStatement + : ACC_DESCR ACC_DESCR_VALUE + ; + +accDescrMultilineStatement + : ACC_DESCR_MULTI ACC_DESCR_MULTILINE_VALUE ACC_DESCR_MULTI_END + ; + +callbackStatement + : CALLBACK_LINE + ; + +clickStatement + : CLICK_LINE + ; + +linkStatement + : LINK_LINE + ; + +callStatement + : CALL_LINE + ; + +stringLiteral + : STRING + ; + +className + : classNameSegment (DOT classNameSegment)* + ; + +classNameSegment + : IDENTIFIER genericSuffix? + | BACKTICK_ID genericSuffix? + | EDGE_STATE + ; + +genericSuffix + : GENERIC + ; diff --git a/packages/mermaid/src/diagrams/class/parser/antlr/antlr-parser.ts b/packages/mermaid/src/diagrams/class/parser/antlr/antlr-parser.ts new file mode 100644 index 000000000..25431ba17 --- /dev/null +++ b/packages/mermaid/src/diagrams/class/parser/antlr/antlr-parser.ts @@ -0,0 +1,729 @@ +import type { ParseTreeListener } from 'antlr4ng'; +import { + BailErrorStrategy, + CharStream, + CommonTokenStream, + ParseCancellationException, + ParseTreeWalker, + 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 { ClassLexer } from './generated/ClassLexer.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[]; +} + +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; + + parse(input: string): unknown { + if (!this.yy) { + throw new Error('Class ANTLR parser missing yy (database).'); + } + + this.yy.clear?.(); + + const inputStream = CharStream.fromString(input); + const lexer = new ClassLexer(inputStream); + const tokenStream = new CommonTokenStream(lexer); + const parser = new ClassParser(tokenStream); + + const anyParser = parser as unknown as { + getErrorHandler?: () => unknown; + setErrorHandler?: (handler: unknown) => void; + errorHandler?: unknown; + }; + const currentHandler = anyParser.getErrorHandler?.() ?? anyParser.errorHandler; + const handlerName = (currentHandler as { constructor?: { name?: string } } | undefined) + ?.constructor?.name; + if (!currentHandler || handlerName !== 'BailErrorStrategy') { + if (typeof anyParser.setErrorHandler === 'function') { + anyParser.setErrorHandler(new BailErrorStrategy()); + } else { + (parser as unknown as { errorHandler: unknown }).errorHandler = new BailErrorStrategy(); + } + } + + try { + const tree = parser.start(); + const listener = new ClassDiagramParseListener(this.yy); + ParseTreeWalker.DEFAULT.walk(listener, tree); + return tree; + } catch (error) { + throw this.transformParseError(error, parser); + } + } + + private transformParseError(error: unknown, parser: ClassParser): Error { + const recognitionError = this.unwrapRecognitionError(error); + const offendingToken = this.resolveOffendingToken(recognitionError, parser); + const line = offendingToken?.line ?? 0; + const column = offendingToken?.column ?? 0; + const message = `Parse error on line ${line}: Expecting 'STR'`; + const cause = error instanceof Error ? error : undefined; + const formatted = cause ? new Error(message, { cause }) : new Error(message); + + Object.assign(formatted, { + hash: { + line, + loc: { + first_line: line, + last_line: line, + first_column: column, + last_column: column, + }, + text: offendingToken?.text ?? '', + }, + }); + + return formatted; + } + + private unwrapRecognitionError(error: unknown): RecognitionException | undefined { + if (!error) { + return undefined; + } + if (error instanceof RecognitionException) { + return error; + } + if (error instanceof ParseCancellationException) { + const cause = (error as { cause?: unknown }).cause; + if (cause instanceof RecognitionException) { + return cause; + } + } + if (typeof error === 'object' && error !== null && 'cause' in error) { + const cause = (error as { cause?: unknown }).cause; + if (cause instanceof RecognitionException) { + return cause; + } + } + return undefined; + } + + private resolveOffendingToken( + error: RecognitionException | undefined, + parser: ClassParser + ): Token | undefined { + const candidate = (error as { offendingToken?: Token })?.offendingToken; + if (candidate) { + return candidate; + } + + const current = ( + parser as unknown as { getCurrentToken?: () => Token | undefined } + ).getCurrentToken?.(); + if (current) { + return current; + } + + const stream = ( + parser as unknown as { _input?: { LT?: (offset: number) => Token | undefined } } + )._input; + return stream?.LT?.(1); + } +} + +const parserInstance = new ANTLRClassParser(); + +const exportedParser = { + parse: (text: string) => parserInstance.parse(text), + parser: parserInstance, + yy: null as ClassDbLike | null, +}; + +Object.defineProperty(exportedParser, 'yy', { + get() { + return parserInstance.yy; + }, + set(value: ClassDbLike | null) { + parserInstance.yy = value; + }, +}); + +export default exportedParser; diff --git a/packages/mermaid/src/diagrams/class/parser/classParser.ts b/packages/mermaid/src/diagrams/class/parser/classParser.ts new file mode 100644 index 000000000..0e6ad71d4 --- /dev/null +++ b/packages/mermaid/src/diagrams/class/parser/classParser.ts @@ -0,0 +1,31 @@ +// @ts-ignore: JISON parser lacks type definitions +import jisonParser from './classDiagram.jison'; +import antlrParser from './antlr/antlr-parser.js'; + +const USE_ANTLR_PARSER = process.env.USE_ANTLR_PARSER === 'true'; + +const baseParser: any = USE_ANTLR_PARSER ? antlrParser : jisonParser; + +const selectedParser: any = Object.create(baseParser); + +selectedParser.parse = (source: string): unknown => { + const normalized = source.replace(/\r\n/g, '\n'); + if (USE_ANTLR_PARSER) { + return antlrParser.parse(normalized); + } + return jisonParser.parse(normalized); +}; + +Object.defineProperty(selectedParser, 'yy', { + get() { + return baseParser.yy; + }, + set(value) { + baseParser.yy = value; + }, + enumerable: true, + configurable: true, +}); + +export default selectedParser; +export const parser = selectedParser;