diff --git a/packages/parser/src/language/mindmap/mindmap-validator.ts b/packages/parser/src/language/mindmap/mindmap-validator.ts index 3271c548c..6170e4003 100644 --- a/packages/parser/src/language/mindmap/mindmap-validator.ts +++ b/packages/parser/src/language/mindmap/mindmap-validator.ts @@ -59,14 +59,14 @@ export class MindmapValidator { ) { rootNodeIndentation = 0; } else if (row.indent === undefined) { - console.debug('FAIL 1', rootNodeIndentation, row.indent); + // console.debug('FAIL 1', rootNodeIndentation, row.indent); // If we've already found a root node, report an error accept('error', 'Multiple root nodes are not allowed in a mindmap.', { node: row, property: 'item', }); } else if (rootNodeIndentation >= row.indent) { - console.debug('FAIL 2', rootNodeIndentation, row.indent, row.item); + // console.debug('FAIL 2', rootNodeIndentation, row.indent, row.item); accept('error', 'Multiple root nodes are not allowed in a mindmap.', { node: row, property: 'item', diff --git a/packages/parser/src/language/mindmap/mindmap.langium b/packages/parser/src/language/mindmap/mindmap.langium index 2b434f757..7606c0c8f 100644 --- a/packages/parser/src/language/mindmap/mindmap.langium +++ b/packages/parser/src/language/mindmap/mindmap.langium @@ -18,19 +18,25 @@ Item: // Use a special rule order to handle the parsing precedence Node: - CircleNode | OtherComplex | SimpleNode | RoundedNode | SquareNode; + SquareNode | RoundedNode | CircleNode | BangNode | CloudNode | HexagonNode | SimpleNode; // Specifically handle double parentheses case - highest priority CircleNode: - (id=ID)? desc=(CIRCLE_STR); - // id=ID '((' desc=(CIRCLE_STR) '))'; - // id=ID '((' desc=(ID|STRING) '))'; + (id=ID)? desc=(CIRCLE_STR|CIRCLE_QSTR); +BangNode: + (id=ID)? desc=(BANG_STR|BANG_QSTR); RoundedNode: - (id=ID)? desc=(ROUNDED_STR_QUOTES|ROUNDED_STR); + (id=ID)? desc=(ROUNDED_STR|ROUNDED_QSTR); SquareNode: - (id=ID)? desc=(SQUARE_STR_QUOTES|SQUARE_STR); + (id=ID)? desc=(SQUARE_STR|SQUARE_QSTR); + +CloudNode: + (id=ID)? desc=(CLOUD_STR|CLOUD_QSTR); + +HexagonNode: + (id=ID)? desc=(HEXAGON_STR|HEXAGON_QSTR); // Handle other complex node variants OtherComplex: @@ -47,33 +53,37 @@ SimpleNode: id=ID; IconDecoration: - ICON_KEYWORD content=(ID|STRING) ')'; + content=(ICON); ClassDecoration: - CLASS_KEYWORD content=(ID|STRING); + content=(CLASS); // This should be processed before whitespace is ignored terminal INDENTATION: /[ \t]{2,}/; // Two or more spaces/tabs for indentation // Keywords with fixed text patterns terminal MINDMAP_KEYWORD: 'mindmap\n'; -terminal ICON_KEYWORD: '::icon('; -terminal CLASS_KEYWORD: ':::'; +// terminal ICON_KEYWORD: '::icon('; +// terminal CLASS_KEYWORD: ':::'; // Basic token types -// terminal CIRCLE_STR: /[\s\S]*?\)\)/; -terminal CIRCLE_STR: /\(\(([\s\S]*?)\)\)/; -terminal ROUNDED_STR_QUOTES: /\(\"([\s\S]*?)\"\)/; -terminal ROUNDED_STR: /\(([\s\S]*?)\)/; -terminal SQUARE_STR_QUOTES: /\[\"([\s\S]*?)\"\]/; +terminal CIRCLE_QSTR: "((\"" -> "\"))"; +terminal CIRCLE_STR: "((" -> "))"; +terminal BANG_QSTR: "))\"" -> "\"(("; +terminal BANG_STR: "))" -> "(("; +terminal CLOUD_QSTR: ")\"" -> "\"("; +terminal CLOUD_STR: ")" -> "("; +terminal HEXAGON_QSTR: "{{\"" -> "\"}}"; +terminal HEXAGON_STR: "{{" -> "}}"; +terminal ROUNDED_QSTR: "(\"" -> "\")"; +terminal ROUNDED_STR: "(" -> ")"; +terminal SQUARE_QSTR: /\[\"([\s\S]*?)\"\]/; terminal SQUARE_STR: /\[([\s\S]*?)\]/; -terminal BANG_STR_QUOTES: /\[\"([\s\S]*?)\"\]/; -terminal BANG_STR: /\[([\s\S]*?)\]/; -terminal CLOUD_STR_QUOTES: /\)\"([\s\S]*?)\"\(/; -terminal CLOUD_STR: /\)([\s\S]*?)\(/; -// terminal CIRCLE_STR: /(?!\(\()[\s\S]+?(?!\(\()/; + +terminal ICON: "::icon(" -> ")"; +terminal CLASS: /:::([^\n:])*/; + terminal ID: /[a-zA-Z0-9_\-\.\/]+/; -terminal STRING: /"[^"]*"|'[^']*'/; // Modified indentation rule to have higher priority than WS diff --git a/packages/parser/src/language/mindmap/valueConverter.ts b/packages/parser/src/language/mindmap/valueConverter.ts index 1182c7596..5dafe2fa3 100644 --- a/packages/parser/src/language/mindmap/valueConverter.ts +++ b/packages/parser/src/language/mindmap/valueConverter.ts @@ -11,26 +11,36 @@ export class MindmapValueConverter extends AbstractMermaidValueConverter { console.debug('MermaidValueConverter', rule.name, input); if (rule.name === 'CIRCLE_STR') { return input.replace('((', '').replace('))', '').trim(); + } else if (rule.name === 'CIRCLE_QSTR') { + return input.replace('(("', '').replace('"))', '').trim(); } else if (rule.name === 'ROUNDED_STR') { return input.replace('(', '').replace(')', '').trim(); - } else if (rule.name === 'ROUNDED_STR_QUOTES') { + } else if (rule.name === 'ROUNDED_QSTR') { return input.replace('("', '').replace('")', '').trim(); } else if (rule.name === 'SQUARE_STR') { return input.replace('[', '').replace(']', '').trim(); - } else if (rule.name === 'SQUARE_STR_QUOTES') { + } else if (rule.name === 'SQUARE_QSTR') { return input.replace('["', '').replace('"]', '').trim(); } else if (rule.name === 'BANG_STR') { return input.replace('))', '').replace('((', '').trim(); - } else if (rule.name === 'BANG_STR_QUOTES') { + } else if (rule.name === 'BANG_QSTR') { return input.replace('))"', '').replace('"((', '').trim(); + } else if (rule.name === 'HEXAGON_STR') { + return input.replace('{{', '').replace('}}', '').trim(); + } else if (rule.name === 'HEXAGON_QSTR') { + return input.replace('{{"', '').replace('"}}', '').trim(); } else if (rule.name === 'CLOUD_STR') { return input.replace(')', '').replace('(', '').trim(); - } else if (rule.name === 'CLOUD_STR_QUOTES') { + } else if (rule.name === 'CLOUD_QSTR') { 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(); + } else if (rule.name === 'CLASS') { + return input.replace(':::', '').trim(); + } else if (rule.name === 'ICON') { + return input.replace('::icon(', '').replace(')', '').trim(); } else if (rule.name === 'INDENTATION') { return input.length; } diff --git a/packages/parser/tests/mindmap-poc.test.ts b/packages/parser/tests/mindmap-poc.test.ts new file mode 100644 index 000000000..587b8af40 --- /dev/null +++ b/packages/parser/tests/mindmap-poc.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest'; +import { validatedMindmapParse as validatedParse, mindmapParse as parse } from './test-util.js'; +import type { CircleNode, SimpleNode, OtherComplex } from '../src/language/generated/ast.js'; + +describe('Nodes (ported from mindmap.spec.ts)', () => { + it('MMP-21 should be possible to have comments in a mindmap', () => { + const result = parse( + 'mindmap\nroot(Root)\n Child(Child)\n a(a)\n\n %% This is a comment\n b[New Stuff]' + ); + expect(result.lexerErrors).toHaveLength(0); + console.debug(result.parserErrors); + expect(result.parserErrors).toHaveLength(0); + const rootNode = result.value.MindmapRows[0].item as OtherComplex; + const childNode = result.value.MindmapRows[1].item as OtherComplex; + const aNode = result.value.MindmapRows[2].item as OtherComplex; + const bNode = result.value.MindmapRows[3].item as OtherComplex; + expect(rootNode.desc).toBe('Root'); + expect(childNode.desc).toBe('Child'); + expect(aNode.desc).toBe('a'); + expect(bNode.desc).toBe('New Stuff'); + }); +}); diff --git a/packages/parser/tests/mindmap.test.ts b/packages/parser/tests/mindmap.test.ts index 5b11426a2..7d6f0f8d4 100644 --- a/packages/parser/tests/mindmap.test.ts +++ b/packages/parser/tests/mindmap.test.ts @@ -174,7 +174,7 @@ describe('Nodes (ported from mindmap.spec.ts)', () => { expect(childNode.desc).toBe('child1'); }); - it.only('MMP-9 should handle an id and type for a node definition', () => { + it('MMP-9 should handle an id and type for a node definition', () => { const result = parse('mindmap\nroot\n theId(child1)'); expect(result.lexerErrors).toHaveLength(0); expect(result.parserErrors).toHaveLength(0); @@ -256,6 +256,51 @@ describe('Decorations (ported from mindmap.spec.ts)', () => { const rootNode = result.value.MindmapRows[0].item as OtherComplex; expect(rootNode.desc).toBe('The root'); }); + it('MMP-16.2 should handle an id and type for a node definition', () => { + const result = parse(`mindmap +id1[SquareNode I am] + id2["SquareNode I am"] + id3("RoundedNode I am") + id4(RoundedNode I am) + id5(("CircleNode I am")) + id6((CircleNode I am)) + id7))BangNode I am(( + id8))"BangNode I am"(( + id9)"CloudNode I am"( + id10)CloudNode I am( + id11{{"HexagonNode I am"}} + id12{{HexagonNode I am}} + id13 +`); + + expect(result.lexerErrors).toHaveLength(0); + expect(result.parserErrors).toHaveLength(0); + expect(result.value.MindmapRows).toHaveLength(13); + expect(result.value.MindmapRows[0].item.$type).toBe('SquareNode'); + expect(result.value.MindmapRows[1].item.$type).toBe('SquareNode'); + expect(result.value.MindmapRows[2].item.$type).toBe('RoundedNode'); + expect(result.value.MindmapRows[3].item.$type).toBe('RoundedNode'); + expect(result.value.MindmapRows[4].item.$type).toBe('CircleNode'); + expect(result.value.MindmapRows[5].item.$type).toBe('CircleNode'); + expect(result.value.MindmapRows[6].item.$type).toBe('BangNode'); + expect(result.value.MindmapRows[7].item.$type).toBe('BangNode'); + expect(result.value.MindmapRows[8].item.$type).toBe('CloudNode'); + expect(result.value.MindmapRows[9].item.$type).toBe('CloudNode'); + expect(result.value.MindmapRows[10].item.$type).toBe('HexagonNode'); + expect(result.value.MindmapRows[11].item.$type).toBe('HexagonNode'); + expect(result.value.MindmapRows[12].item.$type).toBe('SimpleNode'); + + let id = 1; + for (const row of result.value.MindmapRows as MindmapRow[]) { + const item = row.item as Node; + expect(item.id).toBeDefined(); + expect(item?.id).toBe('id' + id); + if (item.id !== 'id13') { + expect(item.desc).toBe(item.$type + ' I am'); + } + id++; + } + }); }); describe('Descriptions (ported from mindmap.spec.ts)', () => {