feat(parser): create info parser with exporting parser internals

This commit is contained in:
Reda Al Sulais
2023-08-20 15:38:46 +03:00
parent 1559c2ca21
commit 1c24617f98
8 changed files with 211 additions and 0 deletions

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
grammar Info
import "../common/common";
entry Info:
NEWLINE*
"info" NEWLINE*
("showInfo" NEWLINE*)?
TitleAndAccessibilities?
;

View File

@@ -0,0 +1,72 @@
import type {
DefaultSharedModuleContext,
LangiumServices,
LangiumSharedServices,
Module,
PartialLangiumServices,
} from 'langium';
import { EmptyFileSystem, createDefaultModule, createDefaultSharedModule, inject } from 'langium';
import { MermaidGeneratedSharedModule, InfoGeneratedModule } from '../generated/module.js';
import { CommonLexer } from '../common/commonLexer.js';
import { CommonValueConverter } from '../common/commonValueConverters.js';
import { InfoTokenBuilder } from './infoTokenBuilder.js';
/**
* Declaration of `Info` services.
*/
type InfoAddedServices = {
parser: {
Lexer: CommonLexer;
TokenBuilder: InfoTokenBuilder;
ValueConverter: CommonValueConverter;
};
};
/**
* Union of Langium default services and `Info` services.
*/
export type InfoServices = LangiumServices & InfoAddedServices;
/**
* Dependency injection module that overrides Langium default services and
* contributes the declared `Info` services.
*/
const InfoModule: Module<InfoServices, PartialLangiumServices & InfoAddedServices> = {
parser: {
Lexer: (services) => new CommonLexer(services),
TokenBuilder: () => new InfoTokenBuilder(),
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 createInfoServices(context: DefaultSharedModuleContext = EmptyFileSystem): {
shared: LangiumSharedServices;
Info: InfoServices;
} {
const shared: LangiumSharedServices = inject(
createDefaultSharedModule(context),
MermaidGeneratedSharedModule
);
const Info: InfoServices = inject(
createDefaultModule({ shared }),
InfoGeneratedModule,
InfoModule
);
shared.ServiceRegistry.register(Info);
return { shared, Info };
}

View File

@@ -0,0 +1,24 @@
import type { GrammarAST, Stream, TokenBuilderOptions } from 'langium';
import { DefaultTokenBuilder } from 'langium';
import type { TokenType } from '../chevrotainWrapper.js';
export class InfoTokenBuilder extends DefaultTokenBuilder {
protected override buildKeywordTokens(
rules: Stream<GrammarAST.AbstractRule>,
terminalTokens: TokenType[],
options?: TokenBuilderOptions
): TokenType[] {
const tokenTypes: TokenType[] = super.buildKeywordTokens(rules, terminalTokens, options);
// to restrict users, they mustn't have any non-whitespace characters after the keyword.
tokenTypes.forEach((tokenType: TokenType): void => {
if (
(tokenType.name === 'info' || tokenType.name === 'showInfo') &&
tokenType.PATTERN !== undefined
) {
tokenType.PATTERN = new RegExp(tokenType.PATTERN.toString() + '(?!\\S)');
}
});
return tokenTypes;
}
}

View File

@@ -0,0 +1,43 @@
import type { LangiumParser, ParseResult } from 'langium';
import type { Info } from './index.js';
import { createInfoServices } from './language/index.js';
export type DiagramAST = Info;
const parsers: Record<string, LangiumParser> = {};
const initializers = {
info: () => {
// Will have to make parse async to use this. Can try later...
// const { createInfoServices } = await import('./language/info/index.js');
const parser = createInfoServices().Info.parser.LangiumParser;
parsers['info'] = parser;
},
} as const;
export function parse(diagramType: 'info', text: string): Info;
export function parse<T extends DiagramAST>(
diagramType: keyof typeof initializers,
text: string
): T {
const initializer = initializers[diagramType];
if (!initializer) {
throw new Error(`Unknown diagram type: ${diagramType}`);
}
if (!parsers[diagramType]) {
initializer();
}
const parser: LangiumParser = parsers[diagramType];
const result: ParseResult<T> = parser.parse<T>(text);
if (result.lexerErrors.length > 0 || result.parserErrors.length > 0) {
throw new MermaidParseError(result);
}
return result.value;
}
export class MermaidParseError extends Error {
constructor(public result: ParseResult<DiagramAST>) {
const lexerErrors: string = result.lexerErrors.map((err) => err.message).join('\n');
const parserErrors: string = result.parserErrors.map((err) => err.message).join('\n');
super(`Parsing failed: ${lexerErrors} ${parserErrors}`);
}
}

View File

@@ -0,0 +1,58 @@
import { describe, expect, it } from 'vitest';
import type { LangiumParser, ParseResult } from 'langium';
import type { InfoServices } from '../src/language/index.js';
import { Info, createInfoServices } from '../src/language/index.js';
const services: InfoServices = createInfoServices().Info;
const parser: LangiumParser = services.parser.LangiumParser;
export function createInfoTestServices(): {
services: InfoServices;
parse: (input: string) => ParseResult<Info>;
} {
const parse = (input: string) => {
return parser.parse<Info>(input);
};
return { services, parse };
}
describe('info', () => {
const { parse } = createInfoTestServices();
it.each([
`info`,
`
info`,
`info
`,
`
info
`,
])('should handle empty info', (context: string) => {
const { parserErrors, lexerErrors, value } = parse(context);
expect(parserErrors).toHaveLength(0);
expect(lexerErrors).toHaveLength(0);
expect(value.$type).toBe(Info);
});
it.each([
`info showInfo`,
`
info showInfo`,
`info
showInfo
`,
`
info
showInfo
`,
])('should handle showInfo', (context: string) => {
const { parserErrors, lexerErrors, value } = parse(context);
expect(parserErrors).toHaveLength(0);
expect(lexerErrors).toHaveLength(0);
expect(value.$type).toBe(Info);
});
});