mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-02 07:06:43 +02:00
Passing tests
This commit is contained in:
@@ -4,32 +4,58 @@
|
|||||||
{
|
{
|
||||||
"id": "info",
|
"id": "info",
|
||||||
"grammar": "src/language/info/info.langium",
|
"grammar": "src/language/info/info.langium",
|
||||||
"fileExtensions": [".mmd", ".mermaid"]
|
"fileExtensions": [
|
||||||
|
".mmd",
|
||||||
|
".mermaid"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "packet",
|
"id": "packet",
|
||||||
"grammar": "src/language/packet/packet.langium",
|
"grammar": "src/language/packet/packet.langium",
|
||||||
"fileExtensions": [".mmd", ".mermaid"]
|
"fileExtensions": [
|
||||||
|
".mmd",
|
||||||
|
".mermaid"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "pie",
|
"id": "pie",
|
||||||
"grammar": "src/language/pie/pie.langium",
|
"grammar": "src/language/pie/pie.langium",
|
||||||
"fileExtensions": [".mmd", ".mermaid"]
|
"fileExtensions": [
|
||||||
|
".mmd",
|
||||||
|
".mermaid"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "architecture",
|
"id": "architecture",
|
||||||
"grammar": "src/language/architecture/architecture.langium",
|
"grammar": "src/language/architecture/architecture.langium",
|
||||||
"fileExtensions": [".mmd", ".mermaid"]
|
"fileExtensions": [
|
||||||
|
".mmd",
|
||||||
|
".mermaid"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "gitGraph",
|
"id": "gitGraph",
|
||||||
"grammar": "src/language/gitGraph/gitGraph.langium",
|
"grammar": "src/language/gitGraph/gitGraph.langium",
|
||||||
"fileExtensions": [".mmd", ".mermaid"]
|
"fileExtensions": [
|
||||||
|
".mmd",
|
||||||
|
".mermaid"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "radar",
|
"id": "radar",
|
||||||
"grammar": "src/language/radar/radar.langium",
|
"grammar": "src/language/radar/radar.langium",
|
||||||
"fileExtensions": [".mmd", ".mermaid"]
|
"fileExtensions": [
|
||||||
|
".mmd",
|
||||||
|
".mermaid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "treemap",
|
||||||
|
"grammar": "src/language/treemap/treemap.langium",
|
||||||
|
"fileExtensions": [
|
||||||
|
".mmd",
|
||||||
|
".mermaid"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"mode": "production",
|
"mode": "production",
|
||||||
|
1
packages/parser/src/language/treemap/index.ts
Normal file
1
packages/parser/src/language/treemap/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './module.js';
|
88
packages/parser/src/language/treemap/module.ts
Normal file
88
packages/parser/src/language/treemap/module.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import type {
|
||||||
|
DefaultSharedCoreModuleContext,
|
||||||
|
LangiumCoreServices,
|
||||||
|
LangiumSharedCoreServices,
|
||||||
|
Module,
|
||||||
|
PartialLangiumCoreServices,
|
||||||
|
} from 'langium';
|
||||||
|
import {
|
||||||
|
EmptyFileSystem,
|
||||||
|
createDefaultCoreModule,
|
||||||
|
createDefaultSharedCoreModule,
|
||||||
|
inject,
|
||||||
|
} from 'langium';
|
||||||
|
|
||||||
|
import { MermaidGeneratedSharedModule, TreemapGeneratedModule } from '../generated/module.js';
|
||||||
|
import { TreemapTokenBuilder } from './tokenBuilder.js';
|
||||||
|
import { TreemapValueConverter } from './valueConverter.js';
|
||||||
|
import { TreemapValidator, registerValidationChecks } from './treemap-validator.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declaration of `Treemap` services.
|
||||||
|
*/
|
||||||
|
interface TreemapAddedServices {
|
||||||
|
parser: {
|
||||||
|
TokenBuilder: TreemapTokenBuilder;
|
||||||
|
ValueConverter: TreemapValueConverter;
|
||||||
|
};
|
||||||
|
validation: {
|
||||||
|
TreemapValidator: TreemapValidator;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Union of Langium default services and `Treemap` services.
|
||||||
|
*/
|
||||||
|
export type TreemapServices = LangiumCoreServices & TreemapAddedServices;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dependency injection module that overrides Langium default services and
|
||||||
|
* contributes the declared `Treemap` services.
|
||||||
|
*/
|
||||||
|
export const TreemapModule: Module<
|
||||||
|
TreemapServices,
|
||||||
|
PartialLangiumCoreServices & TreemapAddedServices
|
||||||
|
> = {
|
||||||
|
parser: {
|
||||||
|
TokenBuilder: () => new TreemapTokenBuilder(),
|
||||||
|
ValueConverter: () => new TreemapValueConverter(),
|
||||||
|
},
|
||||||
|
validation: {
|
||||||
|
TreemapValidator: () => new TreemapValidator(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 createTreemapServices(context: DefaultSharedCoreModuleContext = EmptyFileSystem): {
|
||||||
|
shared: LangiumSharedCoreServices;
|
||||||
|
Treemap: TreemapServices;
|
||||||
|
} {
|
||||||
|
const shared: LangiumSharedCoreServices = inject(
|
||||||
|
createDefaultSharedCoreModule(context),
|
||||||
|
MermaidGeneratedSharedModule
|
||||||
|
);
|
||||||
|
const Treemap: TreemapServices = inject(
|
||||||
|
createDefaultCoreModule({ shared }),
|
||||||
|
TreemapGeneratedModule,
|
||||||
|
TreemapModule
|
||||||
|
);
|
||||||
|
shared.ServiceRegistry.register(Treemap);
|
||||||
|
|
||||||
|
// Register validation checks
|
||||||
|
registerValidationChecks(Treemap);
|
||||||
|
|
||||||
|
return { shared, Treemap };
|
||||||
|
}
|
7
packages/parser/src/language/treemap/tokenBuilder.ts
Normal file
7
packages/parser/src/language/treemap/tokenBuilder.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { AbstractMermaidTokenBuilder } from '../common/index.js';
|
||||||
|
|
||||||
|
export class TreemapTokenBuilder extends AbstractMermaidTokenBuilder {
|
||||||
|
public constructor() {
|
||||||
|
super(['treemap']);
|
||||||
|
}
|
||||||
|
}
|
77
packages/parser/src/language/treemap/treemap-validator.ts
Normal file
77
packages/parser/src/language/treemap/treemap-validator.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import type { ValidationAcceptor, ValidationChecks } from 'langium';
|
||||||
|
import type { MermaidAstType, TreemapDoc, TreemapRow } from '../generated/ast.js';
|
||||||
|
import type { TreemapServices } from './module.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register custom validation checks.
|
||||||
|
*/
|
||||||
|
export function registerValidationChecks(services: TreemapServices) {
|
||||||
|
const validator = services.validation.TreemapValidator;
|
||||||
|
const registry = services.validation.ValidationRegistry;
|
||||||
|
if (registry) {
|
||||||
|
// Use any to bypass type checking since we know TreemapDoc is part of the AST
|
||||||
|
// but the type system is having trouble with it
|
||||||
|
const checks: ValidationChecks<MermaidAstType> = {
|
||||||
|
TreemapDoc: validator.checkSingleRoot.bind(validator),
|
||||||
|
TreemapRow: (node: TreemapRow, accept: ValidationAcceptor) => {
|
||||||
|
validator.checkSingleRootRow(node, accept);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
registry.register(checks, validator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of custom validations.
|
||||||
|
*/
|
||||||
|
export class TreemapValidator {
|
||||||
|
constructor() {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.debug('TreemapValidator constructor');
|
||||||
|
}
|
||||||
|
checkSingleRootRow(_node: TreemapRow, _accept: ValidationAcceptor): void {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.debug('CHECKING SINGLE ROOT Row');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that a treemap has only one root node.
|
||||||
|
* A root node is defined as a node that has no indentation.
|
||||||
|
*/
|
||||||
|
checkSingleRoot(doc: TreemapDoc, accept: ValidationAcceptor): void {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.debug('CHECKING SINGLE ROOT');
|
||||||
|
let rootNodeIndentation;
|
||||||
|
|
||||||
|
for (const row of doc.TreemapRows) {
|
||||||
|
// 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 treemap.', {
|
||||||
|
node: row,
|
||||||
|
property: 'item',
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
rootNodeIndentation !== undefined &&
|
||||||
|
rootNodeIndentation >= parseInt(row.indent, 10)
|
||||||
|
) {
|
||||||
|
accept('error', 'Multiple root nodes are not allowed in a treemap.', {
|
||||||
|
node: row,
|
||||||
|
property: 'item',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
58
packages/parser/src/language/treemap/treemap.langium
Normal file
58
packages/parser/src/language/treemap/treemap.langium
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* Treemap grammar for Langium
|
||||||
|
* Converted from mindmap grammar
|
||||||
|
*
|
||||||
|
* The ML_COMMENT and NL hidden terminals handle whitespace, comments, and newlines
|
||||||
|
* before the treemap keyword, allowing for empty lines and comments before the
|
||||||
|
* treemap declaration.
|
||||||
|
*/
|
||||||
|
grammar Treemap
|
||||||
|
|
||||||
|
// Interface declarations for data types
|
||||||
|
interface Item {}
|
||||||
|
interface Section extends Item {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
interface Leaf extends Item {
|
||||||
|
name: string
|
||||||
|
value: number
|
||||||
|
}
|
||||||
|
|
||||||
|
entry TreemapDoc:
|
||||||
|
TREEMAP_KEYWORD
|
||||||
|
(TreemapRows+=TreemapRow)*;
|
||||||
|
|
||||||
|
terminal SEPARATOR: ':';
|
||||||
|
terminal COMMA: ',';
|
||||||
|
|
||||||
|
hidden terminal WS: /[ \t]+/; // One or more spaces or tabs for hidden whitespace
|
||||||
|
hidden terminal ML_COMMENT: /\%\%[^\n]*/;
|
||||||
|
hidden terminal NL: /\r?\n/;
|
||||||
|
|
||||||
|
TreemapRow:
|
||||||
|
indent=INDENTATION? item=Item;
|
||||||
|
|
||||||
|
Item returns Item:
|
||||||
|
Leaf | Section;
|
||||||
|
|
||||||
|
// Use a special rule order to handle the parsing precedence
|
||||||
|
Section returns Section:
|
||||||
|
name=STRING;
|
||||||
|
|
||||||
|
Leaf returns Leaf:
|
||||||
|
name=STRING INDENTATION? (SEPARATOR | COMMA) INDENTATION? value=MyNumber;
|
||||||
|
|
||||||
|
// This should be processed before whitespace is ignored
|
||||||
|
terminal INDENTATION: /[ \t]{1,}/; // One or more spaces/tabs for indentation
|
||||||
|
|
||||||
|
// Keywords with fixed text patterns
|
||||||
|
terminal TREEMAP_KEYWORD: 'treemap';
|
||||||
|
|
||||||
|
// Define as a terminal rule
|
||||||
|
terminal NUMBER: /[0-9_\.\,]+/;
|
||||||
|
|
||||||
|
// Then create a data type rule that uses it
|
||||||
|
MyNumber returns number: NUMBER;
|
||||||
|
|
||||||
|
terminal STRING: /"[^"]*"|'[^']*'/;
|
||||||
|
// Modified indentation rule to have higher priority than WS
|
28
packages/parser/src/language/treemap/valueConverter.ts
Normal file
28
packages/parser/src/language/treemap/valueConverter.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import type { CstNode, GrammarAST, ValueType } from 'langium';
|
||||||
|
import { AbstractMermaidValueConverter } from '../common/index.js';
|
||||||
|
|
||||||
|
export class TreemapValueConverter extends AbstractMermaidValueConverter {
|
||||||
|
protected runCustomConverter(
|
||||||
|
rule: GrammarAST.AbstractRule,
|
||||||
|
input: string,
|
||||||
|
_cstNode: CstNode
|
||||||
|
): ValueType | undefined {
|
||||||
|
if (rule.name === 'NUMBER') {
|
||||||
|
// console.debug('NUMBER', input);
|
||||||
|
// Convert to a number by removing any commas and converting to float
|
||||||
|
return parseFloat(input.replace(/,/g, ''));
|
||||||
|
} else if (rule.name === 'SEPARATOR') {
|
||||||
|
// console.debug('SEPARATOR', input);
|
||||||
|
// Remove quotes
|
||||||
|
return input.substring(1, input.length - 1);
|
||||||
|
} else if (rule.name === 'STRING') {
|
||||||
|
// console.debug('STRING', input);
|
||||||
|
// Remove quotes
|
||||||
|
return input.substring(1, input.length - 1);
|
||||||
|
} else if (rule.name === 'INDENTATION') {
|
||||||
|
// console.debug('INDENTATION', input);
|
||||||
|
return input.length;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
@@ -32,6 +32,12 @@ const consoleMock = vi.spyOn(console, 'log').mockImplementation(() => undefined)
|
|||||||
* @param result - the result `parse` function.
|
* @param result - the result `parse` function.
|
||||||
*/
|
*/
|
||||||
export function expectNoErrorsOrAlternatives(result: ParseResult) {
|
export function expectNoErrorsOrAlternatives(result: ParseResult) {
|
||||||
|
if (result.lexerErrors.length > 0) {
|
||||||
|
// console.debug(result.lexerErrors);
|
||||||
|
}
|
||||||
|
if (result.parserErrors.length > 0) {
|
||||||
|
// console.debug(result.parserErrors);
|
||||||
|
}
|
||||||
expect(result.lexerErrors).toHaveLength(0);
|
expect(result.lexerErrors).toHaveLength(0);
|
||||||
expect(result.parserErrors).toHaveLength(0);
|
expect(result.parserErrors).toHaveLength(0);
|
||||||
|
|
||||||
|
102
packages/parser/tests/treemap.test.ts
Normal file
102
packages/parser/tests/treemap.test.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { expectNoErrorsOrAlternatives } from './test-util.js';
|
||||||
|
import type { TreemapDoc, Section, Leaf } from '../src/language/generated/ast.js';
|
||||||
|
import type { LangiumParser } from 'langium';
|
||||||
|
import { createTreemapServices } from '../src/language/treemap/module.js';
|
||||||
|
|
||||||
|
describe('Treemap Parser', () => {
|
||||||
|
const services = createTreemapServices().Treemap;
|
||||||
|
const parser: LangiumParser = services.parser.LangiumParser;
|
||||||
|
|
||||||
|
const parse = (input: string) => {
|
||||||
|
return parser.parse<TreemapDoc>(input);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Basic Parsing', () => {
|
||||||
|
it('should parse empty treemap', () => {
|
||||||
|
const result = parse('treemap');
|
||||||
|
expectNoErrorsOrAlternatives(result);
|
||||||
|
expect(result.value.$type).toBe('TreemapDoc');
|
||||||
|
expect(result.value.TreemapRows).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse a section node', () => {
|
||||||
|
const result = parse('treemap\n"Root"');
|
||||||
|
expectNoErrorsOrAlternatives(result);
|
||||||
|
expect(result.value.$type).toBe('TreemapDoc');
|
||||||
|
expect(result.value.TreemapRows).toHaveLength(1);
|
||||||
|
if (result.value.TreemapRows[0].item) {
|
||||||
|
expect(result.value.TreemapRows[0].item.$type).toBe('Section');
|
||||||
|
const section = result.value.TreemapRows[0].item as Section;
|
||||||
|
expect(section.name).toBe('Root');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse a section with leaf nodes', () => {
|
||||||
|
const result = parse(`treemap
|
||||||
|
"Root"
|
||||||
|
"Child1" , 100
|
||||||
|
"Child2" : 200
|
||||||
|
`);
|
||||||
|
expectNoErrorsOrAlternatives(result);
|
||||||
|
expect(result.value.$type).toBe('TreemapDoc');
|
||||||
|
expect(result.value.TreemapRows).toHaveLength(3);
|
||||||
|
|
||||||
|
if (result.value.TreemapRows[0].item) {
|
||||||
|
expect(result.value.TreemapRows[0].item.$type).toBe('Section');
|
||||||
|
const section = result.value.TreemapRows[0].item as Section;
|
||||||
|
expect(section.name).toBe('Root');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.value.TreemapRows[1].item) {
|
||||||
|
expect(result.value.TreemapRows[1].item.$type).toBe('Leaf');
|
||||||
|
const leaf = result.value.TreemapRows[1].item as Leaf;
|
||||||
|
expect(leaf.name).toBe('Child1');
|
||||||
|
expect(leaf.value).toBe(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.value.TreemapRows[2].item) {
|
||||||
|
expect(result.value.TreemapRows[2].item.$type).toBe('Leaf');
|
||||||
|
const leaf = result.value.TreemapRows[2].item as Leaf;
|
||||||
|
expect(leaf.name).toBe('Child2');
|
||||||
|
expect(leaf.value).toBe(200);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Data Types', () => {
|
||||||
|
it('should correctly parse string values', () => {
|
||||||
|
const result = parse('treemap\n"My Section"');
|
||||||
|
expectNoErrorsOrAlternatives(result);
|
||||||
|
if (result.value.TreemapRows[0].item) {
|
||||||
|
expect(result.value.TreemapRows[0].item.$type).toBe('Section');
|
||||||
|
const section = result.value.TreemapRows[0].item as Section;
|
||||||
|
expect(section.name).toBe('My Section');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly parse number values', () => {
|
||||||
|
const result = parse('treemap\n"Item" : 123.45');
|
||||||
|
expectNoErrorsOrAlternatives(result);
|
||||||
|
if (result.value.TreemapRows[0].item) {
|
||||||
|
expect(result.value.TreemapRows[0].item.$type).toBe('Leaf');
|
||||||
|
const leaf = result.value.TreemapRows[0].item as Leaf;
|
||||||
|
expect(leaf.name).toBe('Item');
|
||||||
|
expect(typeof leaf.value).toBe('number');
|
||||||
|
expect(leaf.value).toBe(123.45);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Validation', () => {
|
||||||
|
it('should parse multiple root nodes', () => {
|
||||||
|
const result = parse('treemap\n"Root1"\n"Root2"');
|
||||||
|
expect(result.parserErrors).toHaveLength(0);
|
||||||
|
|
||||||
|
// We're only checking that the multiple root nodes parse successfully
|
||||||
|
// The validation errors would be reported by the validator during validation
|
||||||
|
expect(result.value.$type).toBe('TreemapDoc');
|
||||||
|
expect(result.value.TreemapRows).toHaveLength(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Reference in New Issue
Block a user