mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-19 15:30:03 +02:00
class diagram support added
This commit is contained in:
@@ -50,6 +50,8 @@
|
|||||||
"checkCircle": "npx madge --circular ./src",
|
"checkCircle": "npx madge --circular ./src",
|
||||||
"antlr:sequence:clean": "rimraf src/diagrams/sequence/parser/antlr/generated",
|
"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: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"
|
"prepublishOnly": "pnpm docs:verify-version"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
147
packages/mermaid/src/diagrams/class/ANTLR_MIGRATION.md
Normal file
147
packages/mermaid/src/diagrams/class/ANTLR_MIGRATION.md
Normal file
@@ -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 (`<T>`, 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 `<T>`). 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`
|
@@ -1,4 +1,4 @@
|
|||||||
import { parser } from './parser/classDiagram.jison';
|
import { parser } from './parser/classParser.ts';
|
||||||
import { ClassDB } from './classDb.js';
|
import { ClassDB } from './classDb.js';
|
||||||
|
|
||||||
describe('class diagram, ', function () {
|
describe('class diagram, ', function () {
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||||
// @ts-ignore: JISON doesn't support types
|
// @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 { ClassDB } from './classDb.js';
|
||||||
import styles from './styles.js';
|
import styles from './styles.js';
|
||||||
import renderer from './classRenderer-v3-unified.js';
|
import renderer from './classRenderer-v3-unified.js';
|
||||||
|
@@ -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 */
|
/* 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
|
// @ts-expect-error Parser exposes mutable yy property without typings
|
||||||
import { parser } from './parser/classDiagram.jison';
|
import { parser } from './parser/classParser.ts';
|
||||||
import { ClassDB } from './classDb.js';
|
import { ClassDB } from './classDb.js';
|
||||||
import { vi, describe, it, expect } from 'vitest';
|
import { vi, describe, it, expect } from 'vitest';
|
||||||
import type { ClassMap, NamespaceNode } from './classTypes.js';
|
import type { ClassMap, NamespaceNode } from './classTypes.js';
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||||
// @ts-ignore: JISON doesn't support types
|
// @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 { ClassDB } from './classDb.js';
|
||||||
import styles from './styles.js';
|
import styles from './styles.js';
|
||||||
import renderer from './classRenderer-v3-unified.js';
|
import renderer from './classRenderer-v3-unified.js';
|
||||||
|
229
packages/mermaid/src/diagrams/class/parser/antlr/ClassLexer.g4
Normal file
229
packages/mermaid/src/diagrams/class/parser/antlr/ClassLexer.g4
Normal file
@@ -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);
|
204
packages/mermaid/src/diagrams/class/parser/antlr/ClassParser.g4
Normal file
204
packages/mermaid/src/diagrams/class/parser/antlr/ClassParser.g4
Normal file
@@ -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
|
||||||
|
;
|
729
packages/mermaid/src/diagrams/class/parser/antlr/antlr-parser.ts
Normal file
729
packages/mermaid/src/diagrams/class/parser/antlr/antlr-parser.ts
Normal file
@@ -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<string, any>;
|
||||||
|
|
||||||
|
const stripQuotes = (value: string): string => {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(trimmed.replace(/\r?\n/g, '\\n')) as string;
|
||||||
|
} catch {
|
||||||
|
return trimmed.slice(1, -1).replace(/\\"/g, '"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stripBackticks = (value: string): string => {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (trimmed.length >= 2 && trimmed.startsWith('`') && trimmed.endsWith('`')) {
|
||||||
|
return trimmed.slice(1, -1);
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const splitCommaSeparated = (text: string): string[] =>
|
||||||
|
text
|
||||||
|
.split(',')
|
||||||
|
.map((part) => part.trim())
|
||||||
|
.filter((part) => part.length > 0);
|
||||||
|
|
||||||
|
const getStringFromLiteral = (ctx: StringLiteralContext | undefined | null): string | undefined => {
|
||||||
|
if (!ctx) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return stripQuotes(ctx.getText());
|
||||||
|
};
|
||||||
|
|
||||||
|
const getClassNameText = (ctx: ClassNameContext): string => {
|
||||||
|
const segments = ctx.classNameSegment();
|
||||||
|
const parts: string[] = [];
|
||||||
|
for (const segment of segments) {
|
||||||
|
parts.push(getClassNameSegmentText(segment));
|
||||||
|
}
|
||||||
|
return parts.join('.');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getClassNameSegmentText = (ctx: ClassNameSegmentContext): string => {
|
||||||
|
if (ctx.BACKTICK_ID()) {
|
||||||
|
return stripBackticks(ctx.BACKTICK_ID()!.getText());
|
||||||
|
}
|
||||||
|
if (ctx.EDGE_STATE()) {
|
||||||
|
return ctx.EDGE_STATE()!.getText();
|
||||||
|
}
|
||||||
|
return ctx.getText();
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseRelationArrow = (arrow: string, db: ClassDbLike) => {
|
||||||
|
const relation = {
|
||||||
|
type1: 'none',
|
||||||
|
type2: 'none',
|
||||||
|
lineType: db.lineType?.LINE ?? 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const trimmed = arrow.trim();
|
||||||
|
if (trimmed.includes('..')) {
|
||||||
|
relation.lineType = db.lineType?.DOTTED_LINE ?? relation.lineType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftHeads: [string, keyof typeof db.relationType][] = [
|
||||||
|
['<|', 'EXTENSION'],
|
||||||
|
['()', 'LOLLIPOP'],
|
||||||
|
['o', 'AGGREGATION'],
|
||||||
|
['*', 'COMPOSITION'],
|
||||||
|
['<', 'DEPENDENCY'],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [prefix, key] of leftHeads) {
|
||||||
|
if (trimmed.startsWith(prefix)) {
|
||||||
|
relation.type1 = db.relationType?.[key] ?? relation.type1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rightHeads: [string, keyof typeof db.relationType][] = [
|
||||||
|
['|>', 'EXTENSION'],
|
||||||
|
['()', 'LOLLIPOP'],
|
||||||
|
['o', 'AGGREGATION'],
|
||||||
|
['*', 'COMPOSITION'],
|
||||||
|
['>', 'DEPENDENCY'],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [suffix, key] of rightHeads) {
|
||||||
|
if (trimmed.endsWith(suffix)) {
|
||||||
|
relation.type2 = db.relationType?.[key] ?? relation.type2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return relation;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseStyleLine = (db: ClassDbLike, line: string) => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
const body = trimmed.slice('style'.length).trim();
|
||||||
|
if (!body) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const match = /^(\S+)(\s+.+)?$/.exec(body);
|
||||||
|
if (!match) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const classId = match[1];
|
||||||
|
const styleBody = match[2]?.trim() ?? '';
|
||||||
|
if (!styleBody) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const styles = splitCommaSeparated(styleBody);
|
||||||
|
if (styles.length) {
|
||||||
|
db.setCssStyle?.(classId, styles);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseClassDefLine = (db: ClassDbLike, line: string) => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
const body = trimmed.slice('classDef'.length).trim();
|
||||||
|
if (!body) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const match = /^(\S+)(\s+.+)?$/.exec(body);
|
||||||
|
if (!match) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const idPart = match[1];
|
||||||
|
const stylePart = match[2]?.trim() ?? '';
|
||||||
|
const ids = splitCommaSeparated(idPart);
|
||||||
|
const styles = stylePart ? splitCommaSeparated(stylePart) : [];
|
||||||
|
db.defineClass?.(ids, styles);
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseCssClassLine = (db: ClassDbLike, line: string) => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
const body = trimmed.slice('cssClass'.length).trim();
|
||||||
|
if (!body) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const match = /^("[^"]*"|\S+)\s+(\S+)/.exec(body);
|
||||||
|
if (!match) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const idsRaw = stripQuotes(match[1]);
|
||||||
|
const className = match[2];
|
||||||
|
db.setCssClass?.(idsRaw, className);
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseCallbackLine = (db: ClassDbLike, line: string) => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
const match = /^callback\s+(\S+)\s+("[^"]*")(?:\s+("[^"]*"))?\s*$/.exec(trimmed);
|
||||||
|
if (!match) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const target = match[1];
|
||||||
|
const fn = stripQuotes(match[2]);
|
||||||
|
const tooltip = match[3] ? stripQuotes(match[3]) : undefined;
|
||||||
|
db.setClickEvent?.(target, fn);
|
||||||
|
if (tooltip) {
|
||||||
|
db.setTooltip?.(target, tooltip);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseClickLine = (db: ClassDbLike, line: string) => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
const callMatch = /^click\s+(\S+)\s+call\s+([^(]+)\(([^)]*)\)(?:\s+("[^"]*"))?\s*$/.exec(trimmed);
|
||||||
|
if (callMatch) {
|
||||||
|
const target = callMatch[1];
|
||||||
|
const fnName = callMatch[2].trim();
|
||||||
|
const args = callMatch[3].trim();
|
||||||
|
const tooltip = callMatch[4] ? stripQuotes(callMatch[4]) : undefined;
|
||||||
|
if (args.length > 0) {
|
||||||
|
db.setClickEvent?.(target, fnName, args);
|
||||||
|
} else {
|
||||||
|
db.setClickEvent?.(target, fnName);
|
||||||
|
}
|
||||||
|
if (tooltip) {
|
||||||
|
db.setTooltip?.(target, tooltip);
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hrefMatch = /^click\s+(\S+)\s+href\s+("[^"]*")(?:\s+("[^"]*"))?(?:\s+(\S+))?\s*$/.exec(
|
||||||
|
trimmed
|
||||||
|
);
|
||||||
|
if (hrefMatch) {
|
||||||
|
const target = hrefMatch[1];
|
||||||
|
const url = stripQuotes(hrefMatch[2]);
|
||||||
|
const tooltip = hrefMatch[3] ? stripQuotes(hrefMatch[3]) : undefined;
|
||||||
|
const targetWindow = hrefMatch[4];
|
||||||
|
if (targetWindow) {
|
||||||
|
db.setLink?.(target, url, targetWindow);
|
||||||
|
} else {
|
||||||
|
db.setLink?.(target, url);
|
||||||
|
}
|
||||||
|
if (tooltip) {
|
||||||
|
db.setTooltip?.(target, tooltip);
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
const genericMatch = /^click\s+(\S+)\s+("[^"]*")(?:\s+("[^"]*"))?\s*$/.exec(trimmed);
|
||||||
|
if (genericMatch) {
|
||||||
|
const target = genericMatch[1];
|
||||||
|
const link = stripQuotes(genericMatch[2]);
|
||||||
|
const tooltip = genericMatch[3] ? stripQuotes(genericMatch[3]) : undefined;
|
||||||
|
db.setLink?.(target, link);
|
||||||
|
if (tooltip) {
|
||||||
|
db.setTooltip?.(target, tooltip);
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseLinkLine = (db: ClassDbLike, line: string) => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
const match = /^link\s+(\S+)\s+("[^"]*")(?:\s+("[^"]*"))?(?:\s+(\S+))?\s*$/.exec(trimmed);
|
||||||
|
if (!match) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const target = match[1];
|
||||||
|
const href = stripQuotes(match[2]);
|
||||||
|
const tooltip = match[3] ? stripQuotes(match[3]) : undefined;
|
||||||
|
const targetWindow = match[4];
|
||||||
|
|
||||||
|
if (targetWindow) {
|
||||||
|
db.setLink?.(target, href, targetWindow);
|
||||||
|
} else {
|
||||||
|
db.setLink?.(target, href);
|
||||||
|
}
|
||||||
|
if (tooltip) {
|
||||||
|
db.setTooltip?.(target, tooltip);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseCallLine = (db: ClassDbLike, lastTarget: string | undefined, line: string) => {
|
||||||
|
if (!lastTarget) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const trimmed = line.trim();
|
||||||
|
const match = /^call\s+([^(]+)\(([^)]*)\)\s*("[^"]*")?\s*$/.exec(trimmed);
|
||||||
|
if (!match) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fnName = match[1].trim();
|
||||||
|
const args = match[2].trim();
|
||||||
|
const tooltip = match[3] ? stripQuotes(match[3]) : undefined;
|
||||||
|
if (args.length > 0) {
|
||||||
|
db.setClickEvent?.(lastTarget, fnName, args);
|
||||||
|
} else {
|
||||||
|
db.setClickEvent?.(lastTarget, fnName);
|
||||||
|
}
|
||||||
|
if (tooltip) {
|
||||||
|
db.setTooltip?.(lastTarget, tooltip);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
interface NamespaceFrame {
|
||||||
|
name?: string;
|
||||||
|
classes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ClassDiagramParseListener extends ClassParserListener implements ParseTreeListener {
|
||||||
|
private readonly classNames = new WeakMap<ClassIdentifierContext, string>();
|
||||||
|
private readonly memberLists = new WeakMap<ClassMembersContext, string[]>();
|
||||||
|
private readonly namespaceStack: NamespaceFrame[] = [];
|
||||||
|
private lastClickTarget?: string;
|
||||||
|
|
||||||
|
constructor(private readonly db: ClassDbLike) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
private recordClassInCurrentNamespace(name: string) {
|
||||||
|
const current = this.namespaceStack[this.namespaceStack.length - 1];
|
||||||
|
if (current?.name) {
|
||||||
|
current.classes.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override enterNamespaceStatement = (): void => {
|
||||||
|
this.namespaceStack.push({ classes: [] });
|
||||||
|
};
|
||||||
|
|
||||||
|
override exitNamespaceIdentifier = (ctx: NamespaceIdentifierContext): void => {
|
||||||
|
const frame = this.namespaceStack[this.namespaceStack.length - 1];
|
||||||
|
if (!frame) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const classNameCtx = ctx.namespaceName()?.className();
|
||||||
|
if (!classNameCtx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const name = getClassNameText(classNameCtx);
|
||||||
|
frame.name = name;
|
||||||
|
this.db.addNamespace?.(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
override exitNamespaceStatement = (): void => {
|
||||||
|
const frame = this.namespaceStack.pop();
|
||||||
|
if (!frame?.name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (frame.classes.length) {
|
||||||
|
this.db.addClassesToNamespace?.(frame.name, frame.classes);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
override exitClassIdentifier = (ctx: ClassIdentifierContext): void => {
|
||||||
|
const id = getClassNameText(ctx.className());
|
||||||
|
this.classNames.set(ctx, id);
|
||||||
|
this.db.addClass?.(id);
|
||||||
|
this.recordClassInCurrentNamespace(id);
|
||||||
|
|
||||||
|
const labelCtx = ctx.classLabel?.();
|
||||||
|
if (labelCtx) {
|
||||||
|
const label = getStringFromLiteral(labelCtx.stringLiteral());
|
||||||
|
if (label !== undefined) {
|
||||||
|
this.db.setClassLabel?.(id, label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
override exitClassMembers = (ctx: ClassMembersContext): void => {
|
||||||
|
const members: string[] = [];
|
||||||
|
for (const memberCtx of ctx.classMember() ?? []) {
|
||||||
|
if (memberCtx.MEMBER()) {
|
||||||
|
members.push(memberCtx.MEMBER()!.getText());
|
||||||
|
} else if (memberCtx.EDGE_STATE()) {
|
||||||
|
members.push(memberCtx.EDGE_STATE()!.getText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
members.reverse();
|
||||||
|
this.memberLists.set(ctx, members);
|
||||||
|
};
|
||||||
|
|
||||||
|
override exitClassStatement = (ctx: ClassStatementContext): void => {
|
||||||
|
const identifierCtx = ctx.classIdentifier();
|
||||||
|
if (!identifierCtx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const classId = this.classNames.get(identifierCtx);
|
||||||
|
if (!classId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tailCtx = ctx.classStatementTail?.();
|
||||||
|
const cssRefCtx = tailCtx?.cssClassRef?.();
|
||||||
|
if (cssRefCtx) {
|
||||||
|
const cssTarget = this.resolveCssClassRef(cssRefCtx);
|
||||||
|
if (cssTarget) {
|
||||||
|
this.db.setCssClass?.(classId, cssTarget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const memberContexts: ClassMembersContext[] = [];
|
||||||
|
const cm1 = tailCtx?.classMembers();
|
||||||
|
if (cm1) {
|
||||||
|
memberContexts.push(cm1);
|
||||||
|
}
|
||||||
|
const cssTailCtx = tailCtx?.classStatementCssTail?.();
|
||||||
|
const cm2 = cssTailCtx?.classMembers();
|
||||||
|
if (cm2) {
|
||||||
|
memberContexts.push(cm2);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const membersCtx of memberContexts) {
|
||||||
|
const members = this.memberLists.get(membersCtx) ?? [];
|
||||||
|
if (members.length) {
|
||||||
|
this.db.addMembers?.(classId, members);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private resolveCssClassRef(ctx: CssClassRefContext): string | undefined {
|
||||||
|
if (ctx.className()) {
|
||||||
|
return getClassNameText(ctx.className()!);
|
||||||
|
}
|
||||||
|
if (ctx.IDENTIFIER()) {
|
||||||
|
return ctx.IDENTIFIER()!.getText();
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
override exitRelationStatement = (ctx: RelationStatementContext): void => {
|
||||||
|
const classNames = ctx.className();
|
||||||
|
if (classNames.length < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id1 = getClassNameText(classNames[0]);
|
||||||
|
const id2 = getClassNameText(classNames[classNames.length - 1]);
|
||||||
|
|
||||||
|
const arrow = ctx.relation()?.getText() ?? '';
|
||||||
|
const relation = parseRelationArrow(arrow, this.db);
|
||||||
|
|
||||||
|
let relationTitle1 = 'none';
|
||||||
|
let relationTitle2 = 'none';
|
||||||
|
const stringLiterals = ctx.stringLiteral();
|
||||||
|
if (stringLiterals.length === 1 && ctx.children) {
|
||||||
|
const stringCtx = stringLiterals[0];
|
||||||
|
const children = ctx.children as unknown[];
|
||||||
|
const stringIndex = children.indexOf(stringCtx);
|
||||||
|
const relationCtx = ctx.relation();
|
||||||
|
const relationIndex = relationCtx ? children.indexOf(relationCtx) : -1;
|
||||||
|
if (relationIndex >= 0 && stringIndex >= 0 && stringIndex < relationIndex) {
|
||||||
|
relationTitle1 = getStringFromLiteral(stringCtx) ?? 'none';
|
||||||
|
} else {
|
||||||
|
relationTitle2 = getStringFromLiteral(stringCtx) ?? 'none';
|
||||||
|
}
|
||||||
|
} else if (stringLiterals.length >= 2) {
|
||||||
|
relationTitle1 = getStringFromLiteral(stringLiterals[0]) ?? 'none';
|
||||||
|
relationTitle2 = getStringFromLiteral(stringLiterals[1]) ?? 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = 'none';
|
||||||
|
const labelCtx = ctx.relationLabel?.();
|
||||||
|
if (labelCtx?.LABEL()) {
|
||||||
|
title = this.db.cleanupLabel?.(labelCtx.LABEL().getText()) ?? 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.db.addRelation?.({
|
||||||
|
id1,
|
||||||
|
id2,
|
||||||
|
relation,
|
||||||
|
relationTitle1,
|
||||||
|
relationTitle2,
|
||||||
|
title,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
override exitNoteStatement = (ctx: NoteStatementContext): void => {
|
||||||
|
const noteCtx = ctx.noteBody();
|
||||||
|
const literalText = noteCtx?.getText?.();
|
||||||
|
const text = literalText !== undefined ? stripQuotes(literalText) : undefined;
|
||||||
|
if (text === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ctx.NOTE_FOR()) {
|
||||||
|
const className = getClassNameText(ctx.className()!);
|
||||||
|
this.db.addNote?.(text, className);
|
||||||
|
} else {
|
||||||
|
this.db.addNote?.(text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
override exitAnnotationStatement = (ctx: AnnotationStatementContext): void => {
|
||||||
|
const className = getClassNameText(ctx.className());
|
||||||
|
const nameCtx = ctx.annotationName();
|
||||||
|
let annotation: string | undefined;
|
||||||
|
if (nameCtx.IDENTIFIER()) {
|
||||||
|
annotation = nameCtx.IDENTIFIER()!.getText();
|
||||||
|
} else {
|
||||||
|
annotation = getStringFromLiteral(nameCtx.stringLiteral());
|
||||||
|
}
|
||||||
|
if (annotation !== undefined) {
|
||||||
|
this.db.addAnnotation?.(className, annotation);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
override exitMemberStatement = (ctx: MemberStatementContext): void => {
|
||||||
|
const className = getClassNameText(ctx.className());
|
||||||
|
const labelToken = ctx.LABEL();
|
||||||
|
if (!labelToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cleaned = this.db.cleanupLabel?.(labelToken.getText()) ?? labelToken.getText();
|
||||||
|
this.db.addMember?.(className, cleaned);
|
||||||
|
};
|
||||||
|
|
||||||
|
override exitClassDefStatement = (ctx: ClassDefStatementContext): void => {
|
||||||
|
const token = ctx.CLASSDEF_LINE()?.getSymbol()?.text;
|
||||||
|
if (token) {
|
||||||
|
parseClassDefLine(this.db, token);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
override exitStyleStatement = (ctx: StyleStatementContext): void => {
|
||||||
|
const token = ctx.STYLE_LINE()?.getSymbol()?.text;
|
||||||
|
if (token) {
|
||||||
|
parseStyleLine(this.db, token);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
override exitCssClassStatement = (ctx: CssClassStatementContext): void => {
|
||||||
|
const token = ctx.CSSCLASS_LINE()?.getSymbol()?.text;
|
||||||
|
if (token) {
|
||||||
|
parseCssClassLine(this.db, token);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
override exitDirectionStatement = (ctx: DirectionStatementContext): void => {
|
||||||
|
if (ctx.DIRECTION_TB()) {
|
||||||
|
this.db.setDirection?.('TB');
|
||||||
|
} else if (ctx.DIRECTION_BT()) {
|
||||||
|
this.db.setDirection?.('BT');
|
||||||
|
} else if (ctx.DIRECTION_LR()) {
|
||||||
|
this.db.setDirection?.('LR');
|
||||||
|
} else if (ctx.DIRECTION_RL()) {
|
||||||
|
this.db.setDirection?.('RL');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
override exitAccTitleStatement = (ctx: AccTitleStatementContext): void => {
|
||||||
|
const value = ctx.ACC_TITLE_VALUE()?.getText();
|
||||||
|
if (value !== undefined) {
|
||||||
|
this.db.setAccTitle?.(value.trim());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
override exitAccDescrStatement = (ctx: AccDescrStatementContext): void => {
|
||||||
|
const value = ctx.ACC_DESCR_VALUE()?.getText();
|
||||||
|
if (value !== undefined) {
|
||||||
|
this.db.setAccDescription?.(value.trim());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
override exitAccDescrMultilineStatement = (ctx: AccDescrMultilineStatementContext): void => {
|
||||||
|
const value = ctx.ACC_DESCR_MULTILINE_VALUE()?.getText();
|
||||||
|
if (value !== undefined) {
|
||||||
|
this.db.setAccDescription?.(value.trim());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
override exitCallbackStatement = (ctx: CallbackStatementContext): void => {
|
||||||
|
const token = ctx.CALLBACK_LINE()?.getSymbol()?.text;
|
||||||
|
if (token) {
|
||||||
|
parseCallbackLine(this.db, token);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
override exitClickStatement = (ctx: ClickStatementContext): void => {
|
||||||
|
const token = ctx.CLICK_LINE()?.getSymbol()?.text;
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const target = parseClickLine(this.db, token);
|
||||||
|
if (target) {
|
||||||
|
this.lastClickTarget = target;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
override exitLinkStatement = (ctx: LinkStatementContext): void => {
|
||||||
|
const token = ctx.LINK_LINE()?.getSymbol()?.text;
|
||||||
|
if (token) {
|
||||||
|
parseLinkLine(this.db, token);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
override exitCallStatement = (ctx: CallStatementContext): void => {
|
||||||
|
const token = ctx.CALL_LINE()?.getSymbol()?.text;
|
||||||
|
if (token) {
|
||||||
|
parseCallLine(this.db, this.lastClickTarget, token);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class ANTLRClassParser {
|
||||||
|
yy: ClassDbLike | null = null;
|
||||||
|
|
||||||
|
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;
|
31
packages/mermaid/src/diagrams/class/parser/classParser.ts
Normal file
31
packages/mermaid/src/diagrams/class/parser/classParser.ts
Normal file
@@ -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;
|
Reference in New Issue
Block a user