mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-09 10:36:43 +02:00
WIP - fixing grammar added CircleNode, string handling
This commit is contained in:
@@ -1,94 +1,127 @@
|
|||||||
/**
|
/** mermaid
|
||||||
* Mindmap grammar for Langium
|
* https://knsv.github.io/mermaid
|
||||||
* Converted from mermaid's jison grammar
|
* (c) 2015 Knut Sveidqvist
|
||||||
|
* MIT license.
|
||||||
*/
|
*/
|
||||||
grammar Mindmap
|
%lex
|
||||||
|
|
||||||
// Entry rule - equivalent to the 'start' rule in jison
|
%options case-insensitive
|
||||||
entry MindmapDocument:
|
|
||||||
// The document starts with the 'mindmap' keyword
|
|
||||||
(spaceLines+=SPACELINE)*
|
|
||||||
'mindmap' (NL)?
|
|
||||||
(documentContent=DocumentContent)?;
|
|
||||||
|
|
||||||
// Document contains multiple statements separated by newlines
|
%{
|
||||||
DocumentContent:
|
// Pre-lexer code can go here
|
||||||
statements+=Statement (stop+=Stop statements+=Statement)* (stop+=Stop)?;
|
%}
|
||||||
|
%x NODE
|
||||||
|
%x NSTR
|
||||||
|
%x NSTR2
|
||||||
|
%x ICON
|
||||||
|
%x CLASS
|
||||||
|
|
||||||
// A stop is a newline, EOF, or a spaceline - used to separate statements
|
%%
|
||||||
Stop:
|
|
||||||
NL | EOF | SPACELINE;
|
|
||||||
|
|
||||||
// Statements can be nodes, icons, classes, or empty lines
|
\s*\%\%.* {yy.getLogger().trace('Found comment',yytext); return 'SPACELINE';}
|
||||||
Statement:
|
// \%\%[^\n]*\n /* skip comments */
|
||||||
// The whitespace prefix determines nesting level in the mindmap
|
"mindmap" return 'MINDMAP';
|
||||||
(indent=INDENT)? (
|
":::" { this.begin('CLASS'); }
|
||||||
node=Node | // A node in the mindmap
|
<CLASS>.+ { this.popState();return 'CLASS'; }
|
||||||
icon=IconDecoration | // Icon decoration for a node
|
<CLASS>\n { this.popState();}
|
||||||
cssClass=ClassDecoration // CSS class for a node
|
// [\s]*"::icon(" { this.begin('ICON'); }
|
||||||
) |
|
"::icon(" { yy.getLogger().trace('Begin icon');this.begin('ICON'); }
|
||||||
SPACELINE; // Empty or comment lines
|
[\s]+[\n] {yy.getLogger().trace('SPACELINE');return 'SPACELINE' /* skip all whitespace */ ;}
|
||||||
|
[\n]+ return 'NL';
|
||||||
|
<ICON>[^\)]+ { return 'ICON'; }
|
||||||
|
<ICON>\) {yy.getLogger().trace('end icon');this.popState();}
|
||||||
|
"-)" { yy.getLogger().trace('Exploding node'); this.begin('NODE');return 'NODE_DSTART'; }
|
||||||
|
"(-" { yy.getLogger().trace('Cloud'); this.begin('NODE');return 'NODE_DSTART'; }
|
||||||
|
"))" { yy.getLogger().trace('Explosion Bang'); this.begin('NODE');return 'NODE_DSTART'; }
|
||||||
|
")" { yy.getLogger().trace('Cloud Bang'); this.begin('NODE');return 'NODE_DSTART'; }
|
||||||
|
"((" { this.begin('NODE');return 'NODE_DSTART'; }
|
||||||
|
"{{" { this.begin('NODE');return 'NODE_DSTART'; }
|
||||||
|
"(" { this.begin('NODE');return 'NODE_DSTART'; }
|
||||||
|
"[" { this.begin('NODE');return 'NODE_DSTART'; }
|
||||||
|
[\s]+ return 'SPACELIST' /* skip all whitespace */ ;
|
||||||
|
// !(-\() return 'NODE_ID';
|
||||||
|
[^\(\[\n\)\{\}]+ return 'NODE_ID';
|
||||||
|
<<EOF>> return 'EOF';
|
||||||
|
<NODE>["][`] { this.begin("NSTR2");}
|
||||||
|
<NSTR2>[^`"]+ { return "NODE_DESCR";}
|
||||||
|
<NSTR2>[`]["] { this.popState();}
|
||||||
|
<NODE>["] { yy.getLogger().trace('Starting NSTR');this.begin("NSTR");}
|
||||||
|
<NSTR>[^"]+ { yy.getLogger().trace('description:', yytext); return "NODE_DESCR";}
|
||||||
|
<NSTR>["] {this.popState();}
|
||||||
|
<NODE>[\)]\) {this.popState();yy.getLogger().trace('node end ))');return "NODE_DEND";}
|
||||||
|
<NODE>[\)] {this.popState();yy.getLogger().trace('node end )');return "NODE_DEND";}
|
||||||
|
<NODE>[\]] {this.popState();yy.getLogger().trace('node end ...',yytext);return "NODE_DEND";}
|
||||||
|
<NODE>"}}" {this.popState();yy.getLogger().trace('node end ((');return "NODE_DEND";}
|
||||||
|
<NODE>"(-" {this.popState();yy.getLogger().trace('node end (-');return "NODE_DEND";}
|
||||||
|
<NODE>"-)" {this.popState();yy.getLogger().trace('node end (-');return "NODE_DEND";}
|
||||||
|
<NODE>"((" {this.popState();yy.getLogger().trace('node end ((');return "NODE_DEND";}
|
||||||
|
<NODE>"(" {this.popState();yy.getLogger().trace('node end ((');return "NODE_DEND";}
|
||||||
|
<NODE>[^\)\]\(\}]+ { yy.getLogger().trace('Long description:', yytext); return 'NODE_DESCR';}
|
||||||
|
<NODE>.+(?!\(\() { yy.getLogger().trace('Long description:', yytext); return 'NODE_DESCR';}
|
||||||
|
// [\[] return 'NODE_START';
|
||||||
|
// .+ return 'TXT' ;
|
||||||
|
|
||||||
// A node can be either simple (just ID) or complex (with description)
|
/lex
|
||||||
Node:
|
|
||||||
SimpleNode | ComplexNode;
|
|
||||||
|
|
||||||
// Simple node is just an identifier
|
%start start
|
||||||
SimpleNode:
|
|
||||||
id=NODE_ID;
|
|
||||||
|
|
||||||
// Complex node has a description enclosed in brackets, parentheses, etc.
|
%% /* language grammar */
|
||||||
ComplexNode:
|
|
||||||
// Optional ID followed by a description with delimiters
|
|
||||||
(id=NODE_ID)? start=NODE_DSTART description=NODE_DESCR end=NODE_DEND;
|
|
||||||
|
|
||||||
// Icon decoration for nodes
|
start
|
||||||
IconDecoration:
|
// %{ : info document 'EOF' { return yy; } }
|
||||||
'::icon(' name=ICON ')';
|
: mindMap
|
||||||
|
| spaceLines mindMap
|
||||||
|
;
|
||||||
|
|
||||||
// CSS class decoration for nodes
|
spaceLines
|
||||||
ClassDecoration:
|
: SPACELINE
|
||||||
':::' name=CLASS;
|
| spaceLines SPACELINE
|
||||||
|
| spaceLines NL
|
||||||
|
;
|
||||||
|
|
||||||
// Hidden terminal rules (comments, whitespace that should be ignored during parsing)
|
mindMap
|
||||||
hidden terminal WS: /[ \t]+/;
|
: MINDMAP document { return yy; }
|
||||||
|
| MINDMAP NL document { return yy; }
|
||||||
|
;
|
||||||
|
|
||||||
// Terminal rules (lexer rules)
|
stop
|
||||||
terminal INDENT: /[ \t]+/;
|
: NL {yy.getLogger().trace('Stop NL ');}
|
||||||
terminal SPACELINE: /\s*\%\%.*|[ \t]+\n/;
|
| EOF {yy.getLogger().trace('Stop EOF ');}
|
||||||
terminal NL: /\n+/;
|
| SPACELINE
|
||||||
terminal EOF: /$/;
|
| stop NL {yy.getLogger().trace('Stop NL2 ');}
|
||||||
|
| stop EOF {yy.getLogger().trace('Stop EOF2 ');}
|
||||||
|
;
|
||||||
|
document
|
||||||
|
: document statement stop
|
||||||
|
| statement stop
|
||||||
|
;
|
||||||
|
|
||||||
// Node related terminals with refined regex patterns to match the jison lexer
|
statement
|
||||||
terminal NODE_ID: /[^\(\[\n\)\{\}]+/;
|
: SPACELIST node { yy.getLogger().info('Node: ',$2.id);yy.addNode($1.length, $2.id, $2.descr, $2.type); }
|
||||||
terminal NODE_DSTART: /\(\(|\{\{|\(|\[|\-\)|\(\-|\)\)|\)/;
|
| SPACELIST ICON { yy.getLogger().trace('Icon: ',$2);yy.decorateNode({icon: $2}); }
|
||||||
terminal NODE_DEND: /\)\)|\}\}|\)|\]|\(\-|\-\)|\(\(/;
|
| SPACELIST CLASS { yy.decorateNode({class: $2}); }
|
||||||
terminal NODE_DESCR: /[^"\)`\]]+/;
|
| SPACELINE { yy.getLogger().trace('SPACELIST');}
|
||||||
terminal ICON: /[^\)]+/;
|
| node { yy.getLogger().trace('Node: ',$1.id);yy.addNode(0, $1.id, $1.descr, $1.type); }
|
||||||
terminal CLASS: /[^\n]+/;
|
| ICON { yy.decorateNode({icon: $1}); }
|
||||||
|
| CLASS { yy.decorateNode({class: $1}); }
|
||||||
|
| SPACELIST
|
||||||
|
;
|
||||||
|
|
||||||
// We also need to implement these semantic actions from the jison grammar:
|
|
||||||
// - addNode(level, id, description, type)
|
|
||||||
// - decorateNode({icon: iconName})
|
|
||||||
// - decorateNode({class: className})
|
|
||||||
// - getType(startDelimiter, endDelimiter)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface for a MindmapNode.
|
|
||||||
* This represents the AST node for a mindmap node.
|
|
||||||
*/
|
|
||||||
interface MindmapNode {
|
|
||||||
id: string;
|
|
||||||
description?: string;
|
|
||||||
type: NodeType;
|
|
||||||
level: number; // Indentation level (derived from the INDENT token)
|
|
||||||
icon?: string;
|
|
||||||
cssClass?: string;
|
|
||||||
children?: MindmapNode[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
node
|
||||||
* The different node types in mindmap based on delimiters.
|
:nodeWithId
|
||||||
* This corresponds to the yy.getType() function in the jison grammar.
|
|nodeWithoutId
|
||||||
*/
|
;
|
||||||
type NodeType = 'DEFAULT' | 'CIRCLE' | 'CLOUD' | 'BANG' | 'HEXAGON' | 'ROUND';
|
|
||||||
|
nodeWithoutId
|
||||||
|
: NODE_DSTART NODE_DESCR NODE_DEND
|
||||||
|
{ yy.getLogger().trace("node found ..", $1); $$ = { id: $2, descr: $2, type: yy.getType($1, $3) }; }
|
||||||
|
;
|
||||||
|
|
||||||
|
nodeWithId
|
||||||
|
: NODE_ID { $$ = { id: $1, descr: $1, type: yy.nodeType.DEFAULT }; }
|
||||||
|
| NODE_ID NODE_DSTART NODE_DESCR NODE_DEND
|
||||||
|
{ yy.getLogger().trace("node found ..", $1); $$ = { id: $1, descr: $3, type: yy.getType($2, $4) }; }
|
||||||
|
;
|
||||||
|
%%
|
||||||
|
@@ -9,7 +9,9 @@ entry MindmapDoc:
|
|||||||
(MindmapRows+=MindmapRow)*;
|
(MindmapRows+=MindmapRow)*;
|
||||||
|
|
||||||
MindmapRow:
|
MindmapRow:
|
||||||
(indent=INDENTATION)? item=Item (terminator=NL)?;
|
// indent=(INDENTATION | '0') item=Item (terminator=NL)?;
|
||||||
|
(indent=INDENTATION)? item=Item (terminator=NL)?;
|
||||||
|
|
||||||
|
|
||||||
Item:
|
Item:
|
||||||
Node | IconDecoration | ClassDecoration;
|
Node | IconDecoration | ClassDecoration;
|
||||||
@@ -20,7 +22,9 @@ Node:
|
|||||||
|
|
||||||
// Specifically handle double parentheses case - highest priority
|
// Specifically handle double parentheses case - highest priority
|
||||||
CircleNode:
|
CircleNode:
|
||||||
id=ID '((' desc=(ID | STRING) '))';
|
id=ID desc=(CIRCLE_STR);
|
||||||
|
// id=ID '((' desc=(CIRCLE_STR) '))';
|
||||||
|
// id=ID '((' desc=(ID|STRING) '))';
|
||||||
|
|
||||||
// Handle other complex node variants
|
// Handle other complex node variants
|
||||||
OtherComplex:
|
OtherComplex:
|
||||||
@@ -49,6 +53,9 @@ terminal CLASS_KEYWORD: ':::';
|
|||||||
|
|
||||||
// Basic token types
|
// Basic token types
|
||||||
terminal ID: /[a-zA-Z0-9_\-\.\/]+/;
|
terminal ID: /[a-zA-Z0-9_\-\.\/]+/;
|
||||||
|
// terminal CIRCLE_STR: /[\s\S]*?\)\)/;
|
||||||
|
terminal CIRCLE_STR: /\(\(([\s\S]*?)\)\)/;
|
||||||
|
// terminal CIRCLE_STR: /(?!\(\()[\s\S]+?(?!\(\()/;
|
||||||
terminal STRING: /"[^"]*"|'[^']*'/;
|
terminal STRING: /"[^"]*"|'[^']*'/;
|
||||||
terminal INDENTATION: /[ \t]{2,}/; // Two or more spaces/tabs for indentation
|
terminal INDENTATION: /[ \t]{2,}/; // Two or more spaces/tabs for indentation
|
||||||
terminal NL: /\r?\n/;
|
terminal NL: /\r?\n/;
|
||||||
|
@@ -14,7 +14,7 @@ import {
|
|||||||
|
|
||||||
import { MermaidGeneratedSharedModule, MindmapGeneratedModule } from '../generated/module.js';
|
import { MermaidGeneratedSharedModule, MindmapGeneratedModule } from '../generated/module.js';
|
||||||
import { MindmapTokenBuilder } from './tokenBuilder.js';
|
import { MindmapTokenBuilder } from './tokenBuilder.js';
|
||||||
import { CommonValueConverter } from '../common/valueConverter.js';
|
import { MindmapValueConverter } from './valueConverter.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Declaration of `Mindmap` services.
|
* Declaration of `Mindmap` services.
|
||||||
@@ -22,7 +22,7 @@ import { CommonValueConverter } from '../common/valueConverter.js';
|
|||||||
interface MindmapAddedServices {
|
interface MindmapAddedServices {
|
||||||
parser: {
|
parser: {
|
||||||
TokenBuilder: MindmapTokenBuilder;
|
TokenBuilder: MindmapTokenBuilder;
|
||||||
ValueConverter: CommonValueConverter;
|
ValueConverter: MindmapValueConverter;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ export const MindmapModule: Module<
|
|||||||
> = {
|
> = {
|
||||||
parser: {
|
parser: {
|
||||||
TokenBuilder: () => new MindmapTokenBuilder(),
|
TokenBuilder: () => new MindmapTokenBuilder(),
|
||||||
ValueConverter: () => new CommonValueConverter(),
|
ValueConverter: () => new MindmapValueConverter(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
21
packages/parser/src/language/mindmap/valueConverter.ts
Normal file
21
packages/parser/src/language/mindmap/valueConverter.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { CstNode, GrammarAST, ValueType } from 'langium';
|
||||||
|
|
||||||
|
import { AbstractMermaidValueConverter } from '../common/index.js';
|
||||||
|
|
||||||
|
export class MindmapValueConverter extends AbstractMermaidValueConverter {
|
||||||
|
protected runCustomConverter(
|
||||||
|
rule: GrammarAST.AbstractRule,
|
||||||
|
input: string,
|
||||||
|
_cstNode: CstNode
|
||||||
|
): ValueType | undefined {
|
||||||
|
console.debug('MermaidValueConverter', rule.name, input);
|
||||||
|
if (rule.name === 'CIRCLE_STR') {
|
||||||
|
return input.replace('((', '').replace('))', '').trim();
|
||||||
|
} else if (rule.name === 'ARCH_TEXT_ICON') {
|
||||||
|
return input.replace(/["()]/g, '');
|
||||||
|
} else if (rule.name === 'ARCH_TITLE') {
|
||||||
|
return input.replace(/[[\]]/g, '').trim();
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
@@ -50,37 +50,22 @@ describe('MindMap Parser Tests', () => {
|
|||||||
|
|
||||||
expect(r1.$type).toBe('MindmapRow');
|
expect(r1.$type).toBe('MindmapRow');
|
||||||
const node1 = r1.item as CircleNode;
|
const node1 = r1.item as CircleNode;
|
||||||
console.debug('NODE1:', node1);
|
|
||||||
expect(node1.$type).toBe('CircleNode');
|
expect(node1.$type).toBe('CircleNode');
|
||||||
expect(result.value.rows[1].element.ID).toBe('Root');
|
expect(node1.id).toBe('child1');
|
||||||
expect(result.value.rows[1].element.desc).toBe('Root');
|
expect(node1.desc).toBe('Child 1');
|
||||||
expect(Object.keys(result.value.rows[1].element)).toBe('root');
|
// expect(Object.keys(r1)).toBe(2);
|
||||||
expect(result.value.rows[1].indent).toBe('indent');
|
|
||||||
expect(Object.keys(result.value.rows[1].element)).toBe(true);
|
|
||||||
expect(result.value.rows[1].element.id).toBe('SimpleNode');
|
|
||||||
|
|
||||||
// Temporarily commenting out failing assertions
|
const child2 = rows[2].item as CircleNode;
|
||||||
// expect(result.successful).toBe(true);
|
// expect(result.value.rows[1].indent).toBe('indent');
|
||||||
// Check that there are 4 rows: mindmap, root, child1, child2, grandchild
|
// expect(Object.keys(node1)).toBe(true);
|
||||||
expect(result.value.rows.length).toBe(5);
|
expect(child2.id).toBe('child2');
|
||||||
// Check that the first statement is the mindmap
|
expect(child2.desc).toBe('Child 2');
|
||||||
expect(result.value.rows[0].type).toBe('mindmap');
|
|
||||||
// Check that the second statement is the root
|
const grandChild = rows[3].item as CircleNode;
|
||||||
expect(result.value.rows[1].type.type).toBe('circle');
|
// expect(result.value.rows[1].indent).toBe('indent');
|
||||||
expect(result.value.rows[1].text).toBe('Root');
|
// expect(Object.keys(node1)).toBe(true);
|
||||||
expect(result.value.rows[1].depth).toBe(0);
|
expect(grandChild.id).toBe('grandchild');
|
||||||
// Check that the third statement is the first child
|
expect(grandChild.desc).toBe('Grand Child');
|
||||||
expect(result.value.rows[2].type.type).toBe('circle');
|
|
||||||
expect(result.value.rows[2].text).toBe('Child 1');
|
|
||||||
expect(result.value.rows[2].depth).toBe(1);
|
|
||||||
// Check that the fourth statement is the second child
|
|
||||||
expect(result.value.rows[3].type.type).toBe('circle');
|
|
||||||
expect(result.value.rows[3].text).toBe('Child 2');
|
|
||||||
expect(result.value.rows[3].depth).toBe(1);
|
|
||||||
// Check that the fifth statement is the grandchild
|
|
||||||
expect(result.value.rows[4].type.type).toBe('circle');
|
|
||||||
expect(result.value.rows[4].text).toBe('Grand Child');
|
|
||||||
expect(result.value.rows[4].depth).toBe(2);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user