feat(parser): create pie parser and export it

This commit is contained in:
Reda Al Sulais
2023-08-20 18:48:03 +03:00
parent 4ae361bd1f
commit 49c5f3bb9c
10 changed files with 472 additions and 4 deletions

View File

@@ -5,6 +5,11 @@
"id": "info", "id": "info",
"grammar": "src/language/info/info.langium", "grammar": "src/language/info/info.langium",
"fileExtensions": [".mmd", ".mermaid"] "fileExtensions": [".mmd", ".mermaid"]
},
{
"id": "pie",
"grammar": "src/language/pie/pie.langium",
"fileExtensions": [".mmd", ".mermaid"]
} }
], ],
"mode": "production", "mode": "production",

View File

@@ -1,3 +1,3 @@
export type { Info } from './language/index.js'; export type { Info, Pie, PieSection } from './language/index.js';
export type { DiagramAST } from './parse.js'; export type { DiagramAST } from './parse.js';
export { parse, MermaidParseError } from './parse.js'; export { parse, MermaidParseError } from './parse.js';

View File

@@ -4,3 +4,4 @@ export * from './generated/module.js';
export * from './common/index.js'; export * from './common/index.js';
export * from './info/index.js'; export * from './info/index.js';
export * from './pie/index.js';

View File

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

View File

@@ -0,0 +1,20 @@
grammar Pie
import "../common/common";
entry Pie:
NEWLINE*
"pie" showData?="showData"?
(
NEWLINE* TitleAndAccessibilities sections+=PieSection*
| NEWLINE+ sections+=PieSection+
| NEWLINE*
)
;
PieSection:
label=PIE_SECTION_LABEL ":" value=PIE_SECTION_VALUE
NEWLINE+
;
terminal PIE_SECTION_LABEL: /"[^"]+"/;
terminal PIE_SECTION_VALUE returns number: /(0|[1-9][0-9]*)(\.[0-9]+)?/;

View File

@@ -0,0 +1,68 @@
import type {
DefaultSharedModuleContext,
LangiumServices,
LangiumSharedServices,
Module,
PartialLangiumServices,
} from 'langium';
import { EmptyFileSystem, createDefaultModule, createDefaultSharedModule, inject } from 'langium';
import { MermaidGeneratedSharedModule, PieGeneratedModule } from '../generated/module.js';
import { CommonLexer } from '../common/commonLexer.js';
import { PieTokenBuilder } from './pieTokenBuilder.js';
import { PieValueConverter } from './pieValueConverter.js';
/**
* Declaration of `Pie` services.
*/
type PieAddedServices = {
parser: {
Lexer: CommonLexer;
TokenBuilder: PieTokenBuilder;
ValueConverter: PieValueConverter;
};
};
/**
* Union of Langium default services and `Pie` services.
*/
export type PieServices = LangiumServices & PieAddedServices;
/**
* Dependency injection module that overrides Langium default services and
* contributes the declared `Pie` services.
*/
const PieModule: Module<PieServices, PartialLangiumServices & PieAddedServices> = {
parser: {
Lexer: (services) => new CommonLexer(services),
TokenBuilder: () => new PieTokenBuilder(),
ValueConverter: () => new PieValueConverter(),
},
};
/**
* 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 createPieServices(context: DefaultSharedModuleContext = EmptyFileSystem): {
shared: LangiumSharedServices;
Pie: PieServices;
} {
const shared: LangiumSharedServices = inject(
createDefaultSharedModule(context),
MermaidGeneratedSharedModule
);
const Pie: PieServices = inject(createDefaultModule({ shared }), PieGeneratedModule, PieModule);
shared.ServiceRegistry.register(Pie);
return { shared, Pie };
}

View File

@@ -0,0 +1,23 @@
import type { GrammarAST, Stream, TokenBuilderOptions } from 'langium';
import { DefaultTokenBuilder } from 'langium';
import type { TokenType } from '../chevrotainWrapper.js';
export class PieTokenBuilder extends DefaultTokenBuilder {
protected override buildKeywordTokens(
rules: Stream<GrammarAST.AbstractRule>,
terminalTokens: TokenType[],
options?: TokenBuilderOptions
): TokenType[] {
const tokenTypes: TokenType[] = super.buildKeywordTokens(rules, terminalTokens, options);
tokenTypes.forEach((tokenType: TokenType): void => {
if (
(tokenType.name === 'pie' || tokenType.name === 'showData') &&
tokenType.PATTERN !== undefined
) {
tokenType.PATTERN = new RegExp(tokenType.PATTERN.toString() + '(?!\\S)');
}
});
return tokenTypes;
}
}

View File

@@ -0,0 +1,50 @@
import type { CstNode, GrammarAST, ValueType } from 'langium';
import { DefaultValueConverter } from 'langium';
import { CommonValueConverter } from '../common/commonValueConverters.js';
export class PieValueConverter extends DefaultValueConverter {
protected override runConverter(
rule: GrammarAST.AbstractRule,
input: string,
cstNode: CstNode
): ValueType {
let value: ValueType | undefined = CommonValueConverter.customRunConverter(
rule,
input,
cstNode
);
if (value === undefined) {
value = PieValueConverter.customRunConverter(rule, input, cstNode);
}
if (value === undefined) {
return super.runConverter(rule, input, cstNode);
} else {
return value;
}
}
/**
* A method contains convert logic to be used by class itself or `MermaidValueConverter`.
*
* @param rule - Parsed rule.
* @param input - Matched string.
* @param _cstNode - Node in the Concrete Syntax Tree (CST).
* @returns converted the value if it's pie rule or `null` if it's not.
*/
public static customRunConverter(
rule: GrammarAST.AbstractRule,
input: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_cstNode: CstNode
): ValueType | undefined {
if (rule.name === 'PIE_SECTION_LABEL') {
return input
.replace(/"/g, '')
.trim()
.replaceAll(/[\t ]{2,}/gm, ' ');
}
return undefined;
}
}

View File

@@ -1,8 +1,8 @@
import type { LangiumParser, ParseResult } from 'langium'; import type { LangiumParser, ParseResult } from 'langium';
import type { Info } from './index.js'; import type { Info, Pie } from './index.js';
import { createInfoServices } from './language/index.js'; import { createInfoServices, createPieServices } from './language/index.js';
export type DiagramAST = Info; export type DiagramAST = Info | Pie;
const parsers: Record<string, LangiumParser> = {}; const parsers: Record<string, LangiumParser> = {};
@@ -13,8 +13,14 @@ const initializers = {
const parser = createInfoServices().Info.parser.LangiumParser; const parser = createInfoServices().Info.parser.LangiumParser;
parsers['info'] = parser; parsers['info'] = parser;
}, },
pie: () => {
const parser = createPieServices().Pie.parser.LangiumParser;
parsers['pie'] = parser;
},
} as const; } as const;
export function parse(diagramType: 'info', text: string): Info; export function parse(diagramType: 'info', text: string): Info;
export function parse(diagramType: 'pie', text: string): Pie;
export function parse<T extends DiagramAST>( export function parse<T extends DiagramAST>(
diagramType: keyof typeof initializers, diagramType: keyof typeof initializers,
text: string text: string

View File

@@ -0,0 +1,294 @@
import { describe, expect, it } from 'vitest';
import type { LangiumParser, ParseResult } from 'langium';
import type { PieServices } from '../src/language/index.js';
import { Pie, createPieServices } from '../src/language/index.js';
const services: PieServices = createPieServices().Pie;
const parser: LangiumParser = services.parser.LangiumParser;
export function createPieTestServices(): {
services: PieServices;
parse: (input: string) => ParseResult<Pie>;
} {
const parse = (input: string) => {
return parser.parse<Pie>(input);
};
return { services, parse };
}
describe('pie', () => {
const { parse } = createPieTestServices();
it.each([
`pie`,
` pie `,
`\tpie\t`,
`
\tpie
`,
])('should handle regular pie', (context: string) => {
const result = parse(context);
expect(result.parserErrors).toHaveLength(0);
expect(result.lexerErrors).toHaveLength(0);
const value = result.value;
expect(value.$type).toBe(Pie);
});
it.each([
`pie showData`,
` pie showData `,
`\tpie\tshowData\t`,
`
pie\tshowData
`,
])('should handle regular showData', (context: string) => {
const result = parse(context);
expect(result.parserErrors).toHaveLength(0);
expect(result.lexerErrors).toHaveLength(0);
const value = result.value;
expect(value.$type).toBe(Pie);
expect(value.showData).toBeTruthy();
});
it.each([
`pie title sample title`,
` pie title sample title `,
`\tpie\ttitle sample title\t`,
`pie
\ttitle sample title
`,
])('should handle regular pie + title in same line', (context: string) => {
const result = parse(context);
expect(result.parserErrors).toHaveLength(0);
expect(result.lexerErrors).toHaveLength(0);
const value = result.value;
expect(value.$type).toBe(Pie);
expect(value.title).toBe('sample title');
});
it.each([
`pie
title sample title`,
`pie
title sample title
`,
`pie
title sample title`,
`pie
title sample title
`,
])('should handle regular pie + title in different line', (context: string) => {
const result = parse(context);
expect(result.parserErrors).toHaveLength(0);
expect(result.lexerErrors).toHaveLength(0);
const value = result.value;
expect(value.$type).toBe(Pie);
expect(value.title).toBe('sample title');
});
it.each([
`pie showData title sample title`,
`pie showData title sample title
`,
])('should handle regular pie + showData + title', (context: string) => {
const result = parse(context);
expect(result.parserErrors).toHaveLength(0);
expect(result.lexerErrors).toHaveLength(0);
const value = result.value;
expect(value.$type).toBe(Pie);
expect(value.showData).toBeTruthy();
expect(value.title).toBe('sample title');
});
it.each([
`pie showData
title sample title`,
`pie showData
title sample title
`,
`pie showData
title sample title`,
`pie showData
title sample title
`,
])('should handle regular showData + title in different line', (context: string) => {
const result = parse(context);
expect(result.parserErrors).toHaveLength(0);
expect(result.lexerErrors).toHaveLength(0);
const value = result.value;
expect(value.$type).toBe(Pie);
expect(value.showData).toBeTruthy();
expect(value.title).toBe('sample title');
});
describe('sections', () => {
describe('normal', () => {
it.each([
`pie
"GitHub":100
"GitLab":50`,
`pie
"GitHub" : 100
"GitLab" : 50`,
`pie
"GitHub"\t:\t100
"GitLab"\t:\t50`,
`pie
\t"GitHub" \t : \t 100
\t"GitLab" \t : \t 50
`,
])('should handle regular secions', (context: string) => {
const result = parse(context);
expect(result.parserErrors).toHaveLength(0);
expect(result.lexerErrors).toHaveLength(0);
const value = result.value;
expect(value.$type).toBe(Pie);
const section0 = value.sections[0];
expect(section0?.label).toBe('GitHub');
expect(section0?.value).toBe(100);
const section1 = value.sections[1];
expect(section1?.label).toBe('GitLab');
expect(section1?.value).toBe(50);
});
it('should handle sections with showData', () => {
const context = `pie showData
"GitHub": 100
"GitLab": 50`;
const result = parse(context);
expect(result.parserErrors).toHaveLength(0);
expect(result.lexerErrors).toHaveLength(0);
const value = result.value;
expect(value.$type).toBe(Pie);
expect(value.showData).toBeTruthy();
const section0 = value.sections[0];
expect(section0?.label).toBe('GitHub');
expect(section0?.value).toBe(100);
const section1 = value.sections[1];
expect(section1?.label).toBe('GitLab');
expect(section1?.value).toBe(50);
});
it('should handle sections with title', () => {
const context = `pie title sample wow
"GitHub": 100
"GitLab": 50`;
const result = parse(context);
expect(result.parserErrors).toHaveLength(0);
expect(result.lexerErrors).toHaveLength(0);
const value = result.value;
expect(value.$type).toBe(Pie);
expect(value.title).toBe('sample wow');
const section0 = value.sections[0];
expect(section0?.label).toBe('GitHub');
expect(section0?.value).toBe(100);
const section1 = value.sections[1];
expect(section1?.label).toBe('GitLab');
expect(section1?.value).toBe(50);
});
it('should handle sections with accTitle', () => {
const context = `pie accTitle: sample wow
"GitHub": 100
"GitLab": 50`;
const result = parse(context);
expect(result.parserErrors).toHaveLength(0);
expect(result.lexerErrors).toHaveLength(0);
const value = result.value;
expect(value.$type).toBe(Pie);
expect(value.accTitle).toBe('sample wow');
const section0 = value.sections[0];
expect(section0?.label).toBe('GitHub');
expect(section0?.value).toBe(100);
const section1 = value.sections[1];
expect(section1?.label).toBe('GitLab');
expect(section1?.value).toBe(50);
});
it('should handle sections with single line accDescr', () => {
const context = `pie accDescr: sample wow
"GitHub": 100
"GitLab": 50`;
const result = parse(context);
expect(result.parserErrors).toHaveLength(0);
expect(result.lexerErrors).toHaveLength(0);
const value = result.value;
expect(value.$type).toBe(Pie);
expect(value.accDescr).toBe('sample wow');
const section0 = value.sections[0];
expect(section0?.label).toBe('GitHub');
expect(section0?.value).toBe(100);
const section1 = value.sections[1];
expect(section1?.label).toBe('GitLab');
expect(section1?.value).toBe(50);
});
it('should handle sections with multi line accDescr', () => {
const context = `pie accDescr {
sample wow
}
"GitHub": 100
"GitLab": 50`;
const result = parse(context);
expect(result.parserErrors).toHaveLength(0);
expect(result.lexerErrors).toHaveLength(0);
const value = result.value;
expect(value.$type).toBe(Pie);
expect(value.accDescr).toBe('sample wow');
const section0 = value.sections[0];
expect(section0?.label).toBe('GitHub');
expect(section0?.value).toBe(100);
const section1 = value.sections[1];
expect(section1?.label).toBe('GitLab');
expect(section1?.value).toBe(50);
});
});
describe('duplicate', () => {
it('should handle duplicate sections', () => {
const context = `pie
"GitHub": 100
"GitHub": 50`;
const result = parse(context);
expect(result.parserErrors).toHaveLength(0);
expect(result.lexerErrors).toHaveLength(0);
const value = result.value;
expect(value.$type).toBe(Pie);
const section0 = value.sections[0];
expect(section0?.label).toBe('GitHub');
expect(section0?.value).toBe(100);
const section1 = value.sections[1];
expect(section1?.label).toBe('GitHub');
expect(section1?.value).toBe(50);
});
});
});
});