mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-01-28 07:03:17 +08:00
Merge pull request #4727 from Yokozuna59/add-info-langium-parser
feat: add `@mermaid-js/parser` package and `info` langium parser
This commit is contained in:
commit
44b93c039a
@ -2,6 +2,11 @@
|
||||
* Shared common options for both ESBuild and Vite
|
||||
*/
|
||||
export const packageOptions = {
|
||||
parser: {
|
||||
name: 'mermaid-parser',
|
||||
packageName: 'parser',
|
||||
file: 'index.ts',
|
||||
},
|
||||
mermaid: {
|
||||
name: 'mermaid',
|
||||
packageName: 'mermaid',
|
||||
|
5
.build/generateLangium.ts
Normal file
5
.build/generateLangium.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { generate } from 'langium-cli';
|
||||
|
||||
export async function generateLangium() {
|
||||
await generate({ file: `./packages/parser/langium-config.json` });
|
||||
}
|
9
.build/langium-cli.d.ts
vendored
Normal file
9
.build/langium-cli.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
declare module 'langium-cli' {
|
||||
export interface GenerateOptions {
|
||||
file?: string;
|
||||
mode?: 'development' | 'production';
|
||||
watch?: boolean;
|
||||
}
|
||||
|
||||
export function generate(options: GenerateOptions): Promise<boolean>;
|
||||
}
|
@ -2,6 +2,7 @@ import { build } from 'esbuild';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { MermaidBuildOptions, defaultOptions, getBuildConfig } from './util.js';
|
||||
import { packageOptions } from '../.build/common.js';
|
||||
import { generateLangium } from '../.build/generateLangium.js';
|
||||
|
||||
const shouldVisualize = process.argv.includes('--visualize');
|
||||
|
||||
@ -52,9 +53,13 @@ const handler = (e) => {
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
await generateLangium();
|
||||
await mkdir('stats').catch(() => {});
|
||||
const packageNames = Object.keys(packageOptions) as (keyof typeof packageOptions)[];
|
||||
await Promise.allSettled(packageNames.map((pkg) => buildPackage(pkg).catch(handler)));
|
||||
// it should build `parser` before `mermaid` because it's a dependecy
|
||||
for (const pkg of packageNames) {
|
||||
await buildPackage(pkg).catch(handler);
|
||||
}
|
||||
};
|
||||
|
||||
void main();
|
||||
|
@ -4,7 +4,11 @@ import cors from 'cors';
|
||||
import { getBuildConfig, defaultOptions } from './util.js';
|
||||
import { context } from 'esbuild';
|
||||
import chokidar from 'chokidar';
|
||||
import { generateLangium } from '../.build/generateLangium.js';
|
||||
|
||||
const parserCtx = await context(
|
||||
getBuildConfig({ ...defaultOptions, minify: false, core: false, entryName: 'parser' })
|
||||
);
|
||||
const mermaidCtx = await context(
|
||||
getBuildConfig({ ...defaultOptions, minify: false, core: false, entryName: 'mermaid' })
|
||||
);
|
||||
@ -28,7 +32,7 @@ const externalCtx = await context(
|
||||
const zenumlCtx = await context(
|
||||
getBuildConfig({ ...defaultOptions, minify: false, core: false, entryName: 'mermaid-zenuml' })
|
||||
);
|
||||
const contexts = [mermaidCtx, mermaidIIFECtx, externalCtx, zenumlCtx];
|
||||
const contexts = [parserCtx, mermaidCtx, mermaidIIFECtx, externalCtx, zenumlCtx];
|
||||
|
||||
const rebuildAll = async () => {
|
||||
console.time('Rebuild time');
|
||||
@ -75,10 +79,11 @@ function sendEventsToAll() {
|
||||
}
|
||||
|
||||
async function createServer() {
|
||||
await generateLangium();
|
||||
handleFileChange();
|
||||
const app = express();
|
||||
chokidar
|
||||
.watch('**/src/**/*.{js,ts,yaml,json}', {
|
||||
.watch('**/src/**/*.{js,ts,langium,yaml,json}', {
|
||||
ignoreInitial: true,
|
||||
ignored: [/node_modules/, /dist/, /docs/, /coverage/],
|
||||
})
|
||||
@ -87,12 +92,16 @@ async function createServer() {
|
||||
if (!['add', 'change'].includes(event)) {
|
||||
return;
|
||||
}
|
||||
if (/\.langium$/.test(path)) {
|
||||
await generateLangium();
|
||||
}
|
||||
console.log(`${path} changed. Rebuilding...`);
|
||||
handleFileChange();
|
||||
});
|
||||
|
||||
app.use(cors());
|
||||
app.get('/events', eventsHandler);
|
||||
app.use(express.static('./packages/parser/dist'));
|
||||
app.use(express.static('./packages/mermaid/dist'));
|
||||
app.use(express.static('./packages/mermaid-zenuml/dist'));
|
||||
app.use(express.static('./packages/mermaid-example-diagram/dist'));
|
||||
|
@ -6,3 +6,6 @@ cypress/plugins/index.js
|
||||
coverage
|
||||
*.json
|
||||
node_modules
|
||||
|
||||
# autogenereated by langium-cli
|
||||
generated/
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -47,3 +47,6 @@ stats/
|
||||
demos/dev/**
|
||||
!/demos/dev/example.html
|
||||
!/demos/dev/reload.js
|
||||
|
||||
# autogenereated by langium-cli
|
||||
generated/
|
||||
|
@ -10,3 +10,6 @@ stats
|
||||
.nyc_output
|
||||
# Autogenerated by `pnpm run --filter mermaid types:build-config`
|
||||
packages/mermaid/src/config.type.ts
|
||||
|
||||
# autogenereated by langium-cli
|
||||
generated/
|
||||
|
@ -8,6 +8,7 @@ import { visualizer } from 'rollup-plugin-visualizer';
|
||||
import type { TemplateType } from 'rollup-plugin-visualizer/dist/plugin/template-types.js';
|
||||
import istanbul from 'vite-plugin-istanbul';
|
||||
import { packageOptions } from '../.build/common.js';
|
||||
import { generateLangium } from '../.build/generateLangium.js';
|
||||
|
||||
const visualize = process.argv.includes('--visualize');
|
||||
const watch = process.argv.includes('--watch');
|
||||
@ -82,7 +83,7 @@ export const getBuildConfig = ({ minify, core, watch, entryName }: BuildOptions)
|
||||
// @ts-expect-error According to the type definitions, rollup plugins are incompatible with vite
|
||||
typescript({ compilerOptions: { declaration: false } }),
|
||||
istanbul({
|
||||
exclude: ['node_modules', 'test/', '__mocks__'],
|
||||
exclude: ['node_modules', 'test/', '__mocks__', 'generated'],
|
||||
extension: ['.js', '.ts'],
|
||||
requireEnv: true,
|
||||
forceBuildInstrument: coverage,
|
||||
@ -106,18 +107,24 @@ const buildPackage = async (entryName: keyof typeof packageOptions) => {
|
||||
|
||||
const main = async () => {
|
||||
const packageNames = Object.keys(packageOptions) as (keyof typeof packageOptions)[];
|
||||
for (const pkg of packageNames.filter((pkg) => !mermaidOnly || pkg === 'mermaid')) {
|
||||
for (const pkg of packageNames.filter(
|
||||
(pkg) => !mermaidOnly || pkg === 'mermaid' || pkg === 'parser'
|
||||
)) {
|
||||
await buildPackage(pkg);
|
||||
}
|
||||
};
|
||||
|
||||
await generateLangium();
|
||||
|
||||
if (watch) {
|
||||
await build(getBuildConfig({ minify: false, watch, core: false, entryName: 'parser' }));
|
||||
build(getBuildConfig({ minify: false, watch, core: false, entryName: 'mermaid' }));
|
||||
if (!mermaidOnly) {
|
||||
build(getBuildConfig({ minify: false, watch, entryName: 'mermaid-example-diagram' }));
|
||||
build(getBuildConfig({ minify: false, watch, entryName: 'mermaid-zenuml' }));
|
||||
}
|
||||
} else if (visualize) {
|
||||
await build(getBuildConfig({ minify: false, watch, core: false, entryName: 'parser' }));
|
||||
await build(getBuildConfig({ minify: false, core: true, entryName: 'mermaid' }));
|
||||
await build(getBuildConfig({ minify: false, core: false, entryName: 'mermaid' }));
|
||||
} else {
|
||||
|
@ -14,6 +14,7 @@ async function createServer() {
|
||||
});
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.static('./packages/parser/dist'));
|
||||
app.use(express.static('./packages/mermaid/dist'));
|
||||
app.use(express.static('./packages/mermaid-zenuml/dist'));
|
||||
app.use(express.static('./packages/mermaid-example-diagram/dist'));
|
||||
|
@ -71,6 +71,7 @@
|
||||
"knut",
|
||||
"knutsveidqvist",
|
||||
"laganeckas",
|
||||
"langium",
|
||||
"linetype",
|
||||
"lintstagedrc",
|
||||
"logmsg",
|
||||
|
@ -15,11 +15,11 @@
|
||||
"git graph"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "pnpm run -r clean && pnpm build:esbuild && pnpm build:types",
|
||||
"build": "pnpm build:esbuild && pnpm build:types",
|
||||
"build:esbuild": "pnpm run -r clean && ts-node-esm --transpileOnly .esbuild/build.ts",
|
||||
"build:mermaid": "pnpm build:esbuild --mermaid",
|
||||
"build:viz": "pnpm build:esbuild --visualize",
|
||||
"build:types": "tsc -p ./packages/mermaid/tsconfig.json --emitDeclarationOnly && tsc -p ./packages/mermaid-zenuml/tsconfig.json --emitDeclarationOnly && tsc -p ./packages/mermaid-example-diagram/tsconfig.json --emitDeclarationOnly",
|
||||
"build:types": "tsc -p ./packages/parser/tsconfig.json --emitDeclarationOnly && tsc -p ./packages/mermaid/tsconfig.json --emitDeclarationOnly && tsc -p ./packages/mermaid-zenuml/tsconfig.json --emitDeclarationOnly && tsc -p ./packages/mermaid-example-diagram/tsconfig.json --emitDeclarationOnly",
|
||||
"dev": "ts-node-esm --transpileOnly .esbuild/server.ts",
|
||||
"dev:vite": "ts-node-esm --transpileOnly .vite/server.ts",
|
||||
"dev:coverage": "pnpm coverage:cypress:clean && VITE_COVERAGE=true pnpm dev:vite",
|
||||
@ -107,6 +107,7 @@
|
||||
"jison": "^0.4.18",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsdom": "^22.0.0",
|
||||
"langium-cli": "2.0.1",
|
||||
"lint-staged": "^13.2.1",
|
||||
"nyc": "^15.1.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
|
@ -5,7 +5,6 @@
|
||||
* This is a dummy parser that satisfies the mermaid API logic.
|
||||
*/
|
||||
export default {
|
||||
parser: { yy: {} },
|
||||
parse: () => {
|
||||
// no op
|
||||
},
|
||||
|
@ -72,6 +72,7 @@
|
||||
"khroma": "^2.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mdast-util-from-markdown": "^1.3.0",
|
||||
"mermaid-parser": "workspace:^",
|
||||
"stylis": "^4.1.3",
|
||||
"ts-dedent": "^2.2.0",
|
||||
"uuid": "^9.0.0"
|
||||
|
@ -50,7 +50,10 @@ export class Diagram {
|
||||
this.parser.parse = (text: string) =>
|
||||
originalParse(cleanupComments(extractFrontMatter(text, this.db, configApi.addDirective)));
|
||||
|
||||
this.parser.parser.yy = this.db;
|
||||
if (this.parser.parser !== undefined) {
|
||||
// The parser.parser.yy is only present in JISON parsers. So, we'll only set if required.
|
||||
this.parser.parser.yy = this.db;
|
||||
}
|
||||
this.init = diagram.init;
|
||||
this.parse();
|
||||
}
|
||||
|
@ -45,7 +45,6 @@ export const addDiagrams = () => {
|
||||
styles: {}, // should never be used
|
||||
renderer: {}, // should never be used
|
||||
parser: {
|
||||
parser: { yy: {} },
|
||||
parse: () => {
|
||||
throw new Error(
|
||||
'Diagrams beginning with --- are not valid. ' +
|
||||
|
@ -39,7 +39,6 @@ describe('DiagramAPI', () => {
|
||||
parse: (_text) => {
|
||||
return;
|
||||
},
|
||||
parser: { yy: {} },
|
||||
},
|
||||
renderer: {},
|
||||
styles: {},
|
||||
|
@ -80,7 +80,7 @@ export type DrawDefinition = (
|
||||
|
||||
export interface ParserDefinition {
|
||||
parse: (text: string) => void;
|
||||
parser: { yy: DiagramDB };
|
||||
parser?: { yy: DiagramDB };
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -30,9 +30,6 @@ describe('diagram detection', () => {
|
||||
parse: () => {
|
||||
// no-op
|
||||
},
|
||||
parser: {
|
||||
yy: {},
|
||||
},
|
||||
},
|
||||
renderer: {},
|
||||
styles: {},
|
||||
|
15
packages/mermaid/src/diagrams/common/populateCommonDb.ts
Normal file
15
packages/mermaid/src/diagrams/common/populateCommonDb.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import type { DiagramAST } from 'mermaid-parser';
|
||||
|
||||
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||
|
||||
export function populateCommonDb(ast: DiagramAST, db: DiagramDB) {
|
||||
if (ast.accDescr) {
|
||||
db.setAccDescription?.(ast.accDescr);
|
||||
}
|
||||
if (ast.accTitle) {
|
||||
db.setAccTitle?.(ast.accTitle);
|
||||
}
|
||||
if (ast.title) {
|
||||
db.setDiagramTitle?.(ast.title);
|
||||
}
|
||||
}
|
@ -5,7 +5,6 @@ const diagram: DiagramDefinition = {
|
||||
db: {},
|
||||
renderer,
|
||||
parser: {
|
||||
parser: { yy: {} },
|
||||
parse: (): void => {
|
||||
return;
|
||||
},
|
||||
|
@ -1,24 +1,31 @@
|
||||
// @ts-ignore - jison doesn't export types
|
||||
import { parser } from './parser/info.jison';
|
||||
import { db } from './infoDb.js';
|
||||
|
||||
describe('info diagram', () => {
|
||||
beforeEach(() => {
|
||||
parser.yy = db;
|
||||
parser.yy.clear();
|
||||
});
|
||||
import { parser } from './infoParser.js';
|
||||
|
||||
describe('info', () => {
|
||||
it('should handle an info definition', () => {
|
||||
const str = `info`;
|
||||
parser.parse(str);
|
||||
|
||||
expect(db.getInfo()).toBeFalsy();
|
||||
expect(() => {
|
||||
parser.parse(str);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle an info definition with showInfo', () => {
|
||||
const str = `info showInfo`;
|
||||
parser.parse(str);
|
||||
expect(() => {
|
||||
parser.parse(str);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
expect(db.getInfo()).toBeTruthy();
|
||||
it('should throw because of unsupported info grammar', () => {
|
||||
const str = `info unsupported`;
|
||||
expect(() => {
|
||||
parser.parse(str);
|
||||
}).toThrow('Parsing failed: unexpected character: ->u<- at offset: 5, skipped 11 characters.');
|
||||
});
|
||||
|
||||
it('should throw because of unsupported info grammar', () => {
|
||||
const str = `info unsupported`;
|
||||
expect(() => {
|
||||
parser.parse(str);
|
||||
}).toThrow('Parsing failed: unexpected character: ->u<- at offset: 5, skipped 11 characters.');
|
||||
});
|
||||
});
|
||||
|
@ -1,23 +1,10 @@
|
||||
import type { InfoFields, InfoDB } from './infoTypes.js';
|
||||
import { version } from '../../../package.json';
|
||||
|
||||
export const DEFAULT_INFO_DB: InfoFields = {
|
||||
info: false,
|
||||
} as const;
|
||||
export const DEFAULT_INFO_DB: InfoFields = { version } as const;
|
||||
|
||||
let info: boolean = DEFAULT_INFO_DB.info;
|
||||
|
||||
export const setInfo = (toggle: boolean): void => {
|
||||
info = toggle;
|
||||
};
|
||||
|
||||
export const getInfo = (): boolean => info;
|
||||
|
||||
const clear = (): void => {
|
||||
info = DEFAULT_INFO_DB.info;
|
||||
};
|
||||
export const getVersion = (): string => DEFAULT_INFO_DB.version;
|
||||
|
||||
export const db: InfoDB = {
|
||||
clear,
|
||||
setInfo,
|
||||
getInfo,
|
||||
getVersion,
|
||||
};
|
||||
|
@ -1,6 +1,5 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
// @ts-ignore - jison doesn't export types
|
||||
import parser from './parser/info.jison';
|
||||
import { parser } from './infoParser.js';
|
||||
import { db } from './infoDb.js';
|
||||
import { renderer } from './infoRenderer.js';
|
||||
|
||||
|
12
packages/mermaid/src/diagrams/info/infoParser.ts
Normal file
12
packages/mermaid/src/diagrams/info/infoParser.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import type { Info } from 'mermaid-parser';
|
||||
import { parse } from 'mermaid-parser';
|
||||
|
||||
import { log } from '../../logger.js';
|
||||
import type { ParserDefinition } from '../../diagram-api/types.js';
|
||||
|
||||
export const parser: ParserDefinition = {
|
||||
parse: (input: string): void => {
|
||||
const ast: Info = parse('info', input);
|
||||
log.debug(ast);
|
||||
},
|
||||
};
|
@ -1,11 +1,9 @@
|
||||
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||
|
||||
export interface InfoFields {
|
||||
info: boolean;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface InfoDB extends DiagramDB {
|
||||
clear: () => void;
|
||||
setInfo: (info: boolean) => void;
|
||||
getInfo: () => boolean;
|
||||
getVersion: () => string;
|
||||
}
|
||||
|
@ -1,48 +0,0 @@
|
||||
/** mermaid
|
||||
* https://knsv.github.io/mermaid
|
||||
* (c) 2015 Knut Sveidqvist
|
||||
* MIT license.
|
||||
*/
|
||||
%lex
|
||||
|
||||
%options case-insensitive
|
||||
|
||||
%{
|
||||
// Pre-lexer code can go here
|
||||
%}
|
||||
|
||||
%%
|
||||
|
||||
"info" return 'info' ;
|
||||
[\s\n\r]+ return 'NL' ;
|
||||
[\s]+ return 'space';
|
||||
"showInfo" return 'showInfo';
|
||||
<<EOF>> return 'EOF' ;
|
||||
. return 'TXT' ;
|
||||
|
||||
/lex
|
||||
|
||||
%start start
|
||||
|
||||
%% /* language grammar */
|
||||
|
||||
start
|
||||
// %{ : info document 'EOF' { return yy; } }
|
||||
: info document 'EOF' { return yy; }
|
||||
;
|
||||
|
||||
document
|
||||
: /* empty */
|
||||
| document line
|
||||
;
|
||||
|
||||
line
|
||||
: statement { }
|
||||
| 'NL'
|
||||
;
|
||||
|
||||
statement
|
||||
: showInfo { yy.setInfo(true); }
|
||||
;
|
||||
|
||||
%%
|
@ -102,7 +102,6 @@ describe('when using mermaid and ', () => {
|
||||
parse: (_text) => {
|
||||
return;
|
||||
},
|
||||
parser: { yy: {} },
|
||||
},
|
||||
styles: () => {
|
||||
// do nothing
|
||||
|
21
packages/parser/LICENSE
Normal file
21
packages/parser/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2023 Yokozuna59
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
63
packages/parser/README.md
Normal file
63
packages/parser/README.md
Normal file
@ -0,0 +1,63 @@
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/mermaid-js/mermaid/develop/docs/public/favicon.svg" height="150">
|
||||
|
||||
</p>
|
||||
<h1 align="center">
|
||||
Mermaid Parser
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
Mermaid parser package
|
||||
<p>
|
||||
|
||||
[![NPM](https://img.shields.io/npm/v/mermaid-parser)](https://www.npmjs.com/package/mermaid-parser)
|
||||
|
||||
## How the package works
|
||||
|
||||
The package exports a `parse` function that has two parameters:
|
||||
|
||||
```ts
|
||||
declare function parse<T extends DiagramAST>(
|
||||
diagramType: keyof typeof initializers,
|
||||
text: string
|
||||
): T;
|
||||
```
|
||||
|
||||
## How does a Langium-based parser work?
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor Package
|
||||
participant Module
|
||||
participant TokenBuilder
|
||||
participant Lexer
|
||||
participant Parser
|
||||
participant ValueConverter
|
||||
|
||||
|
||||
Package ->> Module: Create services
|
||||
Module ->> TokenBuilder: Override or/and<br>reorder rules
|
||||
TokenBuilder ->> Lexer: Read the string and transform<br>it into a token stream
|
||||
Lexer ->> Parser: Parse token<br>stream into AST
|
||||
Parser ->> ValueConverter: Clean/modify tokenized<br>rules returned value
|
||||
ValueConverter -->> Package: Return AST
|
||||
```
|
||||
|
||||
- When to override `TokenBuilder`?
|
||||
|
||||
- To override keyword rules.
|
||||
- To override terminal rules that need a custom function.
|
||||
- To manually reorder the list of rules.
|
||||
|
||||
- When to override `Lexer`?
|
||||
|
||||
- To modify input before tokenizing.
|
||||
- To insert/modify tokens that cannot or have not been parsed.
|
||||
|
||||
- When to override `LangiumParser`?
|
||||
|
||||
- To insert or modify attributes that can't be parsed.
|
||||
|
||||
- When to override `ValueConverter`?
|
||||
|
||||
- To modify the returned value from the parser.
|
13
packages/parser/langium-config.json
Normal file
13
packages/parser/langium-config.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"projectName": "Mermaid",
|
||||
"languages": [
|
||||
{
|
||||
"id": "info",
|
||||
"grammar": "src/language/info/info.langium",
|
||||
"fileExtensions": [".mmd", ".mermaid"]
|
||||
}
|
||||
],
|
||||
"mode": "production",
|
||||
"importExtension": ".js",
|
||||
"out": "src/language/generated"
|
||||
}
|
48
packages/parser/package.json
Normal file
48
packages/parser/package.json
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "mermaid-parser",
|
||||
"version": "0.2.0",
|
||||
"description": "MermaidJS parser",
|
||||
"author": "Yokozuna59",
|
||||
"contributors": [
|
||||
"Yokozuna59",
|
||||
"Sidharth Vinod (https://sidharth.dev)"
|
||||
],
|
||||
"homepage": "https://github.com/mermaid-js/mermaid/tree/develop/packages/mermaid/parser/#readme",
|
||||
"types": "dist/src/index.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/mermaid-parser.esm.mjs",
|
||||
"types": "./dist/src/index.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist src/language/generated",
|
||||
"langium:generate": "langium generate",
|
||||
"langium:watch": "langium generate --watch",
|
||||
"prepublishOnly": "pnpm -w run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mermaid-js/mermaid.git",
|
||||
"directory": "packages/parser"
|
||||
},
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"mermaid",
|
||||
"parser",
|
||||
"ast"
|
||||
],
|
||||
"dependencies": {
|
||||
"langium": "2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"langium-cli": "2.0.1"
|
||||
},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
3
packages/parser/src/index.ts
Normal file
3
packages/parser/src/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export type { Info } from './language/index.js';
|
||||
export type { DiagramAST } from './parse.js';
|
||||
export { parse, MermaidParseError } from './parse.js';
|
51
packages/parser/src/language/chevrotainWrapper.ts
Normal file
51
packages/parser/src/language/chevrotainWrapper.ts
Normal file
@ -0,0 +1,51 @@
|
||||
/* 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)[];
|
||||
}
|
14
packages/parser/src/language/common/common.langium
Normal file
14
packages/parser/src/language/common/common.langium
Normal file
@ -0,0 +1,14 @@
|
||||
fragment TitleAndAccessibilities:
|
||||
((accDescr=ACC_DESCR | accTitle=ACC_TITLE | title=TITLE) NEWLINE+)+
|
||||
;
|
||||
|
||||
terminal NEWLINE: /\r?\n/;
|
||||
terminal ACC_DESCR: /accDescr(?:[\t ]*:[\t ]*[^\n\r]*?(?=%%)|\s*{[^}]*})|accDescr(?:[\t ]*:[\t ]*[^\n\r]*|\s*{[^}]*})/;
|
||||
terminal ACC_TITLE: /accTitle[\t ]*:[\t ]*[^\n\r]*?(?=%%)|accTitle[\t ]*:[\t ]*[^\n\r]*/;
|
||||
terminal TITLE: /title(?:[\t ]+[^\n\r]*?|)(?=%%)|title(?:[\t ]+[^\n\r]*|)/;
|
||||
|
||||
hidden terminal WHITESPACE: /[\t ]+/;
|
||||
// TODO: add YAML_COMMENT hidden rule without interfere actual grammar
|
||||
hidden terminal YAML: /---[\t ]*\r?\n[\S\s]*?---[\t ]*(?!.)/;
|
||||
hidden terminal DIRECTIVE: /[\t ]*%%{[\S\s]*?}%%\s*/;
|
||||
hidden terminal SINGLE_LINE_COMMENT: /[\t ]*%%[^\n\r]*/;
|
8
packages/parser/src/language/common/commonLexer.ts
Normal file
8
packages/parser/src/language/common/commonLexer.ts
Normal file
@ -0,0 +1,8 @@
|
||||
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');
|
||||
}
|
||||
}
|
14
packages/parser/src/language/common/commonMatcher.ts
Normal file
14
packages/parser/src/language/common/commonMatcher.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Matches single and multiline accessible description
|
||||
*/
|
||||
export const accessibilityDescrRegex = /accDescr(?:[\t ]*:[\t ]*([^\n\r]*)|\s*{([^}]*)})/;
|
||||
|
||||
/**
|
||||
* Matches single line accessible title
|
||||
*/
|
||||
export const accessibilityTitleRegex = /accTitle[\t ]*:[\t ]*([^\n\r]*)/;
|
||||
|
||||
/**
|
||||
* Matches a single line title
|
||||
*/
|
||||
export const titleRegex = /title([\t ]+([^\n\r]*)|)/;
|
74
packages/parser/src/language/common/commonValueConverters.ts
Normal file
74
packages/parser/src/language/common/commonValueConverters.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import type { CstNode, GrammarAST, ValueType } from 'langium';
|
||||
import { DefaultValueConverter } from 'langium';
|
||||
|
||||
import { accessibilityDescrRegex, accessibilityTitleRegex, titleRegex } from './commonMatcher.js';
|
||||
|
||||
export class CommonValueConverter extends DefaultValueConverter {
|
||||
protected override runConverter(
|
||||
rule: GrammarAST.AbstractRule,
|
||||
input: string,
|
||||
cstNode: CstNode
|
||||
): ValueType {
|
||||
const value: ValueType | undefined = CommonValueConverter.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 common rule or `undefined` 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 {
|
||||
let regex: RegExp | undefined;
|
||||
switch (rule.name) {
|
||||
case 'ACC_DESCR': {
|
||||
regex = new RegExp(accessibilityDescrRegex.source);
|
||||
break;
|
||||
}
|
||||
case 'ACC_TITLE': {
|
||||
regex = new RegExp(accessibilityTitleRegex.source);
|
||||
break;
|
||||
}
|
||||
case 'TITLE': {
|
||||
regex = new RegExp(titleRegex.source);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (regex === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const match = regex.exec(input);
|
||||
if (match === null) {
|
||||
return undefined;
|
||||
}
|
||||
// single line title, accTitle, accDescr
|
||||
if (match[1] !== undefined) {
|
||||
return match[1].trim().replaceAll(/[\t ]{2,}/gm, ' ');
|
||||
}
|
||||
// multi line accDescr
|
||||
if (match[2] !== undefined) {
|
||||
return match[2]
|
||||
.replaceAll(/^\s*/gm, '')
|
||||
.replaceAll(/\s+$/gm, '')
|
||||
.replaceAll(/[\t ]{2,}/gm, ' ')
|
||||
.replaceAll(/[\n\r]{2,}/gm, '\n');
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
2
packages/parser/src/language/common/index.ts
Normal file
2
packages/parser/src/language/common/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './commonLexer.js';
|
||||
export * from './commonValueConverters.js';
|
6
packages/parser/src/language/index.ts
Normal file
6
packages/parser/src/language/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export * from './generated/ast.js';
|
||||
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.
|
||||
*/
|
||||
export 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}`);
|
||||
}
|
||||
}
|
57
packages/parser/tests/info.test.ts
Normal file
57
packages/parser/tests/info.test.ts
Normal file
@ -0,0 +1,57 @@
|
||||
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 };
|
||||
}
|
||||
|
||||
describe('info', () => {
|
||||
const { parse } = createInfoTestServices();
|
||||
|
||||
it.each([
|
||||
`info`,
|
||||
`
|
||||
info`,
|
||||
`info
|
||||
`,
|
||||
`
|
||||
info
|
||||
`,
|
||||
])('should handle empty info', (context: string) => {
|
||||
const result = parse(context);
|
||||
noErrorsOrAlternatives(result);
|
||||
|
||||
expect(result.value.$type).toBe(Info);
|
||||
});
|
||||
|
||||
it.each([
|
||||
`info showInfo`,
|
||||
`
|
||||
info showInfo`,
|
||||
`info
|
||||
showInfo
|
||||
`,
|
||||
`
|
||||
info
|
||||
showInfo
|
||||
`,
|
||||
])('should handle showInfo', (context: string) => {
|
||||
const result = parse(context);
|
||||
noErrorsOrAlternatives(result);
|
||||
|
||||
expect(result.value.$type).toBe(Info);
|
||||
});
|
||||
});
|
18
packages/parser/tests/test-util.ts
Normal file
18
packages/parser/tests/test-util.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { expect, vi } from 'vitest';
|
||||
import type { ParseResult } from 'langium';
|
||||
|
||||
const consoleMock = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||
|
||||
/**
|
||||
* A helper test function that validate that the result doesn't have errors
|
||||
* or any ambiguous alternatives from chevrotain.
|
||||
*
|
||||
* @param result - the result `parse` function.
|
||||
*/
|
||||
export function noErrorsOrAlternatives(result: ParseResult) {
|
||||
expect(result.lexerErrors).toHaveLength(0);
|
||||
expect(result.parserErrors).toHaveLength(0);
|
||||
|
||||
expect(consoleMock).not.toHaveBeenCalled();
|
||||
consoleMock.mockReset();
|
||||
}
|
12
packages/parser/tsconfig.json
Normal file
12
packages/parser/tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "./dist",
|
||||
"allowJs": false,
|
||||
"preserveSymlinks": false,
|
||||
"strictPropertyInitialization": false
|
||||
},
|
||||
"include": ["./src/**/*.ts", "./tests/**/*.ts"],
|
||||
"typeRoots": ["./src/types"]
|
||||
}
|
6560
pnpm-lock.yaml
generated
6560
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
import jison from './.vite/jisonPlugin.js';
|
||||
import jsonSchemaPlugin from './.vite/jsonSchemaPlugin.js';
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { defaultExclude, defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
@ -22,7 +22,7 @@ export default defineConfig({
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html', 'lcov'],
|
||||
reportsDirectory: './coverage/vitest',
|
||||
exclude: ['**/node_modules/**', '**/tests/**', '**/__mocks__/**'],
|
||||
exclude: [...defaultExclude, './tests/**', '**/__mocks__/**', '**/generated/'],
|
||||
},
|
||||
},
|
||||
build: {
|
||||
|
Loading…
x
Reference in New Issue
Block a user