Merge pull request #4751 from Yokozuna59/add-pie-langium-parser

feat: add `pie` langium parser
This commit is contained in:
Sidharth Vinod 2024-02-11 21:54:36 +05:30 committed by GitHub
commit d11bfaa6c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 554 additions and 371 deletions

View File

@ -1,9 +0,0 @@
declare module 'langium-cli' {
export interface GenerateOptions {
file?: string;
mode?: 'development' | 'production';
watch?: boolean;
}
export function generate(options: GenerateOptions): Promise<boolean>;
}

View File

@ -1,74 +0,0 @@
/** mermaid
* https://knsv.github.io/mermaid
* (c) 2015 Knut Sveidqvist
* MIT license.
*/
%lex
%options case-insensitive
%x string
%x title
%x acc_title
%x acc_descr
%x acc_descr_multiline
%%
\%\%(?!\{)[^\n]* /* skip comments */
[^\}]\%\%[^\n]* /* skip comments */{ /*console.log('');*/ }
[\n\r]+ return 'NEWLINE';
\%\%[^\n]* /* do nothing */
[\s]+ /* ignore */
title { this.begin("title");return 'title'; }
<title>(?!\n|;|#)*[^\n]* { this.popState(); return "title_value"; }
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; }
<acc_descr>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_descr_value"; }
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
<acc_descr_multiline>[\}] { this.popState(); }
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
["] { this.begin("string"); }
<string>["] { this.popState(); }
<string>[^"]* { return "txt"; }
"pie" return 'PIE';
"showData" return 'showData';
":"[\s]*[\d]+(?:\.[\d]+)? return "value";
<<EOF>> return 'EOF';
/lex
%start start
%% /* language grammar */
start
: eol start
| PIE document
| PIE showData document {yy.setShowData(true);}
;
document
: /* empty */
| document line
;
line
: statement eol { $$ = $1 }
;
statement
:
| txt value { yy.addSection($1,yy.cleanupValue($2)); }
| title title_value { $$=$2.trim();yy.setDiagramTitle($$); }
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
| acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); } | section {yy.addSection($1.substr(8));$$=$1.substr(8);}
;
eol
: NEWLINE
| ';'
| EOF
;
%%

View File

@ -1,5 +1,4 @@
// @ts-ignore: JISON doesn't support types
import { parser } from './parser/pie.jison';
import { parser } from './pieParser.js';
import { DEFAULT_PIE_DB, db } from './pieDb.js';
import { setConfig } from '../../diagram-api/diagramAPI.js';
@ -8,17 +7,11 @@ setConfig({
});
describe('pie', () => {
beforeAll(() => {
parser.yy = db;
});
beforeEach(() => {
parser.yy.clear();
});
beforeEach(() => db.clear());
describe('parse', () => {
it('should handle very simple pie', () => {
parser.parse(`pie
it('should handle very simple pie', async () => {
await parser.parse(`pie
"ash": 100
`);
@ -26,8 +19,8 @@ describe('pie', () => {
expect(sections['ash']).toBe(100);
});
it('should handle simple pie', () => {
parser.parse(`pie
it('should handle simple pie', async () => {
await parser.parse(`pie
"ash" : 60
"bat" : 40
`);
@ -37,8 +30,8 @@ describe('pie', () => {
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with showData', () => {
parser.parse(`pie showData
it('should handle simple pie with showData', async () => {
await parser.parse(`pie showData
"ash" : 60
"bat" : 40
`);
@ -50,8 +43,8 @@ describe('pie', () => {
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with comments', () => {
parser.parse(`pie
it('should handle simple pie with comments', async () => {
await parser.parse(`pie
%% comments
"ash" : 60
"bat" : 40
@ -62,8 +55,8 @@ describe('pie', () => {
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with a title', () => {
parser.parse(`pie title a 60/40 pie
it('should handle simple pie with a title', async () => {
await parser.parse(`pie title a 60/40 pie
"ash" : 60
"bat" : 40
`);
@ -75,8 +68,8 @@ describe('pie', () => {
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with an acc title (accTitle)', () => {
parser.parse(`pie title a neat chart
it('should handle simple pie with an acc title (accTitle)', async () => {
await parser.parse(`pie title a neat chart
accTitle: a neat acc title
"ash" : 60
"bat" : 40
@ -91,8 +84,8 @@ describe('pie', () => {
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with an acc description (accDescr)', () => {
parser.parse(`pie title a neat chart
it('should handle simple pie with an acc description (accDescr)', async () => {
await parser.parse(`pie title a neat chart
accDescr: a neat description
"ash" : 60
"bat" : 40
@ -107,8 +100,8 @@ describe('pie', () => {
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with a multiline acc description (accDescr)', () => {
parser.parse(`pie title a neat chart
it('should handle simple pie with a multiline acc description (accDescr)', async () => {
await parser.parse(`pie title a neat chart
accDescr {
a neat description
on multiple lines
@ -126,8 +119,8 @@ describe('pie', () => {
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with positive decimal', () => {
parser.parse(`pie
it('should handle simple pie with positive decimal', async () => {
await parser.parse(`pie
"ash" : 60.67
"bat" : 40
`);
@ -138,12 +131,12 @@ describe('pie', () => {
});
it('should handle simple pie with negative decimal', () => {
expect(() => {
parser.parse(`pie
expect(async () => {
await parser.parse(`pie
"ash" : -60.67
"bat" : 40.12
`);
}).toThrowError();
}).rejects.toThrowError();
});
});

View File

@ -1,6 +1,4 @@
import { log } from '../../logger.js';
import { getConfig as commonGetConfig } from '../../diagram-api/diagramAPI.js';
import { sanitizeText } from '../common/common.js';
import {
setAccTitle,
getAccTitle,
@ -10,7 +8,7 @@ import {
setAccDescription,
clear as commonClear,
} from '../common/commonDb.js';
import type { PieFields, PieDB, Sections } from './pieTypes.js';
import type { PieFields, PieDB, Sections, D3Section } from './pieTypes.js';
import type { RequiredDeep } from 'type-fest';
import type { PieDiagramConfig } from '../../config.type.js';
import DEFAULT_CONFIG from '../../defaultConfig.js';
@ -35,8 +33,7 @@ const clear = (): void => {
commonClear();
};
const addSection = (label: string, value: number): void => {
label = sanitizeText(label, commonGetConfig());
const addSection = ({ label, value }: D3Section): void => {
if (sections[label] === undefined) {
sections[label] = value;
log.debug(`added new section: ${label}, with value: ${value}`);
@ -45,13 +42,6 @@ const addSection = (label: string, value: number): void => {
const getSections = (): Sections => sections;
const cleanupValue = (value: string): number => {
if (value.substring(0, 1) === ':') {
value = value.substring(1).trim();
}
return Number(value.trim());
};
const setShowData = (toggle: boolean): void => {
showData = toggle;
};
@ -71,7 +61,6 @@ export const db: PieDB = {
addSection,
getSections,
cleanupValue,
setShowData,
getShowData,
};

View File

@ -1,6 +1,5 @@
import type { DiagramDefinition } from '../../diagram-api/types.js';
// @ts-ignore: JISON doesn't support types
import parser from './parser/pie.jison';
import { parser } from './pieParser.js';
import { db } from './pieDb.js';
import styles from './pieStyles.js';
import { renderer } from './pieRenderer.js';

View File

@ -0,0 +1,21 @@
import type { Pie } from '@mermaid-js/parser';
import { parse } from '@mermaid-js/parser';
import { log } from '../../logger.js';
import type { ParserDefinition } from '../../diagram-api/types.js';
import { populateCommonDb } from '../common/populateCommonDb.js';
import type { PieDB } from './pieTypes.js';
import { db } from './pieDb.js';
const populateDb = (ast: Pie, db: PieDB) => {
populateCommonDb(ast, db);
db.setShowData(ast.showData);
ast.sections.map(db.addSection);
};
export const parser: ParserDefinition = {
parse: async (input: string): Promise<void> => {
const ast: Pie = await parse('pie', input);
log.debug(ast);
populateDb(ast, db);
},
};

View File

@ -5,24 +5,24 @@ import { configureSvgSize } from '../../setupGraphViewbox.js';
import { getConfig } from '../../diagram-api/diagramAPI.js';
import { cleanAndMerge, parseFontSize } from '../../utils.js';
import type { DrawDefinition, Group, SVG } from '../../diagram-api/types.js';
import type { D3Sections, PieDB, Sections } from './pieTypes.js';
import type { D3Section, PieDB, Sections } from './pieTypes.js';
import type { MermaidConfig, PieDiagramConfig } from '../../config.type.js';
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
const createPieArcs = (sections: Sections): d3.PieArcDatum<D3Sections>[] => {
const createPieArcs = (sections: Sections): d3.PieArcDatum<D3Section>[] => {
// Compute the position of each group on the pie:
const pieData: D3Sections[] = Object.entries(sections)
.map((element: [string, number]): D3Sections => {
const pieData: D3Section[] = Object.entries(sections)
.map((element: [string, number]): D3Section => {
return {
label: element[0],
value: element[1],
};
})
.sort((a: D3Sections, b: D3Sections): number => {
.sort((a: D3Section, b: D3Section): number => {
return b.value - a.value;
});
const pie: d3.Pie<unknown, D3Sections> = d3pie<D3Sections>().value(
(d3Section: D3Sections): number => d3Section.value
const pie: d3.Pie<unknown, D3Section> = d3pie<D3Section>().value(
(d3Section: D3Section): number => d3Section.value
);
return pie(pieData);
};
@ -47,7 +47,6 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
const pieWidth: number = height;
const svg: SVG = selectSvgElement(id);
const group: Group = svg.append('g');
const sections: Sections = db.getSections();
group.attr('transform', 'translate(' + pieWidth / 2 + ',' + height / 2 + ')');
const { themeVariables } = globalConfig;
@ -57,13 +56,11 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
const textPosition: number = pieConfig.textPosition;
const radius: number = Math.min(pieWidth, height) / 2 - MARGIN;
// Shape helper to build arcs:
const arcGenerator: d3.Arc<unknown, d3.PieArcDatum<D3Sections>> = arc<
d3.PieArcDatum<D3Sections>
>()
const arcGenerator: d3.Arc<unknown, d3.PieArcDatum<D3Section>> = arc<d3.PieArcDatum<D3Section>>()
.innerRadius(0)
.outerRadius(radius);
const labelArcGenerator: d3.Arc<unknown, d3.PieArcDatum<D3Sections>> = arc<
d3.PieArcDatum<D3Sections>
const labelArcGenerator: d3.Arc<unknown, d3.PieArcDatum<D3Section>> = arc<
d3.PieArcDatum<D3Section>
>()
.innerRadius(radius * textPosition)
.outerRadius(radius * textPosition);
@ -75,7 +72,8 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
.attr('r', radius + outerStrokeWidth / 2)
.attr('class', 'pieOuterCircle');
const arcs: d3.PieArcDatum<D3Sections>[] = createPieArcs(sections);
const sections: Sections = db.getSections();
const arcs: d3.PieArcDatum<D3Section>[] = createPieArcs(sections);
const myGeneratedColors = [
themeVariables.pie1,
@ -101,7 +99,7 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
.enter()
.append('path')
.attr('d', arcGenerator)
.attr('fill', (datum: d3.PieArcDatum<D3Sections>) => {
.attr('fill', (datum: d3.PieArcDatum<D3Section>) => {
return color(datum.data.label);
})
.attr('class', 'pieCircle');
@ -117,10 +115,10 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
.data(arcs)
.enter()
.append('text')
.text((datum: d3.PieArcDatum<D3Sections>): string => {
.text((datum: d3.PieArcDatum<D3Section>): string => {
return ((datum.data.value / sum) * 100).toFixed(0) + '%';
})
.attr('transform', (datum: d3.PieArcDatum<D3Sections>): string => {
.attr('transform', (datum: d3.PieArcDatum<D3Section>): string => {
return 'translate(' + labelArcGenerator.centroid(datum) + ')';
})
.style('text-anchor', 'middle')
@ -160,7 +158,7 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
.append('text')
.attr('x', LEGEND_RECT_SIZE + LEGEND_SPACING)
.attr('y', LEGEND_RECT_SIZE - LEGEND_SPACING)
.text((datum: d3.PieArcDatum<D3Sections>): string => {
.text((datum: d3.PieArcDatum<D3Section>): string => {
const { label, value } = datum.data;
if (db.getShowData()) {
return `${label} [${value}]`;

View File

@ -36,7 +36,7 @@ export interface PieStyleOptions {
export type Sections = Record<string, number>;
export interface D3Sections {
export interface D3Section {
label: string;
value: number;
}
@ -55,9 +55,8 @@ export interface PieDB extends DiagramDB {
getAccDescription: () => string;
// diagram db
addSection: (label: string, value: number) => void;
addSection: ({ label, value }: D3Section) => void;
getSections: () => Sections;
cleanupValue: (value: string) => number;
setShowData: (toggle: boolean) => void;
getShowData: () => boolean;
}

View File

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

View File

@ -12,7 +12,7 @@
"type": "module",
"exports": {
".": {
"import": "./dist/mermaid-parser.esm.mjs",
"import": "./dist/mermaid-parser.core.mjs",
"types": "./dist/src/index.d.ts"
}
},
@ -34,10 +34,10 @@
"ast"
],
"dependencies": {
"langium": "2.0.1"
"langium": "2.1.2"
},
"devDependencies": {
"langium-cli": "2.0.1"
"chevrotain": "^11.0.3"
},
"files": [
"dist/"

View File

@ -1,8 +1,7 @@
import type { AstNode } from 'langium';
export type { Info, Packet } from './language/index.js';
export { MermaidParseError, parse } from './parse.js';
export type { DiagramAST } from './parse.js';
export * from './language/index.js';
export * from './parse.js';
/**
* Exclude/omit all `AstNode` attributes recursively.

View File

@ -1,51 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
type CustomPatternMatcherReturn = [string] & { payload?: any };
export type CustomPatternMatcherFunc = (
text: string,
offset: number,
tokens: IToken[],
groups: {
[groupName: string]: IToken[];
}
) => CustomPatternMatcherReturn | RegExpExecArray | null;
interface ICustomPattern {
exec: CustomPatternMatcherFunc;
}
type TokenPattern = RegExp | string | CustomPatternMatcherFunc | ICustomPattern;
export interface IToken {
image: string;
startOffset: number;
startLine?: number;
startColumn?: number;
endOffset?: number;
endLine?: number;
endColumn?: number;
isInsertedInRecovery?: boolean;
tokenTypeIdx: number;
tokenType: TokenType;
payload?: any;
}
export interface TokenType {
name: string;
GROUP?: string;
PATTERN?: TokenPattern;
LABEL?: string;
LONGER_ALT?: TokenType | TokenType[];
POP_MODE?: boolean;
PUSH_MODE?: string;
LINE_BREAKS?: boolean;
CATEGORIES?: TokenType[];
tokenTypeIdx?: number;
categoryMatches?: number[];
categoryMatchesMap?: {
[tokType: number]: boolean;
};
isParent?: boolean;
START_CHARS_HINT?: (string | number)[];
}

View File

@ -5,15 +5,19 @@ interface Common {
}
fragment TitleAndAccessibilities:
((accDescr=ACC_DESCR | accTitle=ACC_TITLE | title=TITLE) NEWLINE+)+
((accDescr=ACC_DESCR | accTitle=ACC_TITLE | title=TITLE) EOL)+
;
fragment EOL returns string:
NEWLINE+ | EOF
;
terminal NEWLINE: /\r?\n/;
terminal ACC_DESCR: /accDescr([\t ]*:[^\n\r]*(?=%%)|\s*{[^}]*})|accDescr([\t ]*:[^\n\r]*|\s*{[^}]*})/;
terminal ACC_TITLE: /accTitle[\t ]*:[^\n\r]*(?=%%)|accTitle[\t ]*:[^\n\r]*/;
terminal TITLE: /title([\t ][^\n\r]*|)(?=%%)|title([\t ][^\n\r]*|)/;
terminal ACC_DESCR: /[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/;
terminal ACC_TITLE: /[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/;
terminal TITLE: /[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/;
hidden terminal WHITESPACE: /[\t ]+/;
hidden terminal YAML: /---[\t ]*\r?\n[\S\s]*?---[\t ]*(?!.)/;
hidden terminal DIRECTIVE: /[\t ]*%%{[\S\s]*?}%%\s*/;
hidden terminal YAML: /---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/;
hidden terminal DIRECTIVE: /[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/;
hidden terminal SINGLE_LINE_COMMENT: /[\t ]*%%[^\n\r]*/;

View File

@ -1,3 +1,2 @@
export * from './lexer.js';
export * from './tokenBuilder.js';
export { MermaidValueConverter } from './valueConverter.js';
export * from './valueConverter.js';

View File

@ -1,8 +0,0 @@
import type { LexerResult } from 'langium';
import { DefaultLexer } from 'langium';
export class CommonLexer extends DefaultLexer {
public override tokenize(text: string): LexerResult {
return super.tokenize(text + '\n');
}
}

View File

@ -1,14 +1,14 @@
/**
* Matches single and multiline accessible description
* Matches single and multi line accessible description
*/
export const accessibilityDescrRegex = /accDescr(?:[\t ]*:[\t ]*([^\n\r]*)|\s*{([^}]*)})/;
export const accessibilityDescrRegex = /accDescr(?:[\t ]*:([^\n\r]*)|\s*{([^}]*)})/;
/**
* Matches single line accessible title
*/
export const accessibilityTitleRegex = /accTitle[\t ]*:[\t ]*([^\n\r]*)/;
export const accessibilityTitleRegex = /accTitle[\t ]*:([^\n\r]*)/;
/**
* Matches a single line title
*/
export const titleRegex = /title([\t ]+([^\n\r]*)|)/;
export const titleRegex = /title([\t ][^\n\r]*|)/;

View File

@ -1,9 +1,9 @@
import type { GrammarAST, Stream, TokenBuilderOptions } from 'langium';
import type { TokenType } from '../chevrotainWrapper.js';
import type { TokenType } from 'chevrotain';
import { DefaultTokenBuilder } from 'langium';
export abstract class MermaidTokenBuilder extends DefaultTokenBuilder {
export abstract class AbstractMermaidTokenBuilder extends DefaultTokenBuilder {
private keywords: Set<string>;
public constructor(keywords: string[]) {
@ -20,9 +20,11 @@ export abstract class MermaidTokenBuilder extends DefaultTokenBuilder {
// to restrict users, they mustn't have any non-whitespace characters after the keyword.
tokenTypes.forEach((tokenType: TokenType): void => {
if (this.keywords.has(tokenType.name) && tokenType.PATTERN !== undefined) {
tokenType.PATTERN = new RegExp(tokenType.PATTERN.toString() + '(?!\\S)');
tokenType.PATTERN = new RegExp(tokenType.PATTERN.toString() + '(?:(?=%%)|(?!\\S))');
}
});
return tokenTypes;
}
}
export class CommonTokenBuilder extends AbstractMermaidTokenBuilder {}

View File

@ -10,7 +10,7 @@ const rulesRegexes: Record<string, RegExp> = {
TITLE: titleRegex,
};
export abstract class MermaidValueConverter extends DefaultValueConverter {
export abstract class AbstractMermaidValueConverter extends DefaultValueConverter {
/**
* A method contains convert logic to be used by class.
*
@ -71,8 +71,8 @@ export abstract class MermaidValueConverter extends DefaultValueConverter {
}
}
export class CommonValueConverter extends MermaidValueConverter {
protected runCustomConverter(
export class CommonValueConverter extends AbstractMermaidValueConverter {
protected override runCustomConverter(
_rule: GrammarAST.AbstractRule,
_input: string,
_cstNode: CstNode

View File

@ -1,7 +1,25 @@
export * from './generated/ast.js';
export * from './generated/grammar.js';
export * from './generated/module.js';
export {
Info,
MermaidAstType,
Packet,
PacketBlock,
Pie,
PieSection,
isCommon,
isInfo,
isPacket,
isPacketBlock,
isPie,
isPieSection,
} from './generated/ast.js';
export {
InfoGeneratedModule,
MermaidGeneratedSharedModule,
PacketGeneratedModule,
PieGeneratedModule,
} from './generated/module.js';
export * from './common/index.js';
export * from './info/index.js';
export * from './packet/index.js';
export * from './pie/index.js';

View File

@ -7,8 +7,7 @@ import type {
} from 'langium';
import { EmptyFileSystem, createDefaultModule, createDefaultSharedModule, inject } from 'langium';
import { CommonLexer } from '../common/lexer.js';
import { CommonValueConverter } from '../common/valueConverter.js';
import { CommonValueConverter } from '../common/index.js';
import { InfoGeneratedModule, MermaidGeneratedSharedModule } from '../generated/module.js';
import { InfoTokenBuilder } from './tokenBuilder.js';
@ -17,7 +16,6 @@ import { InfoTokenBuilder } from './tokenBuilder.js';
*/
type InfoAddedServices = {
parser: {
Lexer: CommonLexer;
TokenBuilder: InfoTokenBuilder;
ValueConverter: CommonValueConverter;
};
@ -34,7 +32,6 @@ export type InfoServices = LangiumServices & InfoAddedServices;
*/
export const InfoModule: Module<InfoServices, PartialLangiumServices & InfoAddedServices> = {
parser: {
Lexer: (services: InfoServices) => new CommonLexer(services),
TokenBuilder: () => new InfoTokenBuilder(),
ValueConverter: () => new CommonValueConverter(),
},

View File

@ -1,6 +1,6 @@
import { MermaidTokenBuilder } from '../common/index.js';
import { AbstractMermaidTokenBuilder } from '../common/index.js';
export class InfoTokenBuilder extends MermaidTokenBuilder {
export class InfoTokenBuilder extends AbstractMermaidTokenBuilder {
public constructor() {
super(['info', 'showInfo']);
}

View File

@ -6,7 +6,6 @@ import type {
PartialLangiumServices,
} from 'langium';
import { EmptyFileSystem, createDefaultModule, createDefaultSharedModule, inject } from 'langium';
import { CommonLexer } from '../common/lexer.js';
import { CommonValueConverter } from '../common/valueConverter.js';
import { MermaidGeneratedSharedModule, PacketGeneratedModule } from '../generated/module.js';
import { PacketTokenBuilder } from './tokenBuilder.js';
@ -16,7 +15,6 @@ import { PacketTokenBuilder } from './tokenBuilder.js';
*/
type PacketAddedServices = {
parser: {
Lexer: CommonLexer;
TokenBuilder: PacketTokenBuilder;
ValueConverter: CommonValueConverter;
};
@ -33,7 +31,6 @@ export type PacketServices = LangiumServices & PacketAddedServices;
*/
export const PacketModule: Module<PacketServices, PartialLangiumServices & PacketAddedServices> = {
parser: {
Lexer: (services: PacketServices) => new CommonLexer(services),
TokenBuilder: () => new PacketTokenBuilder(),
ValueConverter: () => new CommonValueConverter(),
},

View File

@ -12,7 +12,7 @@ entry Packet:
;
PacketBlock:
start=INT('-' end=INT)? ':' label=STRING NEWLINE+
start=INT('-' end=INT)? ':' label=STRING EOL
;
terminal INT returns number: /0|[1-9][0-9]*/;

View File

@ -1,6 +1,6 @@
import { MermaidTokenBuilder } from '../common/index.js';
import { AbstractMermaidTokenBuilder } from '../common/index.js';
export class PacketTokenBuilder extends MermaidTokenBuilder {
export class PacketTokenBuilder extends AbstractMermaidTokenBuilder {
public constructor() {
super(['packet-beta']);
}

View File

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

View File

@ -0,0 +1,65 @@
import type {
DefaultSharedModuleContext,
LangiumServices,
LangiumSharedServices,
Module,
PartialLangiumServices,
} from 'langium';
import { EmptyFileSystem, createDefaultModule, createDefaultSharedModule, inject } from 'langium';
import { MermaidGeneratedSharedModule, PieGeneratedModule } from '../generated/module.js';
import { PieTokenBuilder } from './tokenBuilder.js';
import { PieValueConverter } from './valueConverter.js';
/**
* Declaration of `Pie` services.
*/
type PieAddedServices = {
parser: {
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.
*/
export const PieModule: Module<PieServices, PartialLangiumServices & PieAddedServices> = {
parser: {
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,19 @@
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 EOL
;
terminal PIE_SECTION_LABEL: /"[^"]+"/;
terminal PIE_SECTION_VALUE returns number: /(0|[1-9][0-9]*)(\.[0-9]+)?/;

View File

@ -0,0 +1,7 @@
import { AbstractMermaidTokenBuilder } from '../common/index.js';
export class PieTokenBuilder extends AbstractMermaidTokenBuilder {
public constructor() {
super(['pie', 'showData']);
}
}

View File

@ -0,0 +1,17 @@
import type { CstNode, GrammarAST, ValueType } from 'langium';
import { AbstractMermaidValueConverter } from '../common/index.js';
export class PieValueConverter extends AbstractMermaidValueConverter {
protected runCustomConverter(
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 undefined;
}
return input.replace(/"/g, '').trim();
}
}

View File

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

View File

@ -1,26 +1,9 @@
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';
import { noErrorsOrAlternatives } from './test-util.js';
const services: InfoServices = createInfoServices().Info;
const parser: LangiumParser = services.parser.LangiumParser;
function createInfoTestServices(): {
services: InfoServices;
parse: (input: string) => ParseResult<Info>;
} {
const parse = (input: string) => {
return parser.parse<Info>(input);
};
return { services, parse };
}
import { Info } from '../src/language/index.js';
import { expectNoErrorsOrAlternatives, infoParse as parse } from './test-util.js';
describe('info', () => {
const { parse } = createInfoTestServices();
it.each([
`info`,
`
@ -32,26 +15,34 @@ describe('info', () => {
`,
])('should handle empty info', (context: string) => {
const result = parse(context);
noErrorsOrAlternatives(result);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Info);
});
it.each([
`info showInfo`,
`info showInfo
`,
`
info showInfo`,
`info
showInfo`,
`info
showInfo
`,
`
info
showInfo
`,
`
info
showInfo`,
`
info showInfo
`,
])('should handle showInfo', (context: string) => {
const result = parse(context);
noErrorsOrAlternatives(result);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Info);
});
});

View File

@ -0,0 +1,229 @@
import { describe, expect, it } from 'vitest';
import { Pie } from '../src/language/index.js';
import { expectNoErrorsOrAlternatives, pieParse as parse } from './test-util.js';
describe('pie', () => {
it.each([
`pie`,
` pie `,
`\tpie\t`,
`
\tpie
`,
])('should handle regular pie', (context: string) => {
const result = parse(context);
expectNoErrorsOrAlternatives(result);
expect(result.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);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Pie);
const { showData } = result.value;
expect(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);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Pie);
const { title } = result.value;
expect(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);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Pie);
const { title } = result.value;
expect(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);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Pie);
const { showData, title } = result.value;
expect(showData).toBeTruthy();
expect(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);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Pie);
const { showData, title } = result.value;
expect(showData).toBeTruthy();
expect(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);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Pie);
const { sections } = result.value;
expect(sections[0].label).toBe('GitHub');
expect(sections[0].value).toBe(100);
expect(sections[1].label).toBe('GitLab');
expect(sections[1].value).toBe(50);
});
it('should handle sections with showData', () => {
const context = `pie showData
"GitHub": 100
"GitLab": 50`;
const result = parse(context);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Pie);
const { showData, sections } = result.value;
expect(showData).toBeTruthy();
expect(sections[0].label).toBe('GitHub');
expect(sections[0].value).toBe(100);
expect(sections[1].label).toBe('GitLab');
expect(sections[1].value).toBe(50);
});
it('should handle sections with title', () => {
const context = `pie title sample wow
"GitHub": 100
"GitLab": 50`;
const result = parse(context);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Pie);
const { title, sections } = result.value;
expect(title).toBe('sample wow');
expect(sections[0].label).toBe('GitHub');
expect(sections[0].value).toBe(100);
expect(sections[1].label).toBe('GitLab');
expect(sections[1].value).toBe(50);
});
it('should handle sections with accTitle', () => {
const context = `pie accTitle: sample wow
"GitHub": 100
"GitLab": 50`;
const result = parse(context);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Pie);
const { accTitle, sections } = result.value;
expect(accTitle).toBe('sample wow');
expect(sections[0].label).toBe('GitHub');
expect(sections[0].value).toBe(100);
expect(sections[1].label).toBe('GitLab');
expect(sections[1].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);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Pie);
const { accDescr, sections } = result.value;
expect(accDescr).toBe('sample wow');
expect(sections[0].label).toBe('GitHub');
expect(sections[0].value).toBe(100);
expect(sections[1].label).toBe('GitLab');
expect(sections[1].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);
expectNoErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Pie);
const { accDescr, sections } = result.value;
expect(accDescr).toBe('sample wow');
expect(sections[0].label).toBe('GitHub');
expect(sections[0].value).toBe(100);
expect(sections[1].label).toBe('GitLab');
expect(sections[1].value).toBe(50);
});
});
});
});

View File

@ -1,5 +1,7 @@
import type { LangiumParser, ParseResult } from 'langium';
import { expect, vi } from 'vitest';
import type { ParseResult } from 'langium';
import type { Info, InfoServices, Pie, PieServices } from '../src/language/index.js';
import { createInfoServices, createPieServices } from '../src/language/index.js';
const consoleMock = vi.spyOn(console, 'log').mockImplementation(() => undefined);
@ -9,10 +11,32 @@ const consoleMock = vi.spyOn(console, 'log').mockImplementation(() => undefined)
*
* @param result - the result `parse` function.
*/
export function noErrorsOrAlternatives(result: ParseResult) {
export function expectNoErrorsOrAlternatives(result: ParseResult) {
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
expect(consoleMock).not.toHaveBeenCalled();
consoleMock.mockReset();
}
const infoServices: InfoServices = createInfoServices().Info;
const infoParser: LangiumParser = infoServices.parser.LangiumParser;
export function createInfoTestServices() {
const parse = (input: string) => {
return infoParser.parse<Info>(input);
};
return { services: infoServices, parse };
}
export const infoParse = createInfoTestServices().parse;
const pieServices: PieServices = createPieServices().Pie;
const pieParser: LangiumParser = pieServices.parser.LangiumParser;
export function createPieTestServices() {
const parse = (input: string) => {
return pieParser.parse<Pie>(input);
};
return { services: pieServices, parse };
}
export const pieParse = createPieTestServices().parse;

77
pnpm-lock.yaml generated
View File

@ -496,12 +496,12 @@ importers:
packages/parser:
dependencies:
langium:
specifier: 2.0.1
version: 2.0.1
specifier: 2.1.2
version: 2.1.2
devDependencies:
langium-cli:
specifier: 2.0.1
version: 2.0.1
chevrotain:
specifier: ^11.0.3
version: 11.0.3
tests/webpack:
dependencies:
@ -11968,20 +11968,6 @@ packages:
engines: {node: '>=12'}
dev: true
/langium-cli@2.0.1:
resolution: {integrity: sha512-dPPaHimIoCgELED4tvRGdU3i26tjWuyVwexXgPtTtTzp1MBdGCBLppLADXHkL8yFVdWM/PWlCq06YyqAT4eV3A==}
engines: {node: '>=16.0.0'}
hasBin: true
dependencies:
chalk: 5.3.0
commander: 10.0.1
fs-extra: 11.1.1
jsonschema: 1.4.1
langium: 2.0.1
langium-railroad: 2.0.0
lodash: 4.17.21
dev: true
/langium-cli@2.1.0:
resolution: {integrity: sha512-Gbj4CvfAc1gP/6ihxikd2Je95j1FWjXZu8bbji2/t2vQ6kEP+vs9Fx7kSGOM0AbU/hjZfy6E35bJPOdwsiyqTA==}
engines: {node: '>=16.0.0'}
@ -11991,37 +11977,20 @@ packages:
commander: 11.0.0
fs-extra: 11.1.1
jsonschema: 1.4.1
langium: 2.1.3
langium: 2.1.2
langium-railroad: 2.1.0
lodash: 4.17.21
dev: true
/langium-railroad@2.0.0:
resolution: {integrity: sha512-g6y8vPh4i7ll/Q4D9aFrjk4UgtkuzkE6WGfiTHJHTFlDwHoiKrPSIIBZO4wjEb3XUF9P5vIt7aRjerTy7Jgm0g==}
dependencies:
langium: 2.0.1
railroad-diagrams: 1.0.0
dev: true
/langium-railroad@2.1.0:
resolution: {integrity: sha512-2IeAIUSTQzbDjNnJA+0ql8tyN/mhCSN4FS50Mo9LOtLj523qUEBwHflDmCiOGZzW9iZdni6NXJgh8nLqjhTlDw==}
dependencies:
langium: 2.1.3
langium: 2.1.2
railroad-diagrams: 1.0.0
dev: true
/langium@2.0.1:
resolution: {integrity: sha512-EGi8NNN/5zxcUL//sA4kqpV9YVOZfDngwkkSxsZ/zfx4Wjdg9von71rWIMCV6kW1M40kPOKF6e8oMTyWeX92fg==}
engines: {node: '>=16.0.0'}
dependencies:
chevrotain: 11.0.3
chevrotain-allstar: 0.3.1(chevrotain@11.0.3)
vscode-languageserver: 8.0.2
vscode-languageserver-textdocument: 1.0.8
vscode-uri: 3.0.7
/langium@2.1.3:
resolution: {integrity: sha512-/WN1xHoNBg0mi1Jp9ydMFSHIv8Jhq7K+0stNVURdoG4NgZx4/06AfNeeixmmU8X842wBl9gFZJP5O93Ge5Oasw==}
/langium@2.1.2:
resolution: {integrity: sha512-1NDUmhm111xs6NLh1DzQ9YPrOhL6JqJryY9igPIGrG0AbKKGmGf3fahAiY1MUChwIYSec6Fvoj+igwKzvGXQog==}
engines: {node: '>=16.0.0'}
dependencies:
chevrotain: 11.0.3
@ -12029,7 +11998,6 @@ packages:
vscode-languageserver: 9.0.1
vscode-languageserver-textdocument: 1.0.11
vscode-uri: 3.0.8
dev: true
/layout-base@1.0.2:
resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==}
@ -16502,37 +16470,22 @@ packages:
vscode-uri: 3.0.7
dev: true
/vscode-jsonrpc@8.0.2:
resolution: {integrity: sha512-RY7HwI/ydoC1Wwg4gJ3y6LpU9FJRZAUnTYMXthqhFXXu77ErDd/xkREpGuk4MyYkk4a+XDWAMqe0S3KkelYQEQ==}
engines: {node: '>=14.0.0'}
/vscode-jsonrpc@8.2.0:
resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==}
engines: {node: '>=14.0.0'}
dev: true
/vscode-languageserver-protocol@3.17.2:
resolution: {integrity: sha512-8kYisQ3z/SQ2kyjlNeQxbkkTNmVFoQCqkmGrzLH6A9ecPlgTbp3wDTnUNqaUxYr4vlAcloxx8zwy7G5WdguYNg==}
dependencies:
vscode-jsonrpc: 8.0.2
vscode-languageserver-types: 3.17.2
/vscode-languageserver-protocol@3.17.5:
resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==}
dependencies:
vscode-jsonrpc: 8.2.0
vscode-languageserver-types: 3.17.5
dev: true
/vscode-languageserver-textdocument@1.0.11:
resolution: {integrity: sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==}
dev: true
/vscode-languageserver-textdocument@1.0.8:
resolution: {integrity: sha512-1bonkGqQs5/fxGT5UchTgjGVnfysL0O8v1AYMBjqTbWQTFn721zaPGDYFkOKtfDgFiSgXM3KwaG3FMGfW4Ed9Q==}
/vscode-languageserver-types@3.17.2:
resolution: {integrity: sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA==}
dev: true
/vscode-languageserver-types@3.17.3:
resolution: {integrity: sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==}
@ -16540,20 +16493,12 @@ packages:
/vscode-languageserver-types@3.17.5:
resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==}
dev: true
/vscode-languageserver@8.0.2:
resolution: {integrity: sha512-bpEt2ggPxKzsAOZlXmCJ50bV7VrxwCS5BI4+egUmure/oI/t4OlFzi/YNtVvY24A2UDOZAgwFGgnZPwqSJubkA==}
hasBin: true
dependencies:
vscode-languageserver-protocol: 3.17.2
/vscode-languageserver@9.0.1:
resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==}
hasBin: true
dependencies:
vscode-languageserver-protocol: 3.17.5
dev: true
/vscode-nls@5.2.0:
resolution: {integrity: sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==}
@ -16569,10 +16514,10 @@ packages:
/vscode-uri@3.0.7:
resolution: {integrity: sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==}
dev: true
/vscode-uri@3.0.8:
resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==}
dev: true
/vue-demi@0.13.11(vue@3.4.15):
resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==}