mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-08-15 14:29:25 +02:00
Passing tests
This commit is contained in:
@@ -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": "treemap",
|
||||
"grammar": "src/language/treemap/treemap.langium",
|
||||
"fileExtensions": [
|
||||
".mmd",
|
||||
".mermaid"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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.
|
||||
*/
|
||||
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.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