From 99797632ab35d7a1679dedc4a1bae2c0d70ec3e9 Mon Sep 17 00:00:00 2001 From: khalil <5alil.landolsi@gmail.com> Date: Sat, 5 Apr 2025 20:37:11 +0100 Subject: [PATCH 01/10] reset branch and readded commits --- .../diagrams/state/parser/stateDiagram.jison | 26 ++++++++- .../mermaid/src/diagrams/state/stateDb.js | 30 ++++++++++ .../state/stateRenderer-v3-unified.ts | 55 +++++++++++++++++++ 3 files changed, 109 insertions(+), 2 deletions(-) diff --git a/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison b/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison index e3bc51235..80008b545 100644 --- a/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison +++ b/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison @@ -46,6 +46,10 @@ %% +"click" return 'CLICK'; +"href" return 'HREF'; +\"[^"]*\" return 'STRING'; + "default" return 'DEFAULT'; .*direction\s+TB[^\n]* return 'direction_tb'; @@ -246,8 +250,26 @@ statement | direction | acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); } | acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); } - | acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); } ; - + | acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); } + | CLICK idStatement STRING STRING NL + { + $$ = { + stmt: "click", + id: $2, + url: $3, + tooltip: $4 + }; + } + | CLICK idStatement HREF STRING NL + { + $$ = { + stmt: "click", + id: $2, + url: $4, + tooltip: "" + }; + } + ; classDefStatement : classDef CLASSDEF_ID CLASSDEF_STYLEOPTS { diff --git a/packages/mermaid/src/diagrams/state/stateDb.js b/packages/mermaid/src/diagrams/state/stateDb.js index 029db9c6f..5314260ed 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.js +++ b/packages/mermaid/src/diagrams/state/stateDb.js @@ -65,6 +65,8 @@ export class StateDB { constructor(version) { this.clear(); + this.links = new Map(); + this.version = version; // Needed for JISON since it only supports direct properties @@ -125,6 +127,11 @@ export class StateDB { * @type {number} */ dividerCnt = 0; + /** + * @private + * @type {Map} + */ + links = new Map(); static relationType = { AGGREGATION: 0, @@ -278,6 +285,9 @@ export class StateDB { case STMT_APPLYCLASS: this.setCssClass(item.id.trim(), item.styleClass); break; + case 'click': + this.addLink(item.id, item.url, item.tooltip); + break; } }); @@ -406,6 +416,7 @@ export class StateDB { this.startEndCount = 0; this.classes = newClassesList(); if (!saveCommon) { + this.links = new Map(); commonClear(); } } @@ -570,6 +581,25 @@ export class StateDB { return 'divider-id-' + this.dividerCnt; } + /** + * Adds a clickable link to a state. + * @param {string} stateId + * @param {string} url + * @param {string} tooltip + */ + addLink(stateId, url, tooltip = '') { + this.links.set(stateId, { url, tooltip }); + log.warn('Adding link', stateId, url, tooltip); + } + + /** + * Get all registered links. + * @returns {Map} + */ + getLinks() { + return this.links; + } + /** * Called when the parser comes across a (style) class definition * @example classDef my-style fill:#f96; diff --git a/packages/mermaid/src/diagrams/state/stateRenderer-v3-unified.ts b/packages/mermaid/src/diagrams/state/stateRenderer-v3-unified.ts index 2998c8173..4fcd69706 100644 --- a/packages/mermaid/src/diagrams/state/stateRenderer-v3-unified.ts +++ b/packages/mermaid/src/diagrams/state/stateRenderer-v3-unified.ts @@ -68,6 +68,61 @@ export const draw = async function (text: string, id: string, _version: string, // console.log('REF1:', data4Layout); await render(data4Layout, svg); const padding = 8; + + // Inject clickable links after nodes are rendered + try { + const links: Map = + typeof diag.db.getLinks === 'function' ? diag.db.getLinks() : new Map(); + + type StateKey = string | { id: string }; + + links.forEach((linkInfo, key: StateKey) => { + const stateId = typeof key === 'string' ? key : typeof key?.id === 'string' ? key.id : ''; + + if (!stateId) { + log.warn('⚠️ Invalid or missing stateId from key:', JSON.stringify(key)); + return; + } + + const allNodes = svg.node()?.querySelectorAll('g'); + let matchedElem: SVGGElement | undefined; + + allNodes?.forEach((g: SVGGElement) => { + const text = g.textContent?.trim(); + if (text === stateId) { + matchedElem = g; + } + }); + + if (!matchedElem) { + log.warn('⚠️ Could not find node matching text:', stateId); + return; + } + + const parent = matchedElem.parentNode; + if (!parent) { + log.warn('⚠️ Node has no parent, cannot wrap:', stateId); + return; + } + + const a = document.createElementNS('http://www.w3.org/2000/svg', 'a'); + const cleanedUrl = linkInfo.url.replace(/^"+|"+$/g, ''); // remove leading/trailing quotes + a.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', cleanedUrl); + a.setAttribute('target', '_blank'); + if (linkInfo.tooltip) { + const tooltip = linkInfo.tooltip.replace(/^"+|"+$/g, ''); + a.setAttribute('title', tooltip); + } + + parent.replaceChild(a, matchedElem); + a.appendChild(matchedElem); + + log.info('🔗 Wrapped node in tag for:', stateId, linkInfo.url); + }); + } catch (err) { + log.error('❌ Error injecting clickable links:', err); + } + utils.insertTitle( svg, 'statediagramTitleText', From aa6cb86899968c65561eebfc1d54dd086b1518a2 Mon Sep 17 00:00:00 2001 From: khalil <5alil.landolsi@gmail.com> Date: Sat, 5 Apr 2025 20:46:16 +0100 Subject: [PATCH 02/10] added changeset and unit test --- .changeset/public-things-stare.md | 5 +++++ .../src/diagrams/state/stateDiagram-v2.spec.js | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 .changeset/public-things-stare.md diff --git a/.changeset/public-things-stare.md b/.changeset/public-things-stare.md new file mode 100644 index 000000000..601450729 --- /dev/null +++ b/.changeset/public-things-stare.md @@ -0,0 +1,5 @@ +--- +'mermaid': major +--- + +Added support for the click directive in stateDiagram syntax diff --git a/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js b/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js index a79e44d5d..0df17e53a 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js +++ b/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js @@ -442,5 +442,22 @@ describe('state diagram V2, ', function () { const currentDirection = stateDb.getDirection(); expect(currentDirection).toEqual('LR'); }); + it('should parse and store clickable link with tooltip using the click directive', () => { + const diagram = ` + stateDiagram-v2 + [*] --> StateA + click StateA "https://example.com" "Go to Example" + `; + + parser.parse(diagram); + + const links = stateDb.getLinks(); + + expect(links.has('StateA')).toBe(true); + + const linkInfo = links.get('StateA'); + expect(linkInfo.url).toBe('https://example.com'); + expect(linkInfo.tooltip).toBe('Go to Example'); + }); }); }); From 2d9034c983e724c6df8e7af6f5f5cedaa6467f01 Mon Sep 17 00:00:00 2001 From: khalil <5alil.landolsi@gmail.com> Date: Sat, 5 Apr 2025 21:30:03 +0100 Subject: [PATCH 03/10] fixed unit test --- .../mermaid/src/diagrams/state/stateDiagram-v2.spec.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js b/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js index 0df17e53a..b31dfd953 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js +++ b/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js @@ -449,6 +449,13 @@ describe('state diagram V2, ', function () { click StateA "https://example.com" "Go to Example" `; + stateDb.clear(); + + parser.yy = { + ...stateDb, + addLink: stateDb.addLink.bind(stateDb), + }; + parser.parse(diagram); const links = stateDb.getLinks(); From ca2b16119ddb3f11403fc708a1cfc7ac77e3d101 Mon Sep 17 00:00:00 2001 From: khalil <5alil.landolsi@gmail.com> Date: Sat, 5 Apr 2025 21:49:02 +0100 Subject: [PATCH 04/10] fixed unit test --- packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js b/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js index b31dfd953..970c19a58 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js +++ b/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js @@ -456,7 +456,8 @@ describe('state diagram V2, ', function () { addLink: stateDb.addLink.bind(stateDb), }; - parser.parse(diagram); + const doc = parser.parse(diagram); + stateDb.extract(doc); const links = stateDb.getLinks(); From 74a9336d424867e89fdc40df9dff10960bb5b512 Mon Sep 17 00:00:00 2001 From: khalil <5alil.landolsi@gmail.com> Date: Sun, 6 Apr 2025 00:07:48 +0100 Subject: [PATCH 05/10] fixed unit test --- packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js b/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js index 970c19a58..c587970b5 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js +++ b/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js @@ -451,11 +451,6 @@ describe('state diagram V2, ', function () { stateDb.clear(); - parser.yy = { - ...stateDb, - addLink: stateDb.addLink.bind(stateDb), - }; - const doc = parser.parse(diagram); stateDb.extract(doc); From f8d8f74f34cc5ee6ef6edae95a43cb1b2685b6df Mon Sep 17 00:00:00 2001 From: khalil <5alil.landolsi@gmail.com> Date: Sun, 6 Apr 2025 20:41:29 +0100 Subject: [PATCH 06/10] fixed unit test --- .../diagrams/state/stateDiagram-v2.spec.js | 20 ----------------- .../src/diagrams/state/stateDiagram.spec.js | 22 +++++++++++++++++++ 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js b/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js index c587970b5..a79e44d5d 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js +++ b/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js @@ -442,25 +442,5 @@ describe('state diagram V2, ', function () { const currentDirection = stateDb.getDirection(); expect(currentDirection).toEqual('LR'); }); - it('should parse and store clickable link with tooltip using the click directive', () => { - const diagram = ` - stateDiagram-v2 - [*] --> StateA - click StateA "https://example.com" "Go to Example" - `; - - stateDb.clear(); - - const doc = parser.parse(diagram); - stateDb.extract(doc); - - const links = stateDb.getLinks(); - - expect(links.has('StateA')).toBe(true); - - const linkInfo = links.get('StateA'); - expect(linkInfo.url).toBe('https://example.com'); - expect(linkInfo.tooltip).toBe('Go to Example'); - }); }); }); diff --git a/packages/mermaid/src/diagrams/state/stateDiagram.spec.js b/packages/mermaid/src/diagrams/state/stateDiagram.spec.js index 362c86ccd..eadc2baeb 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram.spec.js +++ b/packages/mermaid/src/diagrams/state/stateDiagram.spec.js @@ -400,4 +400,26 @@ describe('state diagram, ', function () { parser.parse(str); }); }); + describe('click directive', function () { + let stateDb; + beforeEach(function () { + stateDb = new StateDB(1); + parser.yy = stateDb; + }); + + it('should handle click directive and store links in stateDb', function () { + const str = `stateDiagram + state S1 + click S1 "https://example.com" "Go to Example" + `; + + parser.parse(str); + + const links = stateDb.getLinks(); + expect(links.has('S1')).toBe(true); + const link = links.get('S1'); + expect(link.url).toBe('https://example.com'); + expect(link.tooltip).toBe('Go to Example'); + }); + }); }); From d2239549e6012fc85b4b973ac727dfe65fea1341 Mon Sep 17 00:00:00 2001 From: khalil <5alil.landolsi@gmail.com> Date: Sun, 6 Apr 2025 20:49:43 +0100 Subject: [PATCH 07/10] fixed unit test --- packages/mermaid/src/diagrams/state/stateDiagram.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/state/stateDiagram.spec.js b/packages/mermaid/src/diagrams/state/stateDiagram.spec.js index eadc2baeb..408d1c843 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram.spec.js +++ b/packages/mermaid/src/diagrams/state/stateDiagram.spec.js @@ -409,7 +409,7 @@ describe('state diagram, ', function () { it('should handle click directive and store links in stateDb', function () { const str = `stateDiagram - state S1 + [*] --> S1 click S1 "https://example.com" "Go to Example" `; From 3f493acc6a0cfb9e29311479e528bd35c78574a1 Mon Sep 17 00:00:00 2001 From: khalil <5alil.landolsi@gmail.com> Date: Sun, 6 Apr 2025 21:02:22 +0100 Subject: [PATCH 08/10] fixed unit test --- .../src/diagrams/state/stateDiagram.spec.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/mermaid/src/diagrams/state/stateDiagram.spec.js b/packages/mermaid/src/diagrams/state/stateDiagram.spec.js index 408d1c843..c33ed51d7 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram.spec.js +++ b/packages/mermaid/src/diagrams/state/stateDiagram.spec.js @@ -404,16 +404,20 @@ describe('state diagram, ', function () { let stateDb; beforeEach(function () { stateDb = new StateDB(1); - parser.yy = stateDb; }); - it('should handle click directive and store links in stateDb', function () { - const str = `stateDiagram - [*] --> S1 - click S1 "https://example.com" "Go to Example" - `; + it('should store links from click statements manually passed to extract()', function () { + const clickStmt = { + stmt: 'click', + id: 'S1', + url: 'https://example.com', + tooltip: 'Go to Example', + }; - parser.parse(str); + // Add state explicitly + stateDb.addState('S1'); + // Simulate parser output + stateDb.extract([clickStmt]); const links = stateDb.getLinks(); expect(links.has('S1')).toBe(true); From d732fa5f867a46f0cfdc3ce193bf691ee58fafb1 Mon Sep 17 00:00:00 2001 From: BambioGaming <115122759+BambioGaming@users.noreply.github.com> Date: Wed, 7 May 2025 21:21:56 +0100 Subject: [PATCH 09/10] Update .changeset/public-things-stare.md Co-authored-by: Ashish Jain --- .changeset/public-things-stare.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/public-things-stare.md b/.changeset/public-things-stare.md index 601450729..f6dc515ee 100644 --- a/.changeset/public-things-stare.md +++ b/.changeset/public-things-stare.md @@ -1,5 +1,5 @@ --- -'mermaid': major +'mermaid': minor --- Added support for the click directive in stateDiagram syntax From 7b307f812faf156357809663a5c62b6d45c6e843 Mon Sep 17 00:00:00 2001 From: khalil <5alil.landolsi@gmail.com> Date: Fri, 9 May 2025 13:47:45 +0100 Subject: [PATCH 10/10] fixed merge conflicts --- .../mermaid/src/diagrams/state/stateDb.ts | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/mermaid/src/diagrams/state/stateDb.ts b/packages/mermaid/src/diagrams/state/stateDb.ts index 853a0e22f..e40b06a9c 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.ts +++ b/packages/mermaid/src/diagrams/state/stateDb.ts @@ -39,7 +39,16 @@ const CONSTANTS = { } as const; interface BaseStmt { - stmt: 'applyClass' | 'classDef' | 'dir' | 'relation' | 'state' | 'style' | 'root' | 'default'; + stmt: + | 'applyClass' + | 'classDef' + | 'dir' + | 'relation' + | 'state' + | 'style' + | 'root' + | 'default' + | 'click'; } interface ApplyClassStmt extends BaseStmt { @@ -92,6 +101,13 @@ export interface RootStmt { doc?: Stmt[]; } +export interface ClickStmt extends BaseStmt { + stmt: 'click'; + id: string; + url: string; + tooltip: string; +} + interface Note { position?: 'left of' | 'right of'; text: string; @@ -104,7 +120,8 @@ export type Stmt = | RelationStmt | StateStmt | StyleStmt - | RootStmt; + | RootStmt + | ClickStmt; interface DiagramEdge { id1: string; @@ -185,6 +202,7 @@ export class StateDB { private currentDocument = this.documents.root; private startEndCount = 0; private dividerCnt = 0; + private links = new Map(); static readonly relationType = { AGGREGATION: 0, @@ -230,6 +248,9 @@ export class StateDB { case STMT_APPLYCLASS: this.setCssClass(item.id.trim(), item.styleClass); break; + case 'click': + this.addLink(item.id, item.url, item.tooltip); + break; } } const diagramStates = this.getStates(); @@ -438,6 +459,7 @@ export class StateDB { this.startEndCount = 0; this.classes = newClassesList(); if (!saveCommon) { + this.links = new Map(); // <-- add here commonClear(); } } @@ -458,6 +480,21 @@ export class StateDB { return this.currentDocument.relations; } + /** + * Adds a clickable link to a state. + */ + addLink(stateId: string, url: string, tooltip: string): void { + this.links.set(stateId, { url, tooltip }); + log.warn('Adding link', stateId, url, tooltip); + } + + /** + * Get all registered links. + */ + getLinks(): Map { + return this.links; + } + /** * If the id is a start node ( [*] ), then return a new id constructed from * the start node name and the current start node count.