mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-11-12 16:54:10 +01:00
Compare commits
21 Commits
gitgraph-m
...
6777-er-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee0d3209af | ||
|
|
6ac5e0e132 | ||
|
|
6b9f26dac8 | ||
|
|
ea590cdafe | ||
|
|
f3769c70bc | ||
|
|
4cf4d15197 | ||
|
|
c02cf92656 | ||
|
|
3a1266892d | ||
|
|
67e81de557 | ||
|
|
847b3aa24e | ||
|
|
9ec0e8f932 | ||
|
|
9585ee7533 | ||
|
|
983120d945 | ||
|
|
835de0012d | ||
|
|
96a766dcdb | ||
|
|
39d7ebd32e | ||
|
|
fca17f3b10 | ||
|
|
94dfdf31b8 | ||
|
|
f981d3d5b7 | ||
|
|
7139e1e5f7 | ||
|
|
299226f8c2 |
5
.changeset/angry-trains-fall.md
Normal file
5
.changeset/angry-trains-fall.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Make relationship-label optional in ER diagrams
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Handle master/main merges correctly in GitGraph diagrams
|
||||
5
.changeset/brave-baths-behave.md
Normal file
5
.changeset/brave-baths-behave.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Prevent HTML tags from being escaped in sandbox label rendering
|
||||
5
.changeset/ten-plums-bet.md
Normal file
5
.changeset/ten-plums-bet.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Support ComponentQueue_Ext to prevent parsing error
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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:
|
||||
|
||||
10
package.json
10
package.json
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
58
packages/mermaid/src/diagrams/c4/parser/c4Component.spec.js
Normal file
58
packages/mermaid/src/diagrams/c4/parser/c4Component.spec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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>"," { }
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:' +
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
583
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user