Compare commits

..

2 Commits

Author SHA1 Message Date
autofix-ci[bot]
39b8ad2b09 [autofix.ci] apply automated fixes 2025-10-11 21:20:54 +00:00
Justin Greywolf
709880178e feat: add support for link statements within namespace blocks in class diagrams
Resolves #4700
2025-10-11 14:13:48 -07:00
15 changed files with 73 additions and 123 deletions

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: Support edge animation in hand drawn look

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: Resolved parsing error where direction TD was not recognized within subgraphs

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: Improve participant parsing and prevent recursive loops on invalid syntax

View File

@@ -6,7 +6,6 @@ interface CypressConfig {
listUrl?: boolean;
listId?: string;
name?: string;
screenshot?: boolean;
}
type CypressMermaidConfig = MermaidConfig & CypressConfig;
@@ -91,7 +90,7 @@ export const renderGraph = (
export const openURLAndVerifyRendering = (
url: string,
{ screenshot = true, ...options }: CypressMermaidConfig,
options: CypressMermaidConfig,
validation?: any
): void => {
const name: string = (options.name ?? cy.state('runnable').fullTitle()).replace(/\s+/g, '-');
@@ -104,9 +103,7 @@ export const openURLAndVerifyRendering = (
cy.get('svg').should(validation);
}
if (screenshot) {
verifyScreenshot(name);
}
verifyScreenshot(name);
};
export const verifyScreenshot = (name: string): void => {

View File

@@ -1029,19 +1029,4 @@ graph TD
}
);
});
it('FDH49: should add edge animation', () => {
renderGraph(
`
flowchart TD
A(["Start"]) L_A_B_0@--> B{"Decision"}
B --> C["Option A"] & D["Option B"]
style C stroke-width:4px,stroke-dasharray: 5
L_A_B_0@{ animation: slow }
L_B_D_0@{ animation: fast }`,
{ look: 'handDrawn', screenshot: false }
);
cy.get('path#L_A_B_0').should('have.class', 'edge-animation-slow');
cy.get('path#L_B_D_0').should('have.class', 'edge-animation-fast');
});
});

View File

@@ -774,21 +774,6 @@ describe('Graph', () => {
expect(svg).to.not.have.attr('style');
});
});
it('40: should add edge animation', () => {
renderGraph(
`
flowchart TD
A(["Start"]) L_A_B_0@--> B{"Decision"}
B --> C["Option A"] & D["Option B"]
style C stroke-width:4px,stroke-dasharray: 5
L_A_B_0@{ animation: slow }
L_B_D_0@{ animation: fast }`,
{ screenshot: false }
);
// Verify animation classes are applied to both edges
cy.get('path#L_A_B_0').should('have.class', 'edge-animation-slow');
cy.get('path#L_B_D_0').should('have.class', 'edge-animation-fast');
});
it('58: handle styling with style expressions', () => {
imgSnapshotTest(
`
@@ -988,19 +973,4 @@ graph TD
}
);
});
it('70: should render a subgraph with direction TD', () => {
imgSnapshotTest(
`
flowchart LR
subgraph A
direction TD
a --> b
end
`,
{
fontFamily: 'courier',
}
);
});
});

View File

@@ -702,6 +702,7 @@ classDiagram
It is possible to bind a click event to a node. The click can lead to either a javascript callback or to a link which will be opened in a new browser tab. **Note**: This functionality is disabled when using `securityLevel='strict'` and enabled when using `securityLevel='loose'`.
You would define these actions on a separate line after all classes have been declared.
If you have classes defined within a namespace, you can also add interaction definitions within the namespace definition, after the class(es) is defined
```
action className "reference" "tooltip"

View File

@@ -88,6 +88,50 @@ describe('given a basic class diagram, ', function () {
expect(relations[0].title).toBe('generates');
});
it('should handle link statements within namespaces', function () {
spyOn(classDb, 'setLink');
const str = `classDiagram
namespace MyNamespace {
class UserService {
+createUser()
+deleteUser()
}
class PaymentService {
+processPayment()
+refund()
}
link UserService "https://example.com/user-service"
link PaymentService "https://example.com/payment-service" "Payment Service Documentation"
}`;
parser.parse(str);
// Verify setLink was called for both classes
expect(classDb.setLink).toHaveBeenCalledWith(
'UserService',
'https://example.com/user-service'
);
expect(classDb.setLink).toHaveBeenCalledWith(
'PaymentService',
'https://example.com/payment-service'
);
// Verify the classes have the correct links and are in the namespace
const userService = classDb.getClass('UserService');
const paymentService = classDb.getClass('PaymentService');
expect(userService.parent).toBe('MyNamespace');
expect(userService.link).toBe('https://example.com/user-service');
expect(userService.cssClasses).toBe('default clickable');
expect(paymentService.parent).toBe('MyNamespace');
expect(paymentService.link).toBe('https://example.com/payment-service');
expect(paymentService.tooltip).toBe('Payment Service Documentation');
expect(paymentService.cssClasses).toBe('default clickable');
});
it('should handle accTitle and accDescr', function () {
const str = `classDiagram
accTitle: My Title

View File

@@ -275,14 +275,25 @@ statement
;
namespaceStatement
: namespaceIdentifier STRUCT_START classStatements STRUCT_STOP { yy.addClassesToNamespace($1, $3); }
| namespaceIdentifier STRUCT_START NEWLINE classStatements STRUCT_STOP { yy.addClassesToNamespace($1, $4); }
: namespaceIdentifier STRUCT_START namespaceBodyStatements STRUCT_STOP { yy.addClassesToNamespace($1, $3); }
| namespaceIdentifier STRUCT_START NEWLINE namespaceBodyStatements STRUCT_STOP { yy.addClassesToNamespace($1, $4); }
;
namespaceIdentifier
: NAMESPACE namespaceName { $$=$2; yy.addNamespace($2); }
;
namespaceBodyStatements
: namespaceBodyStatement { $$=[$1].filter(s => s !== null); }
| namespaceBodyStatement NEWLINE { $$=[$1].filter(s => s !== null); }
| namespaceBodyStatement NEWLINE namespaceBodyStatements { var filtered = [$1].filter(s => s !== null); $3.unshift(...filtered); $$=$3; }
;
namespaceBodyStatement
: classStatement { $$=$1; }
| clickStatement { $$=null; /* clickStatements don't return class names, but are processed for side effects */ }
;
classStatements
: classStatement {$$=[$1]}
| classStatement NEWLINE {$$=[$1]}

View File

@@ -140,7 +140,6 @@ that id.
.*direction\s+BT[^\n]* return 'direction_bt';
.*direction\s+RL[^\n]* return 'direction_rl';
.*direction\s+LR[^\n]* return 'direction_lr';
.*direction\s+TD[^\n]* return 'direction_td';
[^\s\"]+\@(?=[^\{\"]) { return 'LINK_ID'; }
[0-9]+ return 'NUM';
@@ -627,8 +626,6 @@ direction
{ $$={stmt:'dir', value:'RL'};}
| direction_lr
{ $$={stmt:'dir', value:'LR'};}
| direction_td
{ $$={stmt:'dir', value:'TD'};}
;
%%

View File

@@ -309,21 +309,4 @@ describe('when parsing subgraphs', function () {
expect(subgraphA.nodes).toContain('a');
expect(subgraphA.nodes).not.toContain('c');
});
it('should correctly parse direction TD inside a subgraph', function () {
const res = flow.parser.parse(`
graph LR
subgraph WithTD
direction TD
A1 --> A2
end
`);
const subgraphs = flow.parser.yy.getSubGraphs();
expect(subgraphs.length).toBe(1);
const subgraph = subgraphs[0];
expect(subgraph.dir).toBe('TD');
expect(subgraph.nodes).toContain('A1');
expect(subgraph.nodes).toContain('A2');
});
});

View File

@@ -32,14 +32,13 @@
<CONFIG>[^\}]+ { return 'CONFIG_CONTENT'; }
<CONFIG>\} { this.popState(); this.popState(); return 'CONFIG_END'; }
<ID>[^\<->\->:\n,;@\s]+(?=\@\{) { yytext = yytext.trim(); return 'ACTOR'; }
<ID>[^<>:\n,;@\s]+(?=\s+as\s) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
<ID>[^<>:\n,;@]+(?=\s*[\n;#]|$) { yytext = yytext.trim(); this.popState(); return 'ACTOR'; }
<ID>[^<>:\n,;@]*\<[^\n]* { this.popState(); return 'INVALID'; }
<ID>[^\<->\->:\n,;@]+?([\-]*[^\<->\->:\n,;@]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
"box" { this.begin('LINE'); return 'box'; }
"participant" { this.begin('ID'); return 'participant'; }
"actor" { this.begin('ID'); return 'participant_actor'; }
"create" return 'create';
"destroy" { this.begin('ID'); return 'destroy'; }
<ID>[^<\->\->:\n,;]+?([\-]*[^<\->\->:\n,;]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
<ALIAS>"as" { this.popState(); this.popState(); this.begin('LINE'); return 'AS'; }
<ALIAS>(?:) { this.popState(); this.popState(); return 'NEWLINE'; }
"loop" { this.begin('LINE'); return 'loop'; }
@@ -146,7 +145,6 @@ line
: SPACE statement { $$ = $2 }
| statement { $$ = $1 }
| NEWLINE { $$=[]; }
| INVALID { $$=[]; }
;
box_section
@@ -413,4 +411,4 @@ text2
: TXT {$$ = yy.parseMessage($1.trim().substring(1)) }
;
%%
%%

View File

@@ -2609,17 +2609,5 @@ Bob->>Alice:Got it!
expect(actors.get('E').type).toBe('entity');
expect(actors.get('E').description).toBe('E');
});
it('should handle fail parsing when alias token causes conflicts in participant definition', async () => {
let error = false;
try {
await Diagram.fromText(`
sequenceDiagram
participant SAS MyServiceWithMoreThan20Chars <br> service decription
`);
} catch (e) {
error = true;
}
expect(error).toBe(true);
});
});
});

View File

@@ -452,6 +452,7 @@ classDiagram
It is possible to bind a click event to a node. The click can lead to either a javascript callback or to a link which will be opened in a new browser tab. **Note**: This functionality is disabled when using `securityLevel='strict'` and enabled when using `securityLevel='loose'`.
You would define these actions on a separate line after all classes have been declared.
If you have classes defined within a namespace, you can also add interaction definitions within the namespace definition, after the class(es) is defined
```
action className "reference" "tooltip"

View File

@@ -605,14 +605,6 @@ export const insertEdge = function (
const edgeStyles = Array.isArray(edge.style) ? edge.style : [edge.style];
let strokeColor = edgeStyles.find((style) => style?.startsWith('stroke:'));
let animationClass = '';
if (edge.animate) {
animationClass = 'edge-animation-fast';
}
if (edge.animation) {
animationClass = 'edge-animation-' + edge.animation;
}
let animatedEdge = false;
if (edge.look === 'handDrawn') {
const rc = rough.svg(elem);
@@ -628,13 +620,7 @@ export const insertEdge = function (
svgPath = select(svgPathNode)
.select('path')
.attr('id', edge.id)
.attr(
'class',
' ' +
strokeClasses +
(edge.classes ? ' ' + edge.classes : '') +
(animationClass ? ' ' + animationClass : '')
)
.attr('class', ' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : ''))
.attr('style', edgeStyles ? edgeStyles.reduce((acc, style) => acc + ';' + style, '') : '');
let d = svgPath.attr('d');
svgPath.attr('d', d);
@@ -642,6 +628,13 @@ export const insertEdge = function (
} else {
const stylesFromClasses = edgeClassStyles.join(';');
const styles = edgeStyles ? edgeStyles.reduce((acc, style) => acc + style + ';', '') : '';
let animationClass = '';
if (edge.animate) {
animationClass = ' edge-animation-fast';
}
if (edge.animation) {
animationClass = ' edge-animation-' + edge.animation;
}
const pathStyle =
(stylesFromClasses ? stylesFromClasses + ';' + styles + ';' : styles) +
@@ -653,10 +646,7 @@ export const insertEdge = function (
.attr('id', edge.id)
.attr(
'class',
' ' +
strokeClasses +
(edge.classes ? ' ' + edge.classes : '') +
(animationClass ? ' ' + animationClass : '')
' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : '') + (animationClass ?? '')
)
.attr('style', pathStyle);