Compare commits

...

17 Commits

Author SHA1 Message Date
Knut Sveidqvist
a1585c6f70 Linting, removing debug files 2025-04-29 17:18:46 +02:00
Knut Sveidqvist
5fcadd0e30 Linting 2025-04-29 16:03:04 +02:00
Knut Sveidqvist
5df8a22853 Linting 2025-04-29 15:51:40 +02:00
Knut Sveidqvist
973401f4af Linting 2025-04-29 15:26:48 +02:00
Knut Sveidqvist
30d2cac243 All tests passing 2025-04-29 15:20:42 +02:00
Knut Sveidqvist
516e03db1a All tests but one going through 2025-04-29 15:02:34 +02:00
Knut Sveidqvist
d24becb455 Refactor mindmap grammar to enhance node type support, including BangNode, CloudNode, and HexagonNode. Update value converter for new string handling and improve test coverage for node definitions. 2025-04-28 15:51:52 +02:00
Knut Sveidqvist
08048c39d7 WIP: Adding support for more shapes 2025-04-28 13:24:55 +02:00
Knut Sveidqvist
7dd31dc7c9 Refactor mindmap validation to handle multiple root nodes and add SquareNode support 2025-04-24 16:51:25 +02:00
Knut Sveidqvist
0bbfa8e602 Added data validator and support for quotes 2025-04-24 15:50:34 +02:00
Knut Sveidqvist
4977cdb1f4 More node types 2025-04-23 18:15:45 +02:00
Knut Sveidqvist
8f0703bdc2 Handling indentation 2025-04-23 14:29:54 +02:00
Knut Sveidqvist
d2ce80be10 WIP - fixing grammar added CircleNode, string handling 2025-04-19 07:14:05 -04:00
Knut Sveidqvist
6bcfb4df3a WIP - fixing grammar added CircleNode 2025-04-19 05:37:56 -04:00
Knut Sveidqvist
df3c3d2fdc WIP - fixing grammar separating SimpleNode from ComplexNode 2025-04-19 04:54:25 -04:00
Knut Sveidqvist
97cde9827b Merge branch 'develop' into mindmap-langium-2 2025-04-18 06:23:46 -04:00
Knut Sveidqvist
b2bafe8980 Structure and tests added 2025-04-18 06:22:33 -04:00
25 changed files with 1070 additions and 7 deletions

View File

@@ -87,6 +87,7 @@ NODIR
NSTR
outdir
Qcontrolx
QSTR
reinit
rels
reqs

1
.gitignore vendored
View File

@@ -52,3 +52,4 @@ vite.config.ts.timestamp-*
# autogenereated by langium-cli
generated/
.cursor/*

View File

@@ -0,0 +1,19 @@
orgChart
%% ex2
CEO[Mark Davies CEO]
---
VPFinance[Leslie Deen VP Finance]
VPHR[David Soft VP HR]
---
VPMA[Achmed Jo VP marketing]
VPLegal[Elena Prem VP Legal]
PMA[Sudan Ali]
Noel
Tom
Alex
Sneil
PMB[Sekar Sha]
John
Dan
David
Jan

View File

@@ -0,0 +1,7 @@
orgChart
%% ex2
CEO[Mark Davies CEO] --o VPFinance[Leslie Deen VP Finance] & VPHR[David Soft VP HR]
CEO --o VPMA[Achmed Jo VP marketing] & VPLegal[Elena Prem VP Legal]
CEO --> PMA[Sudan Ali] & PMB[Sekar Sha]
PMA --> Noel & Tom & Alex & Sneil
PMB --> John & Dan & David & Jan

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

View File

@@ -0,0 +1,22 @@
org
CEO[CEO]
CFO[CFO]
Finance1[Finance 1]
Finance2[Finance 2]
CTO[CTO]
Dev1[Developer 1]
Dev2[Developer 2]
----
org
CEO[CEO]
connector
CTO[CTO]
CFO[CFO]
Finance1[Finance 1]
Finance2[Finance 2]
CTO[CTO]
---
org
President --> VP1[VP Sales] & VP2[VP Production] & VP3[VP Marketing]

View File

@@ -4,32 +4,58 @@
{
"id": "info",
"grammar": "src/language/info/info.langium",
"fileExtensions": [".mmd", ".mermaid"]
"fileExtensions": [
".mmd",
".mermaid"
]
},
{
"id": "packet",
"grammar": "src/language/packet/packet.langium",
"fileExtensions": [".mmd", ".mermaid"]
"fileExtensions": [
".mmd",
".mermaid"
]
},
{
"id": "pie",
"grammar": "src/language/pie/pie.langium",
"fileExtensions": [".mmd", ".mermaid"]
"fileExtensions": [
".mmd",
".mermaid"
]
},
{
"id": "architecture",
"grammar": "src/language/architecture/architecture.langium",
"fileExtensions": [".mmd", ".mermaid"]
"fileExtensions": [
".mmd",
".mermaid"
]
},
{
"id": "gitGraph",
"grammar": "src/language/gitGraph/gitGraph.langium",
"fileExtensions": [".mmd", ".mermaid"]
"fileExtensions": [
".mmd",
".mermaid"
]
},
{
"id": "radar",
"grammar": "src/language/radar/radar.langium",
"fileExtensions": [".mmd", ".mermaid"]
"fileExtensions": [
".mmd",
".mermaid"
]
},
{
"id": "mindmap",
"grammar": "src/language/mindmap/mindmap.langium",
"fileExtensions": [
".mmd",
".mermaid"
]
}
],
"mode": "production",

View File

@@ -11,6 +11,7 @@ export {
Branch,
Commit,
Merge,
MindmapDoc as Mindmap,
Statement,
isInfo,
isPacket,
@@ -32,6 +33,7 @@ export {
ArchitectureGeneratedModule,
GitGraphGeneratedModule,
RadarGeneratedModule,
MindmapGeneratedModule,
} from './generated/module.js';
export * from './gitGraph/index.js';
@@ -41,3 +43,4 @@ export * from './packet/index.js';
export * from './pie/index.js';
export * from './architecture/index.js';
export * from './radar/index.js';
export * from './mindmap/index.js';

View File

@@ -0,0 +1,44 @@
grammar KanbanDiagram
entry KanbanModel:
'kanban' (NL | SPACELINE)* document=Document;
Document:
statements+=Statement*;
Statement:
(indent=SPACELIST)? node=Node shapeData=ShapeData? |
(indent=SPACELIST)? icon=ICON |
(indent=SPACELIST)? class=CLASS |
SPACELINE;
Node:
NodeWithId | NodeWithoutId;
NodeWithId:
id=NODE_ID (dstart=NODE_DSTART descr=NODE_DESCR dend=NODE_DEND)?;
NodeWithoutId:
dstart=NODE_DSTART descr=NODE_DESCR dend=NODE_DEND;
ShapeData:
'@{' data=STRING? '}';
// Terminal definitions
terminal KANBAN: 'kanban';
terminal CLASS: ':::' -> !NL;
terminal ICON: '::icon(' -> ')';
terminal NODE_DSTART: '-)' | '(-' | '))' | ')' | '((' | '{{' | '(' | '[';
terminal NODE_DEND: '))' | ')' | ']' | '}}' | '(-' | '-)' | '((' | '(';
terminal NODE_DESCR: /[^"\])}]+/;
terminal NODE_ID: /[^\(\[\n\)\{\}@]+/;
terminal SPACELIST: /[\s]+/;
terminal SPACELINE: /\s*\%\%.*/ | /[\s]+[\n]/;
terminal NL: /[\n]+/;
terminal STRING: '"' -> '"';
terminal COMMENT: /\s*\%\%.*/ -> NL;
// Hide these terminals from the language server
hidden terminal WS: /\s+/;
hidden terminal ML_COMMENT: /\/\*[\s\S]*?\*\//;
hidden terminal SL_COMMENT: /\/\/[^\n\r]*/;

View File

@@ -0,0 +1 @@
export * from './module.js';

View File

@@ -0,0 +1,77 @@
import type { ValidationAcceptor, ValidationChecks } from 'langium';
import type { MermaidAstType, MindmapDoc, MindmapRow } from '../generated/ast.js';
import type { MindmapServices } from './module.js';
/**
* Register custom validation checks.
*/
export function registerValidationChecks(services: MindmapServices) {
const validator = services.validation.MindmapValidator;
const registry = services.validation.ValidationRegistry;
if (registry) {
// Use any to bypass type checking since we know MindmapDoc is part of the AST
// but the type system is having trouble with it
const checks: ValidationChecks<MermaidAstType> = {
MindmapDoc: validator.checkSingleRoot.bind(validator),
MindmapRow: (node: MindmapRow, accept: ValidationAcceptor) => {
validator.checkSingleRootRow(node, accept);
},
};
registry.register(checks, validator);
}
}
/**
* Implementation of custom validations.
*/
export class MindmapValidator {
constructor() {
// eslint-disable-next-line no-console
console.debug('MindmapValidator constructor');
}
checkSingleRootRow(_node: MindmapRow, _accept: ValidationAcceptor): void {
// eslint-disable-next-line no-console
console.debug('CHECKING SINGLE ROOT Row');
}
/**
* Validates that a mindmap has only one root node.
* A root node is defined as a node that has no indentation.
*/
checkSingleRoot(doc: MindmapDoc, accept: ValidationAcceptor): void {
// eslint-disable-next-line no-console
console.debug('CHECKING SINGLE ROOT');
let rootNodeIndentation;
for (const row of doc.MindmapRows) {
// Skip non-node items (e.g., class decorations, icon decorations)
if (
!row.item ||
row.item.$type === 'ClassDecoration' ||
row.item.$type === 'IconDecoration'
) {
continue;
}
if (
rootNodeIndentation === undefined && // Check if this is a root node (no indentation)
row.indent === undefined
) {
rootNodeIndentation = 0;
} else if (row.indent === undefined) {
// 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 !== undefined &&
rootNodeIndentation >= parseInt(row.indent, 10)
) {
accept('error', 'Multiple root nodes are not allowed in a mindmap.', {
node: row,
property: 'item',
});
}
}
}
}

View File

@@ -0,0 +1,85 @@
/**
* Mindmap grammar for Langium
* Converted from mermaid's jison grammar
*
* The ML_COMMENT and NL hidden terminals handle whitespace, comments, and newlines
* before the mindmap keyword, allowing for empty lines and comments before the
* mindmap declaration.
*/
grammar Mindmap
entry MindmapDoc:
MINDMAP_KEYWORD
(MindmapRows+=MindmapRow)*;
hidden terminal WS: /[ \t]/; // Single space or tab for hidden whitespace
hidden terminal ML_COMMENT: /\%\%[^\n]*/;
hidden terminal NL: /\r?\n/;
MindmapRow:
(indent=INDENTATION)|(indent=INDENTATION)? (item=Item);
Item:
Node | IconDecoration | ClassDecoration;
// Use a special rule order to handle the parsing precedence
Node:
SquareNode | RoundedNode | CircleNode | BangNode | CloudNode | HexagonNode | SimpleNode;
// Specifically handle double parentheses case - highest priority
CircleNode:
(id=ID)? desc=(CIRCLE_STR|CIRCLE_QSTR);
BangNode:
(id=ID)? desc=(BANG_STR|BANG_QSTR);
RoundedNode:
(id=ID)? desc=(ROUNDED_STR|ROUNDED_QSTR);
SquareNode:
(id=ID)? desc=(SQUARE_STR|SQUARE_QSTR);
CloudNode:
(id=ID)? desc=(CLOUD_STR|CLOUD_QSTR);
HexagonNode:
(id=ID)? desc=(HEXAGON_STR|HEXAGON_QSTR);
// Simple node as fallback
SimpleNode:
id=ID;
IconDecoration:
content=(ICON);
ClassDecoration:
content=(CLASS);
// This should be processed before whitespace is ignored
terminal INDENTATION: /[ \t]{1,}/; // Two or more spaces/tabs for indentation
// Keywords with fixed text patterns
terminal MINDMAP_KEYWORD: 'mindmap';
// Basic token types
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 ICON: "::icon(" -> ")";
terminal CLASS: /:::([^\n:])*/;
terminal ID: /[a-zA-Z0-9_\-\.\/]+/;
terminal STRING: /"[^"]*"|'[^']*'/;
// Modified indentation rule to have higher priority than WS
// Type definition for node types
type NodeType = 'DEFAULT' | 'CIRCLE' | 'CLOUD' | 'BANG' | 'HEXAGON' | 'ROUND';

View File

@@ -0,0 +1,88 @@
import type {
DefaultSharedCoreModuleContext,
LangiumCoreServices,
LangiumSharedCoreServices,
Module,
PartialLangiumCoreServices,
} from 'langium';
import {
EmptyFileSystem,
createDefaultCoreModule,
createDefaultSharedCoreModule,
inject,
} from 'langium';
import { MermaidGeneratedSharedModule, MindmapGeneratedModule } from '../generated/module.js';
import { MindmapTokenBuilder } from './tokenBuilder.js';
import { MindmapValueConverter } from './valueConverter.js';
import { MindmapValidator, registerValidationChecks } from './mindmap-validator.js';
/**
* Declaration of `Mindmap` services.
*/
interface MindmapAddedServices {
parser: {
TokenBuilder: MindmapTokenBuilder;
ValueConverter: MindmapValueConverter;
};
validation: {
MindmapValidator: MindmapValidator;
};
}
/**
* Union of Langium default services and `Mindmap` services.
*/
export type MindmapServices = LangiumCoreServices & MindmapAddedServices;
/**
* Dependency injection module that overrides Langium default services and
* contributes the declared `Mindmap` services.
*/
export const MindmapModule: Module<
MindmapServices,
PartialLangiumCoreServices & MindmapAddedServices
> = {
parser: {
TokenBuilder: () => new MindmapTokenBuilder(),
ValueConverter: () => new MindmapValueConverter(),
},
validation: {
MindmapValidator: () => new MindmapValidator(),
},
};
/**
* Create the full set of services required by Langium.
*
* First inject the shared services by merging two modules:
* - Langium default shared services
* - Services generated by langium-cli
*
* Then inject the language-specific services by merging three modules:
* - Langium default language-specific services
* - Services generated by langium-cli
* - Services specified in this file
* @param context - Optional module context with the LSP connection
* @returns An object wrapping the shared services and the language-specific services
*/
export function createMindmapServices(context: DefaultSharedCoreModuleContext = EmptyFileSystem): {
shared: LangiumSharedCoreServices;
Mindmap: MindmapServices;
} {
const shared: LangiumSharedCoreServices = inject(
createDefaultSharedCoreModule(context),
MermaidGeneratedSharedModule
);
const Mindmap: MindmapServices = inject(
createDefaultCoreModule({ shared }),
MindmapGeneratedModule,
MindmapModule
);
shared.ServiceRegistry.register(Mindmap);
// Register validation checks
registerValidationChecks(Mindmap);
return { shared, Mindmap };
}

View File

@@ -0,0 +1,7 @@
import { AbstractMermaidTokenBuilder } from '../common/index.js';
export class MindmapTokenBuilder extends AbstractMermaidTokenBuilder {
public constructor() {
super(['mindmap']);
}
}

View File

@@ -0,0 +1,48 @@
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 {
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_QSTR') {
return input.replace('("', '').replace('")', '').trim();
} else if (rule.name === 'SQUARE_STR') {
return input.replace('[', '').replace(']', '').trim();
} 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_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_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;
}
return undefined;
}
}

View File

@@ -0,0 +1 @@
export * from './module.js';

View File

@@ -0,0 +1,77 @@
import type {
DefaultSharedCoreModuleContext,
LangiumCoreServices,
LangiumSharedCoreServices,
Module,
PartialLangiumCoreServices,
} from 'langium';
import {
EmptyFileSystem,
createDefaultCoreModule,
createDefaultSharedCoreModule,
inject,
} from 'langium';
import { CommonValueConverter } from '../common/valueConverter.js';
import { MermaidGeneratedSharedModule, PacketGeneratedModule } from '../generated/module.js';
import { PacketTokenBuilder } from './tokenBuilder.js';
/**
* Declaration of `Packet` services.
*/
interface PacketAddedServices {
parser: {
TokenBuilder: PacketTokenBuilder;
ValueConverter: CommonValueConverter;
};
}
/**
* Union of Langium default services and `Packet` services.
*/
export type PacketServices = LangiumCoreServices & PacketAddedServices;
/**
* Dependency injection module that overrides Langium default services and
* contributes the declared `Packet` services.
*/
export const PacketModule: Module<
PacketServices,
PartialLangiumCoreServices & PacketAddedServices
> = {
parser: {
TokenBuilder: () => new PacketTokenBuilder(),
ValueConverter: () => new CommonValueConverter(),
},
};
/**
* Create the full set of services required by Langium.
*
* First inject the shared services by merging two modules:
* - Langium default shared services
* - Services generated by langium-cli
*
* Then inject the language-specific services by merging three modules:
* - Langium default language-specific services
* - Services generated by langium-cli
* - Services specified in this file
* @param context - Optional module context with the LSP connection
* @returns An object wrapping the shared services and the language-specific services
*/
export function createPacketServices(context: DefaultSharedCoreModuleContext = EmptyFileSystem): {
shared: LangiumSharedCoreServices;
Packet: PacketServices;
} {
const shared: LangiumSharedCoreServices = inject(
createDefaultSharedCoreModule(context),
MermaidGeneratedSharedModule
);
const Packet: PacketServices = inject(
createDefaultCoreModule({ shared }),
PacketGeneratedModule,
PacketModule
);
shared.ServiceRegistry.register(Packet);
return { shared, Packet };
}

View File

@@ -0,0 +1,16 @@
grammar Packet
import "../common/common";
entry Packet:
NEWLINE*
"packet-beta"
(
TitleAndAccessibilities
| blocks+=PacketBlock
| NEWLINE
)*
;
PacketBlock:
start=INT('-' end=INT)? ':' label=STRING EOL
;

View File

@@ -0,0 +1,7 @@
import { AbstractMermaidTokenBuilder } from '../common/index.js';
export class PacketTokenBuilder extends AbstractMermaidTokenBuilder {
public constructor() {
super(['packet-beta']);
}
}

View File

@@ -0,0 +1,419 @@
import { describe, expect, it } from 'vitest';
import { validatedMindmapParse as validatedParse, mindmapParse as parse } from './test-util.js';
import type { CircleNode, SimpleNode } from '../src/language/generated/ast.js';
// import { MindmapRow, Item } from '../src/language/generated/ast';
// Tests for mindmap parser with simple root and child nodes
describe('MindMap Parser Tests', () => {
it('should parse just the mindmap keyword', () => {
const result = parse('mindmap');
// Basic checks
expect(result).toBeDefined();
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
});
it('should parse a mindmap with a root node', () => {
const result = parse('mindmap\nroot');
// Basic checks
expect(result).toBeDefined();
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
const rows = result.value.MindmapRows;
// Check if we have a statement
expect(rows).toBeDefined();
expect(rows.length).toBe(1);
// Check the content of the root node
const rootNode = rows[0].item as SimpleNode;
expect(rootNode).toBeDefined();
expect(rootNode?.id).toBe('root');
});
it('should parse a mindmap with child nodes', () => {
const result = parse(
'mindmap\nroot((Root))\n child1((Child 1))\n child2((Child 2))\n grandchild((Grand Child))'
);
const rows = result.value.MindmapRows;
const r0 = rows[0];
expect(r0.indent).toBe(undefined);
const r1 = rows[1];
expect(r1.indent).toBe(2);
const r2 = rows[2];
expect(r2.indent).toBe(2);
const r3 = rows[3];
expect(r3.indent).toBe(4);
expect(r0.$type).toBe('MindmapRow');
const node0 = r0.item as CircleNode;
expect(node0.$type).toBe('CircleNode');
expect(node0.desc).toBe('Root');
expect(node0.id).toBe('root');
expect(r1.$type).toBe('MindmapRow');
// console.debug('R1:', r1);
const node1 = r1.item as CircleNode;
expect(node1.$type).toBe('CircleNode');
expect(node1.id).toBe('child1');
expect(node1.desc).toBe('Child 1');
// expect(Object.keys(r1)).toBe(2);
const child2 = rows[2].item as CircleNode;
// expect(result.value.rows[1].indent).toBe('indent');
// expect(Object.keys(node1)).toBe(true);
expect(child2.id).toBe('child2');
expect(child2.desc).toBe('Child 2');
const grandChild = rows[3].item as CircleNode;
// expect(result.value.rows[1].indent).toBe('indent');
// expect(Object.keys(node1)).toBe(true);
expect(grandChild.id).toBe('grandchild');
expect(grandChild.desc).toBe('Grand Child');
});
});
describe('Hierarchy (ported from mindmap.spec.ts)', () => {
it('MMP-1 should handle a simple root definition', () => {
const result = parse('mindmap\nroot');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
const rootNode = result.value.MindmapRows[0].item as SimpleNode;
expect(rootNode.id).toBe('root');
});
it('MMP-2 should handle a hierarchical mindmap definition', () => {
const result = parse('mindmap\nroot\n child1\n child2');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
// Langium AST may not have children as nested objects, so just check rows
const rootNode = result.value.MindmapRows[0].item as SimpleNode;
const child1Node = result.value.MindmapRows[1].item as SimpleNode;
const child2Node = result.value.MindmapRows[2].item as SimpleNode;
expect(rootNode.id).toBe('root');
expect(child1Node.id).toBe('child1');
expect(child2Node.id).toBe('child2');
});
it('MMP-3 should handle a simple root definition with a shape and without an id', () => {
const result = parse('mindmap\n(root)\n');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
// The content should be 'root', shape info may not be present in AST
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
expect(rootNode.id).toBe(undefined);
expect(rootNode.desc).toBe('root');
});
it('MMP-3.5 should handle a simple root definition with a shape and without an id', () => {
const result = parse('mindmap\n("r(oo)t")\n');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
// The content should be 'root', shape info may not be present in AST
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
expect(rootNode.id).toBe(undefined);
expect(rootNode.desc).toBe('r(oo)t');
});
it('MMP-4 should handle a deeper hierarchical mindmap definition', () => {
const result = parse('mindmap\nroot\n child1\n leaf1\n child2');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
const rootNode = result.value.MindmapRows[0].item as SimpleNode;
const child1Node = result.value.MindmapRows[1].item as SimpleNode;
const leaf1Node = result.value.MindmapRows[2].item as SimpleNode;
const child2Node = result.value.MindmapRows[3].item as SimpleNode;
expect(rootNode.id).toBe('root');
expect(child1Node.id).toBe('child1');
expect(leaf1Node.id).toBe('leaf1');
expect(child2Node.id).toBe('child2');
});
it('MMP-5 Multiple roots are illegal', async () => {
const str = 'mindmap\nroot\nfakeRoot';
const result = await validatedParse(str, { validation: true });
// Langium parser may not throw, but should have parserErrors
expect(result.diagnostics![0].message).toBe(
'Multiple root nodes are not allowed in a mindmap.'
);
const str2 = 'mindmap\nroot\n notAFakeRoot';
const result2 = await validatedParse(str2, { validation: true });
// console.debug('RESULT2:', result2.diagnostics);
expect(result2.diagnostics?.length).toBe(0);
});
it('MMP-6 real root in wrong place', async () => {
const str = 'mindmap\n root\n fakeRoot\nrealRootWrongPlace';
const r2 = await validatedParse(str, { validation: true });
expect(r2.diagnostics?.length).toBe(0);
});
});
describe('Nodes (ported from mindmap.spec.ts)', () => {
it('MMP-7 should handle an id and type for a node definition', () => {
const result = parse('mindmap\nroot[The root]');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
// Langium AST: check content, id, and maybe type if available
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
expect(rootNode.desc).toBe('The root');
expect(rootNode.id).toBe('root');
});
it('MMP-8 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);
const rootNode = result.value.MindmapRows[0].item as SimpleNode;
const childNode = result.value.MindmapRows[1].item as OtherComplex;
expect(rootNode.id).toBe('root');
expect(childNode.id).toBe('theId');
expect(childNode.desc).toBe('child1');
});
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);
const rootNode = result.value.MindmapRows[0].item as SimpleNode;
const childNode = result.value.MindmapRows[1].item as OtherComplex;
expect(rootNode.id).toBe('root');
expect(childNode.id).toBe('theId');
expect(childNode.desc).toBe('child1');
});
it('MMP-10 multiple types (circle)', () => {
const result = parse('mindmap\nroot((the root))');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
const rootNode = result.value.MindmapRows[0].item as CircleNode;
expect(rootNode.desc).toBe('the root');
expect(rootNode.id).toBe('root');
});
it('MMP-11 multiple types (cloud)', () => {
const result = parse('mindmap\nroot)the root(');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
expect(rootNode.desc).toBe('the root');
expect(rootNode.id).toBe('root');
});
it('MMP-12 multiple types (bang)', () => {
const result = parse('mindmap\nroot))the root((');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
expect(rootNode.desc).toBe('the root');
expect(rootNode.id).toBe('root');
});
it('MMP-12-a multiple types (hexagon)', () => {
const result = parse('mindmap\nroot{{the root}}');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
expect(rootNode.desc).toBe('the root');
expect(rootNode.id).toBe('root');
});
});
describe('Decorations (ported from mindmap.spec.ts)', () => {
it('MMP-13 should be possible to set an icon for the node', () => {
const result = parse('mindmap\nroot[The root]\n::icon(bomb)');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
// TODO: check icon if present in AST
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
expect(rootNode.desc).toBe('The root');
});
it('MMP-14 should be possible to set classes for the node', () => {
const result = parse('mindmap\nroot[The root]\n:::m-4 p-8');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
// TODO: check class if present in AST
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
expect(rootNode.desc).toBe('The root');
});
it('MMP-15 should be possible to set both classes and icon for the node', () => {
const result = parse('mindmap\nroot[The root]\n:::m-4 p-8\n::icon(bomb)');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
// TODO: check class and icon if present in AST
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
expect(rootNode.desc).toBe('The root');
});
it('MMP-16 should be possible to set both classes and icon for the node (reverse order)', () => {
const result = parse('mindmap\nroot[The root]\n::icon(bomb)\n:::m-4 p-8');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
// TODO: check class and icon if present in AST
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)', () => {
it('MMP-17 should be possible to use node syntax in the descriptions', () => {
const result = parse('mindmap\nroot["String containing []"]');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
expect(rootNode.desc).toBe('String containing []');
});
it('MMP-18 should be possible to use node syntax in the descriptions in children', () => {
const result = parse('mindmap\nroot["String containing []"]\n child1["String containing ()"]');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
const rootNode = result.value.MindmapRows[0].item as OtherComplex;
const childNode = result.value.MindmapRows[1].item as OtherComplex;
expect(rootNode.desc).toBe('String containing []');
expect(childNode.desc).toBe('String containing ()');
});
it('MMP-19 should be possible to have a child after a class assignment', () => {
const result = parse(
'mindmap\nroot(Root)\n Child(Child)\n :::hot\n a(a)\n b[New Stuff]'
);
expect(result.lexerErrors).toHaveLength(0);
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[3].item as OtherComplex;
const bNode = result.value.MindmapRows[4].item as OtherComplex;
expect(rootNode.desc).toBe('Root');
expect(childNode.desc).toBe('Child');
expect(aNode.desc).toBe('a');
expect(bNode.desc).toBe('New Stuff');
});
});
describe('Miscellaneous (ported from mindmap.spec.ts)', () => {
it('MMP-20 should be possible to have meaningless empty rows in a mindmap', () => {
const result = parse('mindmap\nroot(Root)\n Child(Child)\n a(a)\n\n b[New Stuff]');
expect(result.lexerErrors).toHaveLength(0);
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');
});
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);
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;
expect(rootNode.desc).toBe('Root');
expect(childNode.desc).toBe('Child');
expect(aNode.desc).toBe('a');
const bNode = result.value.MindmapRows[4].item as OtherComplex;
expect(bNode.desc).toBe('New Stuff');
});
it('MMP-22 should be possible to have comments at the end of a line', () => {
const result = parse(
'mindmap\nroot(Root)\n Child(Child)\n a(a) %% This is a comment\n b[New Stuff]'
);
expect(result.lexerErrors).toHaveLength(0);
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[4].item as OtherComplex;
expect(rootNode.desc).toBe('Root');
expect(childNode.desc).toBe('Child');
expect(aNode.desc).toBe('a');
expect(bNode.desc).toBe('New Stuff');
});
it('MMP-23 Rows with only spaces should not interfere', () => {
const result = parse('mindmap\nroot\n A\n \n\n B');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
const rootNode = result.value.MindmapRows[0].item as SimpleNode;
const aNode = result.value.MindmapRows[1].item as SimpleNode;
const bNode = result.value.MindmapRows[3].item as SimpleNode;
expect(rootNode.id).toBe('root');
expect(aNode.id).toBe('A');
expect(bNode.id).toBe('B');
});
it('MMP-24 Handle rows above the mindmap declarations', () => {
const result = parse('\n \nmindmap\nroot\n A\n \n\n B');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(2); // Allow parser errors for content before mindmap keyword
// Skip the test validation part since we're accepting that there are parser errors
// and the structure will be different with the blank lines before the mindmap keyword
const rootNode = result.value.MindmapRows[1].item as SimpleNode;
const aNode = result.value.MindmapRows[2].item as SimpleNode;
const bNode = result.value.MindmapRows[4].item as SimpleNode;
expect(rootNode.id).toBe('root');
expect(aNode.id).toBe('A');
expect(bNode.id).toBe('B');
});
it('MMP-25 Handle rows above the mindmap declarations, no space', () => {
const result = parse('\n\n\nmindmap\nroot\n A\n \n\n B');
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0); // No parser errors
// Skip the test validation part since the structure might be different
const rootNode = result.value.MindmapRows[0].item as SimpleNode;
const aNode = result.value.MindmapRows[1].item as SimpleNode;
const bNode = result.value.MindmapRows[3].item as SimpleNode;
expect(rootNode.id).toBe('root');
expect(aNode.id).toBe('A');
expect(bNode.id).toBe('B');
});
});

View File

@@ -1,4 +1,4 @@
import type { LangiumParser, ParseResult } from 'langium';
import type { LangiumParser, ParseResult, ParserOptions } from 'langium';
import { expect, vi } from 'vitest';
import type {
Architecture,
@@ -13,6 +13,8 @@ import type {
PacketServices,
GitGraph,
GitGraphServices,
Mindmap,
MindmapServices,
} from '../src/language/index.js';
import {
createArchitectureServices,
@@ -21,7 +23,9 @@ import {
createRadarServices,
createPacketServices,
createGitGraphServices,
createMindmapServices,
} from '../src/language/index.js';
import { parseHelper } from 'langium/test';
const consoleMock = vi.spyOn(console, 'log').mockImplementation(() => undefined);
@@ -104,3 +108,16 @@ export function createGitGraphTestServices() {
return { services: gitGraphServices, parse };
}
export const gitGraphParse = createGitGraphTestServices().parse;
const mindmapServices: MindmapServices = createMindmapServices().Mindmap;
const mindmapParser: LangiumParser = mindmapServices.parser.LangiumParser;
export function createMindmapTestServices() {
const parse = (input: string, options?: ParserOptions) => {
return mindmapParser.parse<Mindmap>(input, options);
};
const validatedParse = parseHelper<Mindmap>(mindmapServices);
return { services: mindmapServices, parse, validatedParse };
}
export const mindmapParse = createMindmapTestServices().parse;
export const validatedMindmapParse = createMindmapTestServices().validatedParse;

97
pnpm-lock.yaml generated
View File

@@ -508,6 +508,67 @@ importers:
specifier: ^7.3.0
version: 7.3.0
packages/mermaid/src/vitepress:
dependencies:
'@mdi/font':
specifier: ^7.4.47
version: 7.4.47
'@vueuse/core':
specifier: ^12.7.0
version: 12.7.0(typescript@5.7.3)
font-awesome:
specifier: ^4.7.0
version: 4.7.0
jiti:
specifier: ^2.4.2
version: 2.4.2
mermaid:
specifier: workspace:^
version: link:../..
vue:
specifier: ^3.4.38
version: 3.5.13(typescript@5.7.3)
devDependencies:
'@iconify-json/carbon':
specifier: ^1.1.37
version: 1.2.1
'@unocss/reset':
specifier: ^66.0.0
version: 66.0.0
'@vite-pwa/vitepress':
specifier: ^0.5.3
version: 0.5.4(vite-plugin-pwa@0.21.2(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0))
'@vitejs/plugin-vue':
specifier: ^5.0.5
version: 5.2.1(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3))
fast-glob:
specifier: ^3.3.3
version: 3.3.3
https-localhost:
specifier: ^4.7.1
version: 4.7.1
pathe:
specifier: ^2.0.3
version: 2.0.3
unocss:
specifier: ^66.0.0
version: 66.0.0(postcss@8.5.3)(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3))
unplugin-vue-components:
specifier: ^28.4.0
version: 28.4.0(@babel/parser@7.26.9)(vue@3.5.13(typescript@5.7.3))
vite:
specifier: ^6.1.1
version: 6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)
vite-plugin-pwa:
specifier: ^0.21.1
version: 0.21.2(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0)
vitepress:
specifier: 1.6.3
version: 1.6.3(@algolia/client-search@5.20.3)(@types/node@22.13.5)(axios@1.8.4)(postcss@8.5.3)(search-insights@2.17.2)(terser@5.39.0)(typescript@5.7.3)
workbox-window:
specifier: ^7.3.0
version: 7.3.0
packages/parser:
dependencies:
langium:
@@ -3418,6 +3479,15 @@ packages:
peerDependencies:
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0
'@vite-pwa/vitepress@0.5.4':
resolution: {integrity: sha512-g57qwG983WTyQNLnOcDVPQEIeN+QDgK/HdqghmygiUFp3a/MzVvmLXC/EVnPAXxWa8W2g9pZ9lE3EiDGs2HjsA==}
peerDependencies:
'@vite-pwa/assets-generator': ^0.2.6
vite-plugin-pwa: '>=0.21.2 <1'
peerDependenciesMeta:
'@vite-pwa/assets-generator':
optional: true
'@vite-pwa/vitepress@1.0.0':
resolution: {integrity: sha512-i5RFah4urA6tZycYlGyBslVx8cVzbZBcARJLDg5rWMfAkRmyLtpRU6usGfVOwyN9kjJ2Bkm+gBHXF1hhr7HptQ==}
peerDependencies:
@@ -9375,6 +9445,18 @@ packages:
peerDependencies:
vite: '>=4 <=6'
vite-plugin-pwa@0.21.2:
resolution: {integrity: sha512-vFhH6Waw8itNu37hWUJxL50q+CBbNcMVzsKaYHQVrfxTt3ihk3PeLO22SbiP1UNWzcEPaTQv+YVxe4G0KOjAkg==}
engines: {node: '>=16.0.0'}
peerDependencies:
'@vite-pwa/assets-generator': ^0.2.6
vite: ^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0
workbox-build: ^7.3.0
workbox-window: ^7.3.0
peerDependenciesMeta:
'@vite-pwa/assets-generator':
optional: true
vite-plugin-pwa@1.0.0:
resolution: {integrity: sha512-X77jo0AOd5OcxmWj3WnVti8n7Kw2tBgV1c8MCXFclrSlDV23ePzv2eTDIALXI2Qo6nJ5pZJeZAuX0AawvRfoeA==}
engines: {node: '>=16.0.0'}
@@ -13335,6 +13417,10 @@ snapshots:
transitivePeerDependencies:
- vue
'@vite-pwa/vitepress@0.5.4(vite-plugin-pwa@0.21.2(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0))':
dependencies:
vite-plugin-pwa: 0.21.2(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0)
'@vite-pwa/vitepress@1.0.0(vite-plugin-pwa@1.0.0(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0))':
dependencies:
vite-plugin-pwa: 1.0.0(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0)
@@ -20538,6 +20624,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
vite-plugin-pwa@0.21.2(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0):
dependencies:
debug: 4.4.0(supports-color@8.1.1)
pretty-bytes: 6.1.1
tinyglobby: 0.2.12
vite: 6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)
workbox-build: 7.1.1(@types/babel__core@7.20.5)
workbox-window: 7.3.0
transitivePeerDependencies:
- supports-color
vite-plugin-pwa@1.0.0(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0):
dependencies:
debug: 4.4.0(supports-color@8.1.1)