mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-16 05:49:43 +02:00
feat(parser): create info
parser with exporting parser internals
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
export type { Info } from './language/index.js';
|
||||||
|
export type { DiagramAST } from './parse.js';
|
||||||
|
export { parse, MermaidParseError } from './parse.js';
|
||||||
|
@@ -3,3 +3,4 @@ export * from './generated/grammar.js';
|
|||||||
export * from './generated/module.js';
|
export * from './generated/module.js';
|
||||||
|
|
||||||
export * from './common/index.js';
|
export * from './common/index.js';
|
||||||
|
export * from './info/index.js';
|
||||||
|
1
packages/parser/src/language/info/index.ts
Normal file
1
packages/parser/src/language/info/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './infoModule.js';
|
9
packages/parser/src/language/info/info.langium
Normal file
9
packages/parser/src/language/info/info.langium
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
grammar Info
|
||||||
|
import "../common/common";
|
||||||
|
|
||||||
|
entry Info:
|
||||||
|
NEWLINE*
|
||||||
|
"info" NEWLINE*
|
||||||
|
("showInfo" NEWLINE*)?
|
||||||
|
TitleAndAccessibilities?
|
||||||
|
;
|
72
packages/parser/src/language/info/infoModule.ts
Normal file
72
packages/parser/src/language/info/infoModule.ts
Normal 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 };
|
||||||
|
}
|
24
packages/parser/src/language/info/infoTokenBuilder.ts
Normal file
24
packages/parser/src/language/info/infoTokenBuilder.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
43
packages/parser/src/parse.ts
Normal file
43
packages/parser/src/parse.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
58
packages/parser/tests/info.test.ts
Normal file
58
packages/parser/tests/info.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
Reference in New Issue
Block a user