mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-18 14:59:53 +02:00
Merge branch 'develop' into 5367/architecture-diagram
This commit is contained in:
19
packages/parser/CHANGELOG.md
Normal file
19
packages/parser/CHANGELOG.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# @mermaid-js/parser
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#5664](https://github.com/mermaid-js/mermaid/pull/5664) [`5deaef4`](https://github.com/mermaid-js/mermaid/commit/5deaef456e74d796866431c26f69360e4e74dbff) Thanks [@Austin-Fulbright](https://github.com/Austin-Fulbright)! - chore: Migrate git graph to langium, use typescript for internals
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5746](https://github.com/mermaid-js/mermaid/pull/5746) [`83926c9`](https://github.com/mermaid-js/mermaid/commit/83926c9707b09c34e300888186250191ee8ae30a) Thanks [@sidharthv96](https://github.com/sidharthv96)! - test changeset
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#5744](https://github.com/mermaid-js/mermaid/pull/5744) [`5013484`](https://github.com/mermaid-js/mermaid/commit/50134849246141ec400e33e08c12c10539b84de9) Thanks [@sidharthv96](https://github.com/sidharthv96)! - Release parser, test changesets
|
@@ -19,6 +19,10 @@
|
||||
{
|
||||
"id": "architecture",
|
||||
"grammar": "src/language/architecture/architecture.langium",
|
||||
},
|
||||
{
|
||||
"id": "gitGraph",
|
||||
"grammar": "src/language/gitGraph/gitGraph.langium",
|
||||
"fileExtensions": [".mmd", ".mermaid"]
|
||||
}
|
||||
],
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mermaid-js/parser",
|
||||
"version": "0.1.0-rc.2",
|
||||
"version": "0.2.0",
|
||||
"description": "MermaidJS parser",
|
||||
"author": "Yokozuna59",
|
||||
"contributors": [
|
||||
@@ -19,8 +19,7 @@
|
||||
"scripts": {
|
||||
"clean": "rimraf dist src/language/generated",
|
||||
"langium:generate": "langium generate",
|
||||
"langium:watch": "langium generate --watch",
|
||||
"prepublishOnly": "pnpm -w run build"
|
||||
"langium:watch": "langium generate --watch"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
87
packages/parser/src/language/gitGraph/gitGraph.langium
Normal file
87
packages/parser/src/language/gitGraph/gitGraph.langium
Normal file
@@ -0,0 +1,87 @@
|
||||
grammar GitGraph
|
||||
|
||||
interface Common {
|
||||
accDescr?: string;
|
||||
accTitle?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
fragment TitleAndAccessibilities:
|
||||
((accDescr=ACC_DESCR | accTitle=ACC_TITLE | title=TITLE) EOL)+
|
||||
;
|
||||
|
||||
fragment EOL returns string:
|
||||
NEWLINE+ | EOF
|
||||
;
|
||||
|
||||
terminal NEWLINE: /\r?\n/;
|
||||
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]*?\r?\n)?---(?:\r?\n|(?!\S))/;
|
||||
hidden terminal DIRECTIVE: /[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/;
|
||||
hidden terminal SINGLE_LINE_COMMENT: /[\t ]*%%[^\n\r]*/;
|
||||
|
||||
entry GitGraph:
|
||||
NEWLINE*
|
||||
('gitGraph' | 'gitGraph' ':' | 'gitGraph:' | ('gitGraph' Direction ':'))
|
||||
NEWLINE*
|
||||
(
|
||||
NEWLINE*
|
||||
(TitleAndAccessibilities |
|
||||
statements+=Statement |
|
||||
NEWLINE)*
|
||||
)
|
||||
;
|
||||
|
||||
Statement
|
||||
: Commit
|
||||
| Branch
|
||||
| Merge
|
||||
| Checkout
|
||||
| CherryPicking
|
||||
;
|
||||
|
||||
Direction:
|
||||
dir=('LR' | 'TB' | 'BT');
|
||||
|
||||
Commit:
|
||||
'commit'
|
||||
(
|
||||
'id:' id=STRING
|
||||
|'msg:'? message=STRING
|
||||
|'tag:' tags+=STRING
|
||||
|'type:' type=('NORMAL' | 'REVERSE' | 'HIGHLIGHT')
|
||||
)* EOL;
|
||||
Branch:
|
||||
'branch' name=(ID|STRING)
|
||||
('order:' order=INT)?
|
||||
EOL;
|
||||
|
||||
Merge:
|
||||
'merge' branch=(ID|STRING)
|
||||
(
|
||||
'id:' id=STRING
|
||||
|'tag:' tags+=STRING
|
||||
|'type:' type=('NORMAL' | 'REVERSE' | 'HIGHLIGHT')
|
||||
)* EOL;
|
||||
|
||||
Checkout:
|
||||
('checkout'|'switch') branch=(ID|STRING) EOL;
|
||||
|
||||
CherryPicking:
|
||||
'cherry-pick'
|
||||
(
|
||||
'id:' id=STRING
|
||||
|'tag:' tags+=STRING
|
||||
|'parent:' parent=STRING
|
||||
)* EOL;
|
||||
|
||||
|
||||
|
||||
terminal INT returns number: /[0-9]+(?=\s)/;
|
||||
terminal ID returns string: /\w([-\./\w]*[-\w])?/;
|
||||
terminal STRING: /"[^"]*"|'[^']*'/;
|
||||
|
1
packages/parser/src/language/gitGraph/index.ts
Normal file
1
packages/parser/src/language/gitGraph/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './module.js';
|
52
packages/parser/src/language/gitGraph/module.ts
Normal file
52
packages/parser/src/language/gitGraph/module.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type {
|
||||
DefaultSharedCoreModuleContext,
|
||||
LangiumCoreServices,
|
||||
LangiumSharedCoreServices,
|
||||
Module,
|
||||
PartialLangiumCoreServices,
|
||||
} from 'langium';
|
||||
import {
|
||||
inject,
|
||||
createDefaultCoreModule,
|
||||
createDefaultSharedCoreModule,
|
||||
EmptyFileSystem,
|
||||
} from 'langium';
|
||||
import { CommonValueConverter } from '../common/valueConverter.js';
|
||||
import { MermaidGeneratedSharedModule, GitGraphGeneratedModule } from '../generated/module.js';
|
||||
import { GitGraphTokenBuilder } from './tokenBuilder.js';
|
||||
|
||||
interface GitGraphAddedServices {
|
||||
parser: {
|
||||
TokenBuilder: GitGraphTokenBuilder;
|
||||
ValueConverter: CommonValueConverter;
|
||||
};
|
||||
}
|
||||
|
||||
export type GitGraphServices = LangiumCoreServices & GitGraphAddedServices;
|
||||
|
||||
export const GitGraphModule: Module<
|
||||
GitGraphServices,
|
||||
PartialLangiumCoreServices & GitGraphAddedServices
|
||||
> = {
|
||||
parser: {
|
||||
TokenBuilder: () => new GitGraphTokenBuilder(),
|
||||
ValueConverter: () => new CommonValueConverter(),
|
||||
},
|
||||
};
|
||||
|
||||
export function createGitGraphServices(context: DefaultSharedCoreModuleContext = EmptyFileSystem): {
|
||||
shared: LangiumSharedCoreServices;
|
||||
GitGraph: GitGraphServices;
|
||||
} {
|
||||
const shared: LangiumSharedCoreServices = inject(
|
||||
createDefaultSharedCoreModule(context),
|
||||
MermaidGeneratedSharedModule
|
||||
);
|
||||
const GitGraph: GitGraphServices = inject(
|
||||
createDefaultCoreModule({ shared }),
|
||||
GitGraphGeneratedModule,
|
||||
GitGraphModule
|
||||
);
|
||||
shared.ServiceRegistry.register(GitGraph);
|
||||
return { shared, GitGraph };
|
||||
}
|
7
packages/parser/src/language/gitGraph/tokenBuilder.ts
Normal file
7
packages/parser/src/language/gitGraph/tokenBuilder.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { AbstractMermaidTokenBuilder } from '../common/index.js';
|
||||
|
||||
export class GitGraphTokenBuilder extends AbstractMermaidTokenBuilder {
|
||||
public constructor() {
|
||||
super(['gitGraph']);
|
||||
}
|
||||
}
|
@@ -6,6 +6,11 @@ export {
|
||||
Pie,
|
||||
PieSection,
|
||||
Architecture,
|
||||
GitGraph,
|
||||
Branch,
|
||||
Commit,
|
||||
Merge,
|
||||
Statement,
|
||||
isCommon,
|
||||
isInfo,
|
||||
isPacket,
|
||||
@@ -13,6 +18,10 @@ export {
|
||||
isPie,
|
||||
isPieSection,
|
||||
isArchitecture,
|
||||
isGitGraph,
|
||||
isBranch,
|
||||
isCommit,
|
||||
isMerge,
|
||||
} from './generated/ast.js';
|
||||
|
||||
export {
|
||||
@@ -21,8 +30,10 @@ export {
|
||||
PacketGeneratedModule,
|
||||
PieGeneratedModule,
|
||||
ArchitectureGeneratedModule,
|
||||
GitGraphGeneratedModule,
|
||||
} from './generated/module.js';
|
||||
|
||||
export * from './gitGraph/index.js';
|
||||
export * from './common/index.js';
|
||||
export * from './info/index.js';
|
||||
export * from './packet/index.js';
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import type { LangiumParser, ParseResult } from 'langium';
|
||||
|
||||
import type { Info, Packet, Pie, Architecture } from './index.js';
|
||||
import type { Info, Packet, Pie, Architecture, GitGraph } from './index.js';
|
||||
|
||||
export type DiagramAST = Info | Packet | Pie | Architecture;
|
||||
export type DiagramAST = Info | Packet | Pie | Architecture | GitGraph;
|
||||
|
||||
const parsers: Record<string, LangiumParser> = {};
|
||||
const initializers = {
|
||||
@@ -26,12 +26,19 @@ const initializers = {
|
||||
const parser = createArchitectureServices().Architecture.parser.LangiumParser;
|
||||
parsers.architecture = parser;
|
||||
},
|
||||
gitGraph: async () => {
|
||||
const { createGitGraphServices } = await import('./language/gitGraph/index.js');
|
||||
const parser = createGitGraphServices().GitGraph.parser.LangiumParser;
|
||||
parsers.gitGraph = 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(diagramType: 'architecture', text: string): Promise<Architecture>;
|
||||
export async function parse(diagramType: 'gitGraph', text: string): Promise<GitGraph>;
|
||||
|
||||
export async function parse<T extends DiagramAST>(
|
||||
diagramType: keyof typeof initializers,
|
||||
text: string
|
||||
|
207
packages/parser/tests/gitGraph.test.ts
Normal file
207
packages/parser/tests/gitGraph.test.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { Branch, Merge } from '../src/language/index.js';
|
||||
import { gitGraphParse as parse } from './test-util.js';
|
||||
import type { Commit } from '../src/language/index.js';
|
||||
import type { Checkout, CherryPicking } from '../src/language/generated/ast.js';
|
||||
|
||||
describe('Parsing Commit Statements', () => {
|
||||
it('should parse a simple commit', () => {
|
||||
const result = parse(`gitGraph\n commit\n`);
|
||||
expect(result.value.statements[0].$type).toBe('Commit');
|
||||
});
|
||||
|
||||
it('should parse multiple commits', () => {
|
||||
const result = parse(`gitGraph\n commit\n commit\n commit\n`);
|
||||
expect(result.value.statements).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should parse commits with all properties', () => {
|
||||
const result = parse(`gitGraph\n commit id:"1" msg:"Fix bug" tag:"v1.2" type:NORMAL\n`);
|
||||
const commit = result.value.statements[0] as Commit;
|
||||
expect(commit.$type).toBe('Commit');
|
||||
expect(commit.id).toBe('1');
|
||||
expect(commit.message).toBe('Fix bug');
|
||||
expect(commit.tags).toEqual(['v1.2']);
|
||||
expect(commit.type).toBe('NORMAL');
|
||||
});
|
||||
|
||||
it('should handle commit messages with special characters', () => {
|
||||
const result = parse(`gitGraph\n commit msg:"Fix issue #123: Handle errors"\n`);
|
||||
const commit = result.value.statements[0] as Commit;
|
||||
expect(commit.message).toBe('Fix issue #123: Handle errors');
|
||||
});
|
||||
|
||||
it('should parse commits with only a message and no other properties', () => {
|
||||
const result = parse(`gitGraph\n commit msg:"Initial release"\n`);
|
||||
const commit = result.value.statements[0] as Commit;
|
||||
expect(commit.message).toBe('Initial release');
|
||||
expect(commit.id).toBeUndefined();
|
||||
expect(commit.type).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should ignore malformed properties and not break parsing', () => {
|
||||
const result = parse(`gitGraph\n commit id:"2" msg:"Malformed commit" oops:"ignored"\n`);
|
||||
const commit = result.value.statements[0] as Commit;
|
||||
expect(commit.id).toBe('2');
|
||||
expect(commit.message).toBe('Malformed commit');
|
||||
expect(commit.hasOwnProperty('oops')).toBe(false);
|
||||
});
|
||||
|
||||
it('should parse multiple commits with different types', () => {
|
||||
const result = parse(`gitGraph\n commit type:NORMAL\n commit type:REVERSE\n`);
|
||||
const commit1 = result.value.statements[0] as Commit;
|
||||
const commit2 = result.value.statements[1] as Commit;
|
||||
expect(commit1.type).toBe('NORMAL');
|
||||
expect(commit2.type).toBe('REVERSE');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Parsing Branch Statements', () => {
|
||||
it('should parse a branch with a simple name', () => {
|
||||
const result = parse(`gitGraph\n commit\n commit\n branch master\n`);
|
||||
const branch = result.value.statements[2] as Branch;
|
||||
expect(branch.name).toBe('master');
|
||||
});
|
||||
|
||||
it('should parse a branch with an order property', () => {
|
||||
const result = parse(`gitGraph\n commit\n branch feature order:1\n`);
|
||||
const branch = result.value.statements[1] as Branch;
|
||||
expect(branch.name).toBe('feature');
|
||||
expect(branch.order).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle branch names with special characters', () => {
|
||||
const result = parse(`gitGraph\n branch feature/test-branch\n`);
|
||||
const branch = result.value.statements[0] as Branch;
|
||||
expect(branch.name).toBe('feature/test-branch');
|
||||
});
|
||||
|
||||
it('should parse branches with hyphens and underscores', () => {
|
||||
const result = parse(`gitGraph\n branch my-feature_branch\n`);
|
||||
const branch = result.value.statements[0] as Branch;
|
||||
expect(branch.name).toBe('my-feature_branch');
|
||||
});
|
||||
|
||||
it('should correctly handle branch without order property', () => {
|
||||
const result = parse(`gitGraph\n branch feature\n`);
|
||||
const branch = result.value.statements[0] as Branch;
|
||||
expect(branch.name).toBe('feature');
|
||||
expect(branch.order).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Parsing Merge Statements', () => {
|
||||
it('should parse a merge with a branch name', () => {
|
||||
const result = parse(`gitGraph\n merge master\n`);
|
||||
const merge = result.value.statements[0] as Merge;
|
||||
expect(merge.branch).toBe('master');
|
||||
});
|
||||
|
||||
it('should handle merges with additional properties', () => {
|
||||
const result = parse(`gitGraph\n merge feature id:"m1" tag:"release" type:HIGHLIGHT\n`);
|
||||
const merge = result.value.statements[0] as Merge;
|
||||
expect(merge.branch).toBe('feature');
|
||||
expect(merge.id).toBe('m1');
|
||||
expect(merge.tags).toEqual(['release']);
|
||||
expect(merge.type).toBe('HIGHLIGHT');
|
||||
});
|
||||
|
||||
it('should parse merge without any properties', () => {
|
||||
const result = parse(`gitGraph\n merge feature\n`);
|
||||
const merge = result.value.statements[0] as Merge;
|
||||
expect(merge.branch).toBe('feature');
|
||||
});
|
||||
|
||||
it('should ignore malformed properties in merge statements', () => {
|
||||
const result = parse(`gitGraph\n merge feature random:"ignored"\n`);
|
||||
const merge = result.value.statements[0] as Merge;
|
||||
expect(merge.branch).toBe('feature');
|
||||
expect(merge.hasOwnProperty('random')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Parsing Checkout Statements', () => {
|
||||
it('should parse a checkout to a named branch', () => {
|
||||
const result = parse(
|
||||
`gitGraph\n commit id:"1"\n branch develop\n branch fun\n checkout develop\n`
|
||||
);
|
||||
const checkout = result.value.statements[3] as Checkout;
|
||||
expect(checkout.branch).toBe('develop');
|
||||
});
|
||||
|
||||
it('should parse checkout to branches with complex names', () => {
|
||||
const result = parse(`gitGraph\n checkout hotfix-123\n`);
|
||||
const checkout = result.value.statements[0] as Checkout;
|
||||
expect(checkout.branch).toBe('hotfix-123');
|
||||
});
|
||||
|
||||
it('should parse checkouts with hyphens and numbers', () => {
|
||||
const result = parse(`gitGraph\n checkout release-2021\n`);
|
||||
const checkout = result.value.statements[0] as Checkout;
|
||||
expect(checkout.branch).toBe('release-2021');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Parsing CherryPicking Statements', () => {
|
||||
it('should parse cherry-picking with a commit id', () => {
|
||||
const result = parse(`gitGraph\n commit id:"123" commit id:"321" cherry-pick id:"123"\n`);
|
||||
const cherryPick = result.value.statements[2] as CherryPicking;
|
||||
expect(cherryPick.id).toBe('123');
|
||||
});
|
||||
|
||||
it('should parse cherry-picking with multiple properties', () => {
|
||||
const result = parse(`gitGraph\n cherry-pick id:"123" tag:"urgent" parent:"100"\n`);
|
||||
const cherryPick = result.value.statements[0] as CherryPicking;
|
||||
expect(cherryPick.id).toBe('123');
|
||||
expect(cherryPick.tags).toEqual(['urgent']);
|
||||
expect(cherryPick.parent).toBe('100');
|
||||
});
|
||||
|
||||
describe('Parsing with Accessibility Titles and Descriptions', () => {
|
||||
it('should parse accessibility titles', () => {
|
||||
const result = parse(`gitGraph\n accTitle: Accessible Graph\n commit\n`);
|
||||
expect(result.value.accTitle).toBe('Accessible Graph');
|
||||
});
|
||||
|
||||
it('should parse multiline accessibility descriptions', () => {
|
||||
const result = parse(
|
||||
`gitGraph\n accDescr {\n Detailed description\n across multiple lines\n }\n commit\n`
|
||||
);
|
||||
expect(result.value.accDescr).toBe('Detailed description\nacross multiple lines');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration Tests', () => {
|
||||
it('should correctly parse a complex graph with various elements', () => {
|
||||
const result = parse(`
|
||||
gitGraph TB:
|
||||
accTitle: Complex Example
|
||||
commit id:"init" type:NORMAL
|
||||
branch feature
|
||||
commit id:"feat1" msg:"Add feature"
|
||||
checkout main
|
||||
merge feature tag:"v1.0"
|
||||
cherry-pick id:"feat1" tag:"critical fix"
|
||||
`);
|
||||
expect(result.value.accTitle).toBe('Complex Example');
|
||||
expect(result.value.statements[0].$type).toBe('Commit');
|
||||
expect(result.value.statements[1].$type).toBe('Branch');
|
||||
expect(result.value.statements[2].$type).toBe('Commit');
|
||||
expect(result.value.statements[3].$type).toBe('Checkout');
|
||||
expect(result.value.statements[4].$type).toBe('Merge');
|
||||
expect(result.value.statements[5].$type).toBe('CherryPicking');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling for Invalid Syntax', () => {
|
||||
it('should report errors for unknown properties in commit', () => {
|
||||
const result = parse(`gitGraph\n commit unknown:"oops"\n`);
|
||||
expect(result.parserErrors).not.toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should report errors for invalid branch order', () => {
|
||||
const result = parse(`gitGraph\n branch feature order:xyz\n`);
|
||||
expect(result.parserErrors).not.toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,7 +1,18 @@
|
||||
import type { LangiumParser, ParseResult } from 'langium';
|
||||
import { expect, vi } from 'vitest';
|
||||
import type { Info, InfoServices, Pie, PieServices } from '../src/language/index.js';
|
||||
import { createInfoServices, createPieServices } from '../src/language/index.js';
|
||||
import type {
|
||||
Info,
|
||||
InfoServices,
|
||||
Pie,
|
||||
PieServices,
|
||||
GitGraph,
|
||||
GitGraphServices,
|
||||
} from '../src/language/index.js';
|
||||
import {
|
||||
createInfoServices,
|
||||
createPieServices,
|
||||
createGitGraphServices,
|
||||
} from '../src/language/index.js';
|
||||
|
||||
const consoleMock = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||
|
||||
@@ -40,3 +51,14 @@ export function createPieTestServices() {
|
||||
return { services: pieServices, parse };
|
||||
}
|
||||
export const pieParse = createPieTestServices().parse;
|
||||
|
||||
const gitGraphServices: GitGraphServices = createGitGraphServices().GitGraph;
|
||||
const gitGraphParser: LangiumParser = gitGraphServices.parser.LangiumParser;
|
||||
export function createGitGraphTestServices() {
|
||||
const parse = (input: string) => {
|
||||
return gitGraphParser.parse<GitGraph>(input);
|
||||
};
|
||||
|
||||
return { services: gitGraphServices, parse };
|
||||
}
|
||||
export const gitGraphParse = createGitGraphTestServices().parse;
|
||||
|
Reference in New Issue
Block a user