Compare commits

..

21 Commits

Author SHA1 Message Date
darshanr0107
ee0d3209af Merge branch 'develop' into 6777-er-relationship-label-optional 2025-11-12 15:33:36 +05:30
darshanr0107
6ac5e0e132 chore: update docs
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-11-12 13:54:04 +05:30
Shubham P
6b9f26dac8 Merge pull request #7080 from mermaid-js/7079-c4context-componentqueue-ext-lexical-error
7079: add missing support for ComponentQueue_Ext in C4Context diagrams
2025-11-11 04:07:06 +00:00
Shubham P
ea590cdafe Merge pull request #7075 from mermaid-js/6889-fix-escaped-p-tags-in-sandbox-mode
6889: Fix escaped <p> tags in labels when securityLevel is set to "sandbox"
2025-11-11 04:05:37 +00:00
Shubham P
f3769c70bc Merge branch 'develop' into 7079-c4context-componentqueue-ext-lexical-error 2025-11-11 09:24:14 +05:30
Shubham P
4cf4d15197 Merge branch 'develop' into 6889-fix-escaped-p-tags-in-sandbox-mode 2025-11-11 09:23:23 +05:30
Shubham P
c02cf92656 Merge pull request #7149 from mermaid-js/renovate/patch-dompurify
fix(deps): update dependency dompurify to ^3.3.0
2025-11-10 10:15:31 +00:00
Shubham P
3a1266892d Merge pull request #7148 from mermaid-js/renovate/patch-all-patch
fix(deps): update all patch dependencies (patch)
2025-11-10 10:15:16 +00:00
renovate[bot]
67e81de557 fix(deps): update dependency dompurify to ^3.3.0 2025-11-10 02:54:45 +00:00
renovate[bot]
847b3aa24e fix(deps): update all patch dependencies 2025-11-10 02:54:19 +00:00
darshanr0107
9ec0e8f932 Merge branch 'develop' of https://github.com/mermaid-js/mermaid into 6889-fix-escaped-p-tags-in-sandbox-mode 2025-11-07 11:58:38 +05:30
darshanr0107
9585ee7533 chore:add e2e test
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-11-07 11:46:41 +05:30
darshanr0107
983120d945 fix: add test case for C4Context diagram with ComponentQueue_Ext
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-11-05 17:18:34 +05:30
darshanr0107
835de0012d fix:ComponentQueue_Ext throws lexical error
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-10-14 19:00:17 +05:30
darshanr0107
96a766dcdb chore: added changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-10-13 13:42:41 +05:30
darshanr0107
39d7ebd32e fix: escaped p tags in sandbox mode
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-10-13 13:16:58 +05:30
darshanr0107
fca17f3b10 chore: update ER diagram docs
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-03 13:10:50 +05:30
darshanr0107
94dfdf31b8 fix: add test case to endsure whitespace-only relationship labels are handled
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-02 11:40:35 +05:30
darshanr0107
f981d3d5b7 chore: updated changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-20 20:06:45 +05:30
darshanr0107
7139e1e5f7 chore: added changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-20 18:58:20 +05:30
darshanr0107
299226f8c2 fix: relationship label to be optional in ER diagram syntax
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-20 18:36:45 +05:30
23 changed files with 607 additions and 370 deletions

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Make relationship-label optional in ER diagrams

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: Handle master/main merges correctly in GitGraph diagrams

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Prevent HTML tags from being escaped in sandbox label rendering

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Support ComponentQueue_Ext to prevent parsing error

View File

@@ -98,12 +98,21 @@ export const openURLAndVerifyRendering = (
cy.visit(url);
cy.window().should('have.property', 'rendered', true);
cy.get('svg').should('be.visible');
// cspell:ignore viewbox
cy.get('svg').should('not.have.attr', 'viewbox');
if (validation) {
cy.get('svg').should(validation);
// Handle sandbox mode where SVG is inside an iframe
if (options.securityLevel === 'sandbox') {
cy.get('iframe').should('be.visible');
if (validation) {
cy.get('iframe').should(validation);
}
} else {
cy.get('svg').should('be.visible');
// cspell:ignore viewbox
cy.get('svg').should('not.have.attr', 'viewbox');
if (validation) {
cy.get('svg').should(validation);
}
}
if (screenshot) {

View File

@@ -114,4 +114,28 @@ describe('C4 diagram', () => {
{}
);
});
it('C4.6 should render C4Context diagram with ComponentQueue_Ext', () => {
imgSnapshotTest(
`
C4Context
title System Context diagram with ComponentQueue_Ext
Enterprise_Boundary(b0, "BankBoundary0") {
Person(customerA, "Banking Customer A", "A customer of the bank, with personal bank accounts.")
System(SystemAA, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments.")
Enterprise_Boundary(b1, "BankBoundary") {
ComponentQueue_Ext(msgQueue, "Message Queue", "RabbitMQ", "External message queue system for processing banking transactions")
System_Ext(SystemC, "E-mail system", "The internal Microsoft Exchange e-mail system.")
}
}
BiRel(customerA, SystemAA, "Uses")
Rel(SystemAA, msgQueue, "Sends messages to")
Rel(SystemAA, SystemC, "Sends e-mails", "SMTP")
`,
{}
);
});
});

View File

@@ -322,6 +322,18 @@ ORDER ||--|{ LINE-ITEM : contains
);
});
it('should render an ER diagram without labels also', () => {
imgSnapshotTest(
`
erDiagram
BOOK }|..|{ AUTHOR
BOOK }|..|{ GENRE
AUTHOR }|..|{ GENRE
`,
{ logLevel: 1 }
);
});
it('should render relationship labels with line breaks', () => {
imgSnapshotTest(
`

View File

@@ -79,6 +79,18 @@ describe('Flowchart v2', () => {
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
);
});
it('6a: should render complex HTML in labels with sandbox security', () => {
imgSnapshotTest(
`flowchart TD
A[Christmas] -->|Get money| B(Go shopping)
B --> C{Let me think}
C -->|One| D[Laptop]
C -->|Two| E[iPhone]
C -->|Three| F[fa:fa-car Car]
`,
{ securityLevel: 'sandbox', flowchart: { htmlLabels: true } }
);
});
it('7: should render a flowchart when useMaxWidth is true (default)', () => {
renderGraph(
`flowchart TD

View File

@@ -1569,14 +1569,4 @@ gitGraph TB:
{}
);
});
it('77: should render a gitGraph merging main into a newly created branch', () => {
imgSnapshotTest(
`gitGraph
commit
branch stable
checkout stable
merge main`,
{}
);
});
});

View File

@@ -135,6 +135,44 @@ erDiagram
"This **is** _Markdown_"
```
#### Optional Relationship Labels (v\<MERMAID_RELEASE_VERSION>+)
The relationship label in ER diagrams is optional. You can define relationships without specifying a label, and the diagram will render correctly.
For example, the following is valid:
```mermaid-example
erDiagram
BOOK }|..|{ AUTHOR
BOOK }|..|{ GENRE
AUTHOR }|..|{ GENRE
```
```mermaid
erDiagram
BOOK }|..|{ AUTHOR
BOOK }|..|{ GENRE
AUTHOR }|..|{ GENRE
```
This will show the relationships between the entities without any labels on the connecting lines.
You can still add a label if you want to describe the relationship:
```mermaid-example
erDiagram
BOOK }|..|{ AUTHOR : written_by
BOOK }|..|{ GENRE : categorized_as
AUTHOR }|..|{ GENRE : specializes_in
```
```mermaid
erDiagram
BOOK }|..|{ AUTHOR : written_by
BOOK }|..|{ GENRE : categorized_as
AUTHOR }|..|{ GENRE : specializes_in
```
### Relationship Syntax
The `relationship` part of each statement can be broken down into three sub-components:

View File

@@ -63,8 +63,8 @@
]
},
"devDependencies": {
"@applitools/eyes-cypress": "^3.55.4",
"@argos-ci/cypress": "^6.1.5",
"@applitools/eyes-cypress": "^3.56.3",
"@argos-ci/cypress": "^6.2.1",
"@changesets/changelog-github": "^0.5.1",
"@changesets/cli": "^2.29.7",
"@cspell/eslint-plugin": "^9.3.0",
@@ -77,7 +77,7 @@
"@types/jsdom": "^21.1.7",
"@types/lodash": "^4.17.20",
"@types/mdast": "^4.0.4",
"@types/node": "^22.18.13",
"@types/node": "^22.19.0",
"@types/rollup-plugin-visualizer": "^5.0.3",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/spy": "^3.2.4",
@@ -88,7 +88,7 @@
"cors": "^2.8.5",
"cpy-cli": "^5.0.0",
"cross-env": "^7.0.3",
"cspell": "^9.2.2",
"cspell": "^9.3.0",
"cypress": "^14.5.4",
"cypress-image-snapshot": "^4.0.1",
"cypress-split": "^1.24.25",
@@ -127,7 +127,7 @@
"tsx": "^4.20.6",
"typescript": "~5.7.3",
"typescript-eslint": "^8.38.0",
"vite": "^7.0.7",
"vite": "^7.0.8",
"vite-plugin-istanbul": "^7.0.0",
"vitest": "^3.2.4"
},

View File

@@ -78,7 +78,7 @@
"d3-sankey": "^0.12.3",
"dagre-d3-es": "7.0.13",
"dayjs": "^1.11.19",
"dompurify": "^3.2.7",
"dompurify": "^3.3.0",
"katex": "^0.16.25",
"khroma": "^2.1.0",
"lodash-es": "^4.17.21",

View File

@@ -0,0 +1,58 @@
import c4Db from '../c4Db.js';
import c4 from './c4Diagram.jison';
import { setConfig } from '../../../config.js';
setConfig({
securityLevel: 'strict',
});
describe.each([
['Component', 'component'],
['ComponentDb', 'component_db'],
['ComponentQueue', 'component_queue'],
['Component_Ext', 'external_component'],
['ComponentDb_Ext', 'external_component_db'],
['ComponentQueue_Ext', 'external_component_queue'],
])('parsing a C4 %s', function (macroName, elementName) {
beforeEach(function () {
c4.parser.yy = c4Db;
c4.parser.yy.clear();
});
it('should parse a C4 diagram with one Component correctly', function () {
c4.parser.parse(`C4Component
title Component diagram for Internet Banking Component
${macroName}(ComponentAA, "Internet Banking Component", "Technology", "Allows customers to view information about their bank accounts, and make payments.")`);
const yy = c4.parser.yy;
const shapes = yy.getC4ShapeArray();
expect(shapes.length).toBe(1);
const onlyShape = shapes[0];
expect(onlyShape).toMatchObject({
alias: 'ComponentAA',
descr: {
text: 'Allows customers to view information about their bank accounts, and make payments.',
},
label: {
text: 'Internet Banking Component',
},
techn: {
text: 'Technology',
},
typeC4Shape: {
text: elementName,
},
});
});
it('should handle a trailing whitespaces after Component', function () {
const whitespace = ' ';
const rendered = c4.parser.parse(`C4Component${whitespace}
title Component diagram for Internet Banking Component${whitespace}
${macroName}(ComponentAA, "Internet Banking Component", "Technology", "Allows customers to view information about their bank accounts, and make payments.")${whitespace}`);
expect(rendered).toBe(true);
});
});

View File

@@ -158,10 +158,10 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
"UpdateRelStyle" { this.begin("update_rel_style"); return 'UPDATE_REL_STYLE';}
"UpdateLayoutConfig" { this.begin("update_layout_config"); return 'UPDATE_LAYOUT_CONFIG';}
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config><<EOF>> return "EOF_IN_STRUCT";
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config>[(][ ]*[,] { this.begin("attribute"); return "ATTRIBUTE_EMPTY";}
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config>[(] { this.begin("attribute"); }
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config,attribute>[)] { this.popState();this.popState();}
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext_queue,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config><<EOF>> return "EOF_IN_STRUCT";
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext_queue,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config>[(][ ]*[,] { this.begin("attribute"); return "ATTRIBUTE_EMPTY";}
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext_queue,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config>[(] { this.begin("attribute"); }
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext_queue,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config,attribute>[)] { this.popState();this.popState();}
<attribute>",," { return 'ATTRIBUTE_EMPTY';}
<attribute>"," { }

View File

@@ -70,6 +70,31 @@ describe('Sanitize text', () => {
});
expect(result).not.toContain('javascript:alert(1)');
});
it('should allow HTML tags in sandbox mode', () => {
const htmlStr = '<p>This is a <strong>bold</strong> text</p>';
const result = sanitizeText(htmlStr, {
securityLevel: 'sandbox',
flowchart: { htmlLabels: true },
});
expect(result).toContain('<p>');
expect(result).toContain('<strong>');
expect(result).toContain('</strong>');
expect(result).toContain('</p>');
});
it('should remove script tags in sandbox mode', () => {
const maliciousStr = '<p>Hello <script>alert(1)</script> world</p>';
const result = sanitizeText(maliciousStr, {
securityLevel: 'sandbox',
flowchart: { htmlLabels: true },
});
expect(result).not.toContain('<script>');
expect(result).not.toContain('alert(1)');
expect(result).toContain('<p>');
expect(result).toContain('Hello');
expect(result).toContain('world');
});
});
describe('generic parser', () => {

View File

@@ -66,7 +66,7 @@ export const removeScript = (txt: string): string => {
const sanitizeMore = (text: string, config: MermaidConfig) => {
if (config.flowchart?.htmlLabels !== false) {
const level = config.securityLevel;
if (level === 'antiscript' || level === 'strict') {
if (level === 'antiscript' || level === 'strict' || level === 'sandbox') {
text = removeScript(text);
} else if (level !== 'loose') {
text = breakToPlaceholder(text);

View File

@@ -99,6 +99,22 @@ start
: 'ER_DIAGRAM' document 'EOF' { /*console.log('finished parsing');*/ }
;
relationship
: ENTITY relationType ENTITY maybeRole
{
yy.addRelationship($1, $4, $3, $2);
};
maybeRole
: COLON role
{
$$ = $2;
}
| /* empty */
{
$$ = '';
};
document
: /* empty */ { $$ = [] }
| document line {$1.push($2);$$ = $1}
@@ -113,32 +129,34 @@ line
statement
: entityName relSpec entityName COLON role
: entityName relSpec entityName maybeRole
{
yy.addEntity($1);
yy.addEntity($3);
yy.addRelationship($1, $5, $3, $2);
yy.addRelationship($1, $4, $3, $2);
}
| entityName STYLE_SEPARATOR idList relSpec entityName STYLE_SEPARATOR idList COLON role
| entityName STYLE_SEPARATOR idList relSpec entityName STYLE_SEPARATOR idList maybeRole
{
yy.addEntity($1);
yy.addEntity($5);
yy.addRelationship($1, $9, $5, $4);
yy.addRelationship($1, $8, $5, $4);
yy.setClass([$1], $3);
yy.setClass([$5], $7);
}
| entityName STYLE_SEPARATOR idList relSpec entityName COLON role
| entityName STYLE_SEPARATOR idList relSpec entityName maybeRole
{
yy.addEntity($1);
yy.addEntity($5);
yy.addRelationship($1, $7, $5, $4);
yy.addRelationship($1, $6, $5, $4);
yy.setClass([$1], $3);
}
| entityName relSpec entityName STYLE_SEPARATOR idList COLON role
| entityName relSpec entityName STYLE_SEPARATOR idList maybeRole
{
yy.addEntity($1);
yy.addEntity($3);
yy.addRelationship($1, $7, $3, $2);
yy.addRelationship($1, $6, $3, $2);
yy.setClass([$3], $5);
}
| entityName BLOCK_START attributes BLOCK_STOP

View File

@@ -981,6 +981,12 @@ describe('when parsing ER diagram it...', function () {
expect(rels[0].roleA).toBe('places');
});
it('should allow label as optional', function () {
erDiagram.parser.parse('erDiagram\nCUSTOMER ||--|{ ORDER');
const rels = erDb.getRelationships();
expect(rels[0].roleA).toBe('');
});
it('should represent parent-child relationship correctly', function () {
erDiagram.parser.parse('erDiagram\nPROJECT u--o{ TEAM_MEMBER : "parent"');
const rels = erDb.getRelationships();
@@ -989,6 +995,20 @@ describe('when parsing ER diagram it...', function () {
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.MD_PARENT);
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE);
});
it('should handle whitespace-only relationship labels', function () {
erDiagram.parser.parse('erDiagram\nBOOK }|..|{ AUTHOR : " "');
let rels = erDb.getRelationships();
expect(rels[rels.length - 1].roleA).toBe(' ');
erDiagram.parser.parse('erDiagram\nBOOK }|..|{ GENRE : "\t"');
rels = erDb.getRelationships();
expect(rels[rels.length - 1].roleA).toBe('\t');
erDiagram.parser.parse('erDiagram\nAUTHOR }|..|{ GENRE : " "');
rels = erDb.getRelationships();
expect(rels[rels.length - 1].roleA).toBe(' ');
});
});
describe('prototype properties', function () {

View File

@@ -1,7 +1,6 @@
import { log } from '../../logger.js';
import { db } from './gitGraphAst.js';
import { parser } from './gitGraphParser.js';
import { commitType } from './gitGraphTypes.js';
describe('when parsing a gitGraph', function () {
beforeEach(function () {
@@ -844,39 +843,6 @@ describe('when parsing a gitGraph', function () {
expect(db.getBranchesAsObjArray()).toStrictEqual([{ name: 'main' }, { name: 'testBranch' }]);
});
it('should handle merging the same branch multiple times', async () => {
const str = `gitGraph:
commit
branch stable
checkout stable
merge main
checkout main
commit
commit
checkout stable
merge main
`;
await parser.parse(str);
const commits = db.getCommits();
expect(commits.size).toBe(5);
expect(db.getCurrentBranch()).toBe('stable');
expect(db.getDirection()).toBe('LR');
expect(db.getBranches().size).toBe(2);
const commitsArray = db.getCommitsArray();
expect(commitsArray[0].branch).toBe('main');
expect(commitsArray[0].parents).toStrictEqual([]);
expect(commitsArray[1].branch).toBe('stable');
expect(commitsArray[1].type).toBe(commitType.MERGE);
expect(commitsArray[1].parents.length).toBe(2);
expect(commitsArray[2].branch).toBe('main');
expect(commitsArray[3].branch).toBe('main');
expect(commitsArray[4].branch).toBe('stable');
expect(commitsArray[4].type).toBe(commitType.MERGE);
expect(commitsArray[4].parents.length).toBe(2);
});
it('should handle merge with custom ids, tags and type', async () => {
const str = `gitGraph:
commit
@@ -1270,7 +1236,7 @@ describe('when parsing a gitGraph', function () {
);
}
});
it('should allow merging branches having same heads', async () => {
it('should throw error when trying to merge branches having same heads', async () => {
const str = `gitGraph
commit
branch testBranch
@@ -1278,19 +1244,13 @@ describe('when parsing a gitGraph', function () {
merge testBranch
`;
await parser.parse(str);
const commits = db.getCommits();
expect(commits.size).toBe(2);
expect(db.getCurrentBranch()).toBe('main');
const commitsArray = db.getCommitsArray();
expect(commitsArray[0].branch).toBe('main');
expect(commitsArray[0].parents).toStrictEqual([]);
expect(commitsArray[1].branch).toBe('main');
expect(commitsArray[1].type).toBe(commitType.MERGE);
expect(commitsArray[1].parents.length).toBe(2);
expect(commitsArray[1].parents[0]).toBe(commitsArray[0].id);
expect(commitsArray[1].parents[1]).toBe(commitsArray[0].id);
try {
await parser.parse(str);
// Fail test if above expression doesn't throw anything.
expect(true).toBe(false);
} catch (e: any) {
expect(e.message).toBe('Incorrect usage of "merge". Both branches have same head');
}
});
it('should throw error when trying to merge branch which has no commits', async () => {
const str = `gitGraph

View File

@@ -167,6 +167,9 @@ export const merge = (mergeDB: MergeDB): void => {
const otherCommit: Commit | undefined = otherBranchCheck
? state.records.commits.get(otherBranchCheck)
: undefined;
if (currentCommit && otherCommit && currentCommit.branch === otherBranch) {
throw new Error(`Cannot merge branch '${otherBranch}' into itself.`);
}
if (state.records.currBranch === otherBranch) {
const error: any = new Error('Incorrect usage of "merge". Cannot merge a branch to itself');
error.hash = {
@@ -209,6 +212,15 @@ export const merge = (mergeDB: MergeDB): void => {
};
throw error;
}
if (currentCommit === otherCommit) {
const error: any = new Error('Incorrect usage of "merge". Both branches have same head');
error.hash = {
text: `merge ${otherBranch}`,
token: `merge ${otherBranch}`,
expected: ['branch abc'],
};
throw error;
}
if (customId && state.records.commits.has(customId)) {
const error: any = new Error(
'Incorrect usage of "merge". Commit with id:' +

View File

@@ -21,19 +21,19 @@
"font-awesome": "^4.7.0",
"jiti": "^2.4.2",
"mermaid": "workspace:^",
"vue": "^3.5.23"
"vue": "^3.5.24"
},
"devDependencies": {
"@iconify-json/carbon": "^1.2.14",
"@unocss/reset": "^66.5.4",
"@unocss/reset": "^66.5.5",
"@vite-pwa/vitepress": "^1.0.1",
"@vitejs/plugin-vue": "^6.0.1",
"fast-glob": "^3.3.3",
"https-localhost": "^4.7.1",
"pathe": "^2.0.3",
"unocss": "^66.5.4",
"unocss": "^66.5.5",
"unplugin-vue-components": "^28.8.0",
"vite": "^7.0.7",
"vite": "^7.0.8",
"vite-plugin-pwa": "^1.0.3",
"vitepress": "1.6.4",
"workbox-window": "^7.3.0"

View File

@@ -89,6 +89,30 @@ erDiagram
"This **is** _Markdown_"
```
#### Optional Relationship Labels (v<MERMAID_RELEASE_VERSION>+)
The relationship label in ER diagrams is optional. You can define relationships without specifying a label, and the diagram will render correctly.
For example, the following is valid:
```mermaid-example
erDiagram
BOOK }|..|{ AUTHOR
BOOK }|..|{ GENRE
AUTHOR }|..|{ GENRE
```
This will show the relationships between the entities without any labels on the connecting lines.
You can still add a label if you want to describe the relationship:
```mermaid-example
erDiagram
BOOK }|..|{ AUTHOR : written_by
BOOK }|..|{ GENRE : categorized_as
AUTHOR }|..|{ GENRE : specializes_in
```
### Relationship Syntax
The `relationship` part of each statement can be broken down into three sub-components:

583
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff