mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-15 21:39:40 +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 './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