diff --git a/__mocks__/d3.ts b/__mocks__/d3.ts deleted file mode 100644 index 67f09b6f4f..0000000000 --- a/__mocks__/d3.ts +++ /dev/null @@ -1,67 +0,0 @@ -// @ts-nocheck TODO: Fix TS -import { vi } from 'vitest'; - -const NewD3 = function () { - /** - * - */ - function returnThis() { - return this; - } - return { - append: function () { - return NewD3(); - }, - lower: returnThis, - attr: returnThis, - style: returnThis, - text: returnThis, - 0: { - 0: { - getBBox: function () { - return { - height: 10, - width: 20, - }; - }, - }, - }, - }; -}; - -export const select = function () { - return new NewD3(); -}; - -export const selectAll = function () { - return new NewD3(); -}; - -export const curveBasis = 'basis'; -export const curveLinear = 'linear'; -export const curveCardinal = 'cardinal'; - -export const MockD3 = (name, parent) => { - const children = []; - const elem = { - get __children() { - return children; - }, - get __name() { - return name; - }, - get __parent() { - return parent; - }, - }; - elem.append = (name) => { - const mockElem = MockD3(name, elem); - children.push(mockElem); - return mockElem; - }; - elem.lower = vi.fn(() => elem); - elem.attr = vi.fn(() => elem); - elem.text = vi.fn(() => elem); - elem.style = vi.fn(() => elem); - return elem; -}; diff --git a/__mocks__/dagre-d3.ts b/__mocks__/dagre-d3.ts deleted file mode 100644 index a1a6775916..0000000000 --- a/__mocks__/dagre-d3.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { vi } from 'vitest'; - -// export const render = vi.fn(); diff --git a/__mocks__/entity-decode/browser.ts b/__mocks__/entity-decode/browser.ts deleted file mode 100644 index bd82d79fd9..0000000000 --- a/__mocks__/entity-decode/browser.ts +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = function (txt: string) { - return txt; -}; diff --git a/demos/1871-1732.html b/demos/1871-1732.html new file mode 100644 index 0000000000..1d76617f8d --- /dev/null +++ b/demos/1871-1732.html @@ -0,0 +1,26 @@ + + + Issue #1732 + + + + +

graph

+
+      graph LR
+      A>Test];
+      A <--> B;
+  
+      C[Test];
+      C <--> D;
+    
+ + + + + + diff --git a/demos/1871-c4.html b/demos/1871-c4.html new file mode 100644 index 0000000000..67ae8b4d73 --- /dev/null +++ b/demos/1871-c4.html @@ -0,0 +1,28 @@ + + + C4 + + + + + + + + +

C4 Diagrams

+ Regression should be same as dist +
+    %%{init:{"theme":"base", "themeVariables": {"personBorder":"red"}}}%%
+    C4Context
+    Person(A, "A", "")
+    System(B, "B", "")      
+    Rel(A, B, "Uses")
+    
+ + + + + + diff --git a/demos/1871-class-v2.html b/demos/1871-class-v2.html new file mode 100644 index 0000000000..e8c81584ba --- /dev/null +++ b/demos/1871-class-v2.html @@ -0,0 +1,30 @@ + + + classDiagram-v2 + + + + +

classDiagram-v2

+
+      %%{init:{"theme":"base", "themeVariables": {"lineColor":"red"}}}%%
+      classDiagram-v2
+      Animal <|-- Duck
+      
+ + Marker should be blue: +
+    %%{init:{"theme":"base", "themeVariables": {"lineColor":"blue"}}}%%
+    classDiagram-v2
+    Animal <|-- Duck
+  
+ + + + + + diff --git a/demos/1871-class.html b/demos/1871-class.html new file mode 100644 index 0000000000..73e5988c65 --- /dev/null +++ b/demos/1871-class.html @@ -0,0 +1,34 @@ + + + classDiagram + + + + +

classDiagram

+
+      %%{init:{"theme":"base", "themeVariables": {"lineColor":"red"}}}%%
+      classDiagram
+      A <|--|> B
+      C <--> D
+      E *--* F
+      G o--o H
+    
+ Marker should be blue: + +
+      %%{init:{"theme":"base", "themeVariables": {"lineColor":"blue"}}}%%
+      classDiagram
+      A <|--|> B
+      C <--> D
+      E *--* F
+      G o--o H
+    
+ + + + + + diff --git a/demos/1871-er.html b/demos/1871-er.html new file mode 100644 index 0000000000..9d95497ce3 --- /dev/null +++ b/demos/1871-er.html @@ -0,0 +1,21 @@ + + + + erDiagram + + + + + Regression should be same as dist +
+    %%{init:{"theme":"base"}}%%
+    erDiagram
+    A ||--o{ B : label
+    
+ + + + + diff --git a/demos/1871-flowchart.html b/demos/1871-flowchart.html new file mode 100644 index 0000000000..facbcf74cd --- /dev/null +++ b/demos/1871-flowchart.html @@ -0,0 +1,27 @@ + + + flowchart + + + + +

flowchart

+
+      %%{init:{"theme":"base", "themeVariables": {"lineColor":"red"}}}%%
+      flowchart LR
+      A <--> B x--x C o--o D
+    
+ Markers should be blue: +
+      %%{init:{"theme":"base", "themeVariables": {"lineColor":"blue"}}}%%
+      flowchart LR
+      A <--> B x--x C o--o D
+    
+ + + + + + diff --git a/demos/1871-gantt.html b/demos/1871-gantt.html new file mode 100644 index 0000000000..a86dbb526a --- /dev/null +++ b/demos/1871-gantt.html @@ -0,0 +1,29 @@ + + + gantt + + + + +

gantt

+ Regression should be same as dist +
+      %%{init:{"theme":"base", "themeVariables": {"lineColor":"red"}}}%%
+      gantt
+      title A Gantt Diagram
+      dateFormat  YYYY-MM-DD
+      section Section
+      A task           :a1, 2014-01-01, 30d
+      Another task     :after a1  , 20d
+      section Another
+      Task in sec      :2014-01-12  , 12d
+      another task      : 24d    >
+    
+ + + + + + diff --git a/demos/1871-git.html b/demos/1871-git.html new file mode 100644 index 0000000000..8d712fdd10 --- /dev/null +++ b/demos/1871-git.html @@ -0,0 +1,25 @@ + + + git + + + + +

git

+
+      %% {init:{"theme":"base", "themeVariables": {"lineColor":"red"}}}%%
+      gitGraph
+      commit
+      branch A
+      checkout A
+      commit
+      
+ + + + + + diff --git a/demos/1871-graph.html b/demos/1871-graph.html new file mode 100644 index 0000000000..892b3f1cf5 --- /dev/null +++ b/demos/1871-graph.html @@ -0,0 +1,37 @@ + + + graph + + + + +

graph

+
+      graph LR
+      A>Test];
+      A <--> B;
+  
+      C[Test];
+      C <--> D;
+    
+
+      %%{init:{"theme":"base", "themeVariables": {"lineColor":"red"}}}%%
+      graph LR
+      A <--> B x--x C o--o D
+    
+ Markers should be blue: +
+      %%{init:{"theme":"base", "themeVariables": {"lineColor":"blue"}}}%%
+      graph LR
+      A <--> B x--x C o--o D
+    
+ + + + + + diff --git a/demos/1871-journey.html b/demos/1871-journey.html new file mode 100644 index 0000000000..08c9a6605c --- /dev/null +++ b/demos/1871-journey.html @@ -0,0 +1,36 @@ + + + + journey + + + + +
+        %%{init:{"theme":"base", "themeVariables": {"textColor":"red"}}}%%
+        journey
+          A: B
+				
+ + Marker should be blue: +
+        %%{init:{"theme":"base", "themeVariables": {"textColor":"blue"}}}%%
+        journey
+          A: B
+				
+ + + + + + + diff --git a/demos/1871-requirement.html b/demos/1871-requirement.html new file mode 100644 index 0000000000..cc633305da --- /dev/null +++ b/demos/1871-requirement.html @@ -0,0 +1,37 @@ + + + requirememt + + + + +

requirememt

+ Regression: should be same as dist. +
+      %%{init:{"theme":"base", "themeVariables": {"lineColor":"red"}}}%%
+      requirementDiagram
+
+      requirement B {
+      id: 1
+      text: the test text.
+      risk: high
+      verifymethod: test
+      }
+  
+      element A {
+      type: simulation
+      }
+  
+      A - satisfies -> B
+        
+ + + + + + diff --git a/demos/1871-sequence.html b/demos/1871-sequence.html new file mode 100644 index 0000000000..998dfa6799 --- /dev/null +++ b/demos/1871-sequence.html @@ -0,0 +1,43 @@ + + + + sequence + + + + +

sequence

+ Regression: should be same as dist. +
+      %%{init:{"theme":"base", "themeVariables": {"actorBorder":"red"}}}%%
+      sequenceDiagram
+      participant Alice
+      participant Bob
+      Alice ->> Bob: Hello Bob, how are you?
+	  
+
+      %%{init:{"theme":"base", "themeVariables": {"actorBorder":"blue"}}}%%
+      sequenceDiagram
+      participant Alice
+      participant Bob
+      Alice ->> Bob: Hello Bob, how are you?
+      
+ + + + + + diff --git a/demos/1871-state-v2.html b/demos/1871-state-v2.html new file mode 100644 index 0000000000..37d9d851c5 --- /dev/null +++ b/demos/1871-state-v2.html @@ -0,0 +1,35 @@ + + + stateDiagram-v2 + + + + +

stateDiagram-v2

+
+      %%{init:{"theme":"base", "themeVariables": {"lineColor":"red"}}}%%
+      stateDiagram-v2
+      [*] --> A
+      state A {
+          [*] --> B
+      }  
+ + Markers should be blue: +
+    %%{init:{"theme":"base", "themeVariables": {"lineColor":"blue"}}}%%
+    stateDiagram-v2
+    [*] --> A
+    state A {
+        [*] --> B
+    }
+      
+ + + + + + diff --git a/demos/1871-state.html b/demos/1871-state.html new file mode 100644 index 0000000000..7d80ba5d3e --- /dev/null +++ b/demos/1871-state.html @@ -0,0 +1,35 @@ + + + stateDiagram + + + + +

stateDiagram

+
+      %%{init:{"theme":"base", "themeVariables": {"lineColor":"red"}}}%%
+      stateDiagram
+      [*] --> A
+      state A {
+          [*] --> B
+      }  
+ +
+    %%{init:{"theme":"base", "themeVariables": {"lineColor":"blue"}}}%%
+    stateDiagram
+    [*] --> A
+    state A {
+        [*] --> B
+    }
+      
+ + + + + + + diff --git a/packages/mermaid/src/__mocks__/mermaidAPI.ts b/packages/mermaid/src/__mocks__/mermaidAPI.ts deleted file mode 100644 index f15db139f4..0000000000 --- a/packages/mermaid/src/__mocks__/mermaidAPI.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Mocks for `./mermaidAPI`. - * - * We can't easily use `vi.spyOn(mermaidAPI, "function")` since the object is frozen with `Object.freeze()`. - */ -import * as configApi from '../config'; -import { vi } from 'vitest'; -import { addDiagrams } from '../diagram-api/diagram-orchestration'; -import Diagram from '../Diagram'; - -// Normally, we could just do the following to get the original `parse()` -// implementation, however, requireActual returns a promise and it's not documented how to use withing mock file. - -let hasLoadedDiagrams = false; -/** - * @param text - * @param parseError - */ -// eslint-disable-next-line @typescript-eslint/ban-types -function parse(text: string, parseError?: Function): boolean { - if (!hasLoadedDiagrams) { - addDiagrams(); - hasLoadedDiagrams = true; - } - const diagram = new Diagram(text, parseError); - return diagram.parse(text, parseError); -} - -// original version cannot be modified since it was frozen with `Object.freeze()` -export const mermaidAPI = { - render: vi.fn(), - parse, - parseDirective: vi.fn(), - initialize: vi.fn(), - getConfig: configApi.getConfig, - setConfig: configApi.setConfig, - getSiteConfig: configApi.getSiteConfig, - updateSiteConfig: configApi.updateSiteConfig, - reset: () => { - configApi.reset(); - }, - globalReset: () => { - configApi.reset(configApi.defaultConfig); - }, - defaultConfig: configApi.defaultConfig, -}; - -export default mermaidAPI; diff --git a/packages/mermaid/src/dagre-wrapper/edges.js b/packages/mermaid/src/dagre-wrapper/edges.js index 6ed08e924f..4c50675322 100644 --- a/packages/mermaid/src/dagre-wrapper/edges.js +++ b/packages/mermaid/src/dagre-wrapper/edges.js @@ -3,6 +3,7 @@ import createLabel from './createLabel'; import { line, curveBasis, select } from 'd3'; import { getConfig } from '../config'; import utils from '../utils'; +import markers from '../markers'; import { evaluate } from '../diagrams/common/common'; let edgeLabels = {}; @@ -453,12 +454,14 @@ export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph break; } - const svgPath = elem + elem .append('path') .attr('d', lineFunction(lineData)) .attr('id', edge.id) .attr('class', ' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : '')) - .attr('style', edge.style); + .attr('style', edge.style) + .attr('marker-start', markers.markerUrl(elem, startMarkerName(edge.arrowTypeStart))) + .attr('marker-end', markers.markerUrl(elem, endMarkerName(edge.arrowTypeEnd))); // DEBUG code, adds a red circle at each edge coordinate // edge.points.forEach(point => { @@ -471,81 +474,9 @@ export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph // .attr('cy', point.y); // }); - let url = ''; - // // TODO: Can we load this config only from the rendered graph type? - if (getConfig().flowchart.arrowMarkerAbsolute || getConfig().state.arrowMarkerAbsolute) { - url = - window.location.protocol + - '//' + - window.location.host + - window.location.pathname + - window.location.search; - url = url.replace(/\(/g, '\\('); - url = url.replace(/\)/g, '\\)'); - } log.info('arrowTypeStart', edge.arrowTypeStart); log.info('arrowTypeEnd', edge.arrowTypeEnd); - switch (edge.arrowTypeStart) { - case 'arrow_cross': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-crossStart' + ')'); - break; - case 'arrow_point': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-pointStart' + ')'); - break; - case 'arrow_barb': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-barbStart' + ')'); - break; - case 'arrow_circle': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-circleStart' + ')'); - break; - case 'aggregation': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-aggregationStart' + ')'); - break; - case 'extension': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-extensionStart' + ')'); - break; - case 'composition': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-compositionStart' + ')'); - break; - case 'dependency': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-dependencyStart' + ')'); - break; - case 'lollipop': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-lollipopStart' + ')'); - break; - default: - } - switch (edge.arrowTypeEnd) { - case 'arrow_cross': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-crossEnd' + ')'); - break; - case 'arrow_point': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-pointEnd' + ')'); - break; - case 'arrow_barb': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-barbEnd' + ')'); - break; - case 'arrow_circle': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-circleEnd' + ')'); - break; - case 'aggregation': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-aggregationEnd' + ')'); - break; - case 'extension': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-extensionEnd' + ')'); - break; - case 'composition': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-compositionEnd' + ')'); - break; - case 'dependency': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-dependencyEnd' + ')'); - break; - case 'lollipop': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-lollipopEnd' + ')'); - break; - default: - } let paths = {}; if (pointsHasChanged) { paths.updatedPath = points; @@ -553,3 +484,7 @@ export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph paths.originalPath = edge.points; return paths; }; + +const markerName = (arrowType) => arrowType && arrowType.replace('arrow_', ''); +const startMarkerName = (arrowType) => arrowType && markerName(arrowType) + 'Start'; +const endMarkerName = (arrowType) => arrowType && markerName(arrowType) + 'End'; diff --git a/packages/mermaid/src/dagre-wrapper/markers.js b/packages/mermaid/src/dagre-wrapper/markers.js index c231eb3e5b..92e665e7e6 100644 --- a/packages/mermaid/src/dagre-wrapper/markers.js +++ b/packages/mermaid/src/dagre-wrapper/markers.js @@ -1,20 +1,16 @@ /** Setup arrow head and define the marker. The result is appended to the svg. */ -import { log } from '../logger'; +import { appendMarker } from '../markers'; // Only add the number of markers that the diagram needs -const insertMarkers = (elem, markerArray, type, id) => { +export const insertMarkers = (elem, markerArray, type, id) => { markerArray.forEach((markerName) => { markers[markerName](elem, type, id); }); }; -const extension = (elem, type, id) => { - log.trace('Making markers for ', id); - elem - .append('defs') - .append('marker') - .attr('id', type + '-extensionStart') +const extension = (elem, type) => { + appendMarker(elem, 'extensionStart') .attr('class', 'marker extension ' + type) .attr('refX', 0) .attr('refY', 7) @@ -24,10 +20,7 @@ const extension = (elem, type, id) => { .append('path') .attr('d', 'M 1,7 L18,13 V 1 Z'); - elem - .append('defs') - .append('marker') - .attr('id', type + '-extensionEnd') + appendMarker(elem, 'extensionEnd') .attr('class', 'marker extension ' + type) .attr('refX', 19) .attr('refY', 7) @@ -39,10 +32,7 @@ const extension = (elem, type, id) => { }; const composition = (elem, type) => { - elem - .append('defs') - .append('marker') - .attr('id', type + '-compositionStart') + appendMarker(elem, 'compositionStart') .attr('class', 'marker composition ' + type) .attr('refX', 0) .attr('refY', 7) @@ -52,10 +42,7 @@ const composition = (elem, type) => { .append('path') .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z'); - elem - .append('defs') - .append('marker') - .attr('id', type + '-compositionEnd') + appendMarker(elem, 'compositionEnd') .attr('class', 'marker composition ' + type) .attr('refX', 19) .attr('refY', 7) @@ -65,11 +52,9 @@ const composition = (elem, type) => { .append('path') .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z'); }; + const aggregation = (elem, type) => { - elem - .append('defs') - .append('marker') - .attr('id', type + '-aggregationStart') + appendMarker(elem, 'aggregationStart') .attr('class', 'marker aggregation ' + type) .attr('refX', 0) .attr('refY', 7) @@ -79,10 +64,7 @@ const aggregation = (elem, type) => { .append('path') .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z'); - elem - .append('defs') - .append('marker') - .attr('id', type + '-aggregationEnd') + appendMarker(elem, 'aggregationEnd') .attr('class', 'marker aggregation ' + type) .attr('refX', 19) .attr('refY', 7) @@ -92,11 +74,9 @@ const aggregation = (elem, type) => { .append('path') .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z'); }; + const dependency = (elem, type) => { - elem - .append('defs') - .append('marker') - .attr('id', type + '-dependencyStart') + appendMarker(elem, 'dependencyStart') .attr('class', 'marker dependency ' + type) .attr('refX', 0) .attr('refY', 7) @@ -106,10 +86,7 @@ const dependency = (elem, type) => { .append('path') .attr('d', 'M 5,7 L9,13 L1,7 L9,1 Z'); - elem - .append('defs') - .append('marker') - .attr('id', type + '-dependencyEnd') + appendMarker(elem, 'dependencyEnd') .attr('class', 'marker dependency ' + type) .attr('refX', 19) .attr('refY', 7) @@ -119,11 +96,9 @@ const dependency = (elem, type) => { .append('path') .attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z'); }; + const lollipop = (elem, type) => { - elem - .append('defs') - .append('marker') - .attr('id', type + '-lollipopStart') + appendMarker(elem, 'lollipopStart') .attr('class', 'marker lollipop ' + type) .attr('refX', 0) .attr('refY', 7) @@ -137,11 +112,10 @@ const lollipop = (elem, type) => { .attr('cy', 7) .attr('r', 6); }; + const point = (elem, type) => { - elem - .append('marker') - .attr('id', type + '-pointEnd') - .attr('class', 'marker ' + type) + appendMarker(elem, 'pointEnd') + .attr('class', 'marker point ' + type) .attr('viewBox', '0 0 10 10') .attr('refX', 10) .attr('refY', 5) @@ -154,10 +128,9 @@ const point = (elem, type) => { .attr('class', 'arrowMarkerPath') .style('stroke-width', 1) .style('stroke-dasharray', '1,0'); - elem - .append('marker') - .attr('id', type + '-pointStart') - .attr('class', 'marker ' + type) + + appendMarker(elem, 'pointStart') + .attr('class', 'marker point ' + type) .attr('viewBox', '0 0 10 10') .attr('refX', 0) .attr('refY', 5) @@ -171,10 +144,9 @@ const point = (elem, type) => { .style('stroke-width', 1) .style('stroke-dasharray', '1,0'); }; + const circle = (elem, type) => { - elem - .append('marker') - .attr('id', type + '-circleEnd') + appendMarker(elem, 'circleEnd') .attr('class', 'marker ' + type) .attr('viewBox', '0 0 10 10') .attr('refX', 11) @@ -191,9 +163,7 @@ const circle = (elem, type) => { .style('stroke-width', 1) .style('stroke-dasharray', '1,0'); - elem - .append('marker') - .attr('id', type + '-circleStart') + appendMarker(elem, 'circleStart') .attr('class', 'marker ' + type) .attr('viewBox', '0 0 10 10') .attr('refX', -1) @@ -210,10 +180,9 @@ const circle = (elem, type) => { .style('stroke-width', 1) .style('stroke-dasharray', '1,0'); }; + const cross = (elem, type) => { - elem - .append('marker') - .attr('id', type + '-crossEnd') + appendMarker(elem, 'crossEnd') .attr('class', 'marker cross ' + type) .attr('viewBox', '0 0 11 11') .attr('refX', 12) @@ -229,9 +198,7 @@ const cross = (elem, type) => { .style('stroke-width', 2) .style('stroke-dasharray', '1,0'); - elem - .append('marker') - .attr('id', type + '-crossStart') + appendMarker(elem, 'crossStart') .attr('class', 'marker cross ' + type) .attr('viewBox', '0 0 11 11') .attr('refX', -1) @@ -247,11 +214,10 @@ const cross = (elem, type) => { .style('stroke-width', 2) .style('stroke-dasharray', '1,0'); }; + const barb = (elem, type) => { - elem - .append('defs') - .append('marker') - .attr('id', type + '-barbEnd') + appendMarker(elem, 'barbEnd') + .attr('class', 'marker barb ' + type) .attr('refX', 19) .attr('refY', 7) .attr('markerWidth', 20) @@ -259,7 +225,8 @@ const barb = (elem, type) => { .attr('markerUnits', 'strokeWidth') .attr('orient', 'auto') .append('path') - .attr('d', 'M 19,7 L9,13 L14,7 L9,1 Z'); + .attr('d', 'M 19,7 L9,13 L14,7 L9,1 Z') + .attr('class', 'arrowMarkerPath'); }; // TODO rename the class diagram markers to something shape descriptive and semantic free @@ -274,4 +241,5 @@ const markers = { cross, barb, }; + export default insertMarkers; diff --git a/packages/mermaid/src/dagre-wrapper/patterns.js b/packages/mermaid/src/dagre-wrapper/patterns.js index 75afa8bcc0..b4bc4a41c2 100644 --- a/packages/mermaid/src/dagre-wrapper/patterns.js +++ b/packages/mermaid/src/dagre-wrapper/patterns.js @@ -1,3 +1,4 @@ +import { appendMarker } from '../markers'; /** Setup arrow head and define the marker. The result is appended to the svg. */ // import { log } from '../logger'; @@ -30,11 +31,8 @@ const insertPatterns = (elem, patternArray, type, id) => { ; */ } -const dots = (elem, type) => { - elem - .append('defs') - .append('marker') - .attr('id', type + '-barbEnd') +const dots = (elem) => { + appendMarker(elem, 'barbEnd') .attr('refX', 19) .attr('refY', 7) .attr('markerWidth', 20) diff --git a/packages/mermaid/src/diagrams/c4/svgDraw.js b/packages/mermaid/src/diagrams/c4/svgDraw.js index 5666d9f844..1946822d7b 100644 --- a/packages/mermaid/src/diagrams/c4/svgDraw.js +++ b/packages/mermaid/src/diagrams/c4/svgDraw.js @@ -1,5 +1,6 @@ import common from '../common/common'; import { sanitizeUrl } from '@braintree/sanitize-url'; +import { appendMarker, markerUrl } from '../../markers'; export const drawRect = function (elem, rectData) { const rectElem = elem.append('rect'); @@ -220,7 +221,6 @@ export const drawRels = (elem, rels, conf) => { let offsetX = rel.offsetX ? parseInt(rel.offsetX) : 0; let offsetY = rel.offsetY ? parseInt(rel.offsetY) : 0; - let url = ''; if (i === 0) { let line = relsElem.append('line'); line.attr('x1', rel.startPoint.x); @@ -231,9 +231,9 @@ export const drawRels = (elem, rels, conf) => { line.attr('stroke-width', '1'); line.attr('stroke', strokeColor); line.style('fill', 'none'); - if (rel.type !== 'rel_b') line.attr('marker-end', 'url(' + url + '#arrowhead)'); + if (rel.type !== 'rel_b') line.attr('marker-end', markerUrl(elem, 'arrowhead)')); if (rel.type === 'birel' || rel.type === 'rel_b') - line.attr('marker-start', 'url(' + url + '#arrowend)'); + line.attr('marker-start', markerUrl(elem, 'arrowend')); i = -1; } else { let line = relsElem.append('path'); @@ -256,9 +256,9 @@ export const drawRels = (elem, rels, conf) => { .replaceAll('stopx', rel.endPoint.x) .replaceAll('stopy', rel.endPoint.y) ); - if (rel.type !== 'rel_b') line.attr('marker-end', 'url(' + url + '#arrowhead)'); + if (rel.type !== 'rel_b') line.attr('marker-end', markerUrl(elem, 'arrowhead)')); if (rel.type === 'birel' || rel.type === 'rel_b') - line.attr('marker-start', 'url(' + url + '#arrowend)'); + line.attr('marker-start', markerUrl(elem, 'arrowend)')); } let messageConf = conf.messageFont(); @@ -633,10 +633,7 @@ export const insertClockIcon = function (elem) { * @param elem */ export const insertArrowHead = function (elem) { - elem - .append('defs') - .append('marker') - .attr('id', 'arrowhead') + appendMarker(elem, 'arrowhead') .attr('refX', 9) .attr('refY', 5) .attr('markerUnits', 'userSpaceOnUse') @@ -647,10 +644,7 @@ export const insertArrowHead = function (elem) { .attr('d', 'M 0 0 L 10 5 L 0 10 z'); // this is actual shape for arrowhead }; export const insertArrowEnd = function (elem) { - elem - .append('defs') - .append('marker') - .attr('id', 'arrowend') + appendMarker(elem, 'arrowend') .attr('refX', 1) .attr('refY', 5) .attr('markerUnits', 'userSpaceOnUse') @@ -666,10 +660,7 @@ export const insertArrowEnd = function (elem) { * @param {any} elem */ export const insertArrowFilledHead = function (elem) { - elem - .append('defs') - .append('marker') - .attr('id', 'filled-head') + appendMarker(elem, 'filled-head') .attr('refX', 18) .attr('refY', 7) .attr('markerWidth', 20) @@ -684,10 +675,7 @@ export const insertArrowFilledHead = function (elem) { * @param {any} elem */ export const insertDynamicNumber = function (elem) { - elem - .append('defs') - .append('marker') - .attr('id', 'sequencenumber') + appendMarker(elem, 'sequencenumber') .attr('refX', 15) .attr('refY', 15) .attr('markerWidth', 60) @@ -705,10 +693,7 @@ export const insertDynamicNumber = function (elem) { * @param {any} elem */ export const insertArrowCrossHead = function (elem) { - const defs = elem.append('defs'); - const marker = defs - .append('marker') - .attr('id', 'crosshead') + const marker = appendMarker(elem, 'crosshead') .attr('markerWidth', 15) .attr('markerHeight', 8) .attr('orient', 'auto') diff --git a/packages/mermaid/src/diagrams/class/classRenderer.js b/packages/mermaid/src/diagrams/class/classRenderer.js index c1236afea7..839807662a 100644 --- a/packages/mermaid/src/diagrams/class/classRenderer.js +++ b/packages/mermaid/src/diagrams/class/classRenderer.js @@ -6,6 +6,7 @@ import svgDraw from './svgDraw'; import { configureSvgSize } from '../../setupGraphViewbox'; import { getConfig } from '../../config'; import addSVGAccessibilityFields from '../../accessibility'; +import { appendMarker } from '../../markers'; let idCache = {}; const padding = 20; @@ -30,10 +31,7 @@ const getGraphId = function (label) { * @param {SVGSVGElement} elem The SVG element to append to */ const insertMarkers = function (elem) { - elem - .append('defs') - .append('marker') - .attr('id', 'extensionStart') + appendMarker(elem, 'extensionStart') .attr('class', 'extension') .attr('refX', 0) .attr('refY', 7) @@ -43,10 +41,7 @@ const insertMarkers = function (elem) { .append('path') .attr('d', 'M 1,7 L18,13 V 1 Z'); - elem - .append('defs') - .append('marker') - .attr('id', 'extensionEnd') + appendMarker(elem, 'extensionEnd') .attr('refX', 19) .attr('refY', 7) .attr('markerWidth', 20) @@ -55,10 +50,7 @@ const insertMarkers = function (elem) { .append('path') .attr('d', 'M 1,1 V 13 L18,7 Z'); // this is actual shape for arrowhead - elem - .append('defs') - .append('marker') - .attr('id', 'compositionStart') + appendMarker(elem, 'compositionStart') .attr('class', 'extension') .attr('refX', 0) .attr('refY', 7) @@ -68,10 +60,7 @@ const insertMarkers = function (elem) { .append('path') .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z'); - elem - .append('defs') - .append('marker') - .attr('id', 'compositionEnd') + appendMarker(elem, 'compositionEnd') .attr('refX', 19) .attr('refY', 7) .attr('markerWidth', 20) @@ -80,10 +69,7 @@ const insertMarkers = function (elem) { .append('path') .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z'); - elem - .append('defs') - .append('marker') - .attr('id', 'aggregationStart') + appendMarker(elem, 'aggregationStart') .attr('class', 'extension') .attr('refX', 0) .attr('refY', 7) @@ -93,10 +79,7 @@ const insertMarkers = function (elem) { .append('path') .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z'); - elem - .append('defs') - .append('marker') - .attr('id', 'aggregationEnd') + appendMarker(elem, 'aggregationEnd') .attr('refX', 19) .attr('refY', 7) .attr('markerWidth', 20) @@ -105,10 +88,7 @@ const insertMarkers = function (elem) { .append('path') .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z'); - elem - .append('defs') - .append('marker') - .attr('id', 'dependencyStart') + appendMarker(elem, 'dependencyStart') .attr('class', 'extension') .attr('refX', 0) .attr('refY', 7) @@ -118,10 +98,7 @@ const insertMarkers = function (elem) { .append('path') .attr('d', 'M 5,7 L9,13 L1,7 L9,1 Z'); - elem - .append('defs') - .append('marker') - .attr('id', 'dependencyEnd') + appendMarker(elem, 'dependencyEnd') .attr('refX', 19) .attr('refY', 7) .attr('markerWidth', 20) diff --git a/packages/mermaid/src/diagrams/class/svgDraw.js b/packages/mermaid/src/diagrams/class/svgDraw.js index 3d44e94b45..c82bb76d7c 100644 --- a/packages/mermaid/src/diagrams/class/svgDraw.js +++ b/packages/mermaid/src/diagrams/class/svgDraw.js @@ -2,6 +2,7 @@ import { line, curveBasis } from 'd3'; import utils from '../../utils'; import { log } from '../../logger'; import { parseGenericTypes } from '../common/common'; +import { markerUrl } from '../../markers'; let edgeCount = 0; export const drawEdge = function (elem, path, relation, conf, diagObj) { @@ -40,32 +41,15 @@ export const drawEdge = function (elem, path, relation, conf, diagObj) { .attr('d', lineFunction(lineData)) .attr('id', 'edge' + edgeCount) .attr('class', 'relation'); - let url = ''; - if (conf.arrowMarkerAbsolute) { - url = - window.location.protocol + - '//' + - window.location.host + - window.location.pathname + - window.location.search; - url = url.replace(/\(/g, '\\('); - url = url.replace(/\)/g, '\\)'); - } if (relation.relation.lineType == 1) { svgPath.attr('class', 'relation dashed-line'); } if (relation.relation.type1 !== 'none') { - svgPath.attr( - 'marker-start', - 'url(' + url + '#' + getRelationType(relation.relation.type1) + 'Start' + ')' - ); + svgPath.attr('marker-start', markerUrl(elem, getRelationType(relation.relation.type1))); } if (relation.relation.type2 !== 'none') { - svgPath.attr( - 'marker-end', - 'url(' + url + '#' + getRelationType(relation.relation.type2) + 'End' + ')' - ); + svgPath.attr('marker-end', markerUrl(elem, getRelationType(relation.relation.type2))); } let x, y; diff --git a/packages/mermaid/src/diagrams/common/common.ts b/packages/mermaid/src/diagrams/common/common.ts index 9f6ae2cdbb..8c26262123 100644 --- a/packages/mermaid/src/diagrams/common/common.ts +++ b/packages/mermaid/src/diagrams/common/common.ts @@ -99,28 +99,6 @@ const breakToPlaceholder = (s: string): string => { return s.replace(lineBreakRegex, '#br#'); }; -/** - * Gets the current URL - * - * @param {boolean} useAbsolute Whether to return the absolute URL or not - * @returns {string} The current URL - */ -const getUrl = (useAbsolute: boolean): string => { - let url = ''; - if (useAbsolute) { - url = - window.location.protocol + - '//' + - window.location.host + - window.location.pathname + - window.location.search; - url = url.replaceAll(/\(/g, '\\('); - url = url.replaceAll(/\)/g, '\\)'); - } - - return url; -}; - /** * Converts a string/boolean into a boolean * @@ -161,6 +139,5 @@ export default { splitBreaks, lineBreakRegex, removeScript, - getUrl, evaluate, }; diff --git a/packages/mermaid/src/diagrams/er/erMarkers.js b/packages/mermaid/src/diagrams/er/erMarkers.js index 9484117725..5eb7d01aff 100644 --- a/packages/mermaid/src/diagrams/er/erMarkers.js +++ b/packages/mermaid/src/diagrams/er/erMarkers.js @@ -1,3 +1,5 @@ +import { appendMarker } from '../../markers'; + const ERMarkers = { ONLY_ONE_START: 'ONLY_ONE_START', ONLY_ONE_END: 'ONLY_ONE_END', @@ -18,10 +20,7 @@ const ERMarkers = { const insertMarkers = function (elem, conf) { let marker; - elem - .append('defs') - .append('marker') - .attr('id', ERMarkers.ONLY_ONE_START) + appendMarker(elem, ERMarkers.ONLY_ONE_START) .attr('refX', 0) .attr('refY', 9) .attr('markerWidth', 18) @@ -32,10 +31,7 @@ const insertMarkers = function (elem, conf) { .attr('fill', 'none') .attr('d', 'M9,0 L9,18 M15,0 L15,18'); - elem - .append('defs') - .append('marker') - .attr('id', ERMarkers.ONLY_ONE_END) + appendMarker(elem, ERMarkers.ONLY_ONE_END) .attr('refX', 18) .attr('refY', 9) .attr('markerWidth', 18) @@ -46,10 +42,7 @@ const insertMarkers = function (elem, conf) { .attr('fill', 'none') .attr('d', 'M3,0 L3,18 M9,0 L9,18'); - marker = elem - .append('defs') - .append('marker') - .attr('id', ERMarkers.ZERO_OR_ONE_START) + marker = appendMarker(elem, ERMarkers.ZERO_OR_ONE_START) .attr('refX', 0) .attr('refY', 9) .attr('markerWidth', 30) @@ -64,10 +57,7 @@ const insertMarkers = function (elem, conf) { .attr('r', 6); marker.append('path').attr('stroke', conf.stroke).attr('fill', 'none').attr('d', 'M9,0 L9,18'); - marker = elem - .append('defs') - .append('marker') - .attr('id', ERMarkers.ZERO_OR_ONE_END) + marker = appendMarker(elem, ERMarkers.ZERO_OR_ONE_END) .attr('refX', 30) .attr('refY', 9) .attr('markerWidth', 30) @@ -82,10 +72,7 @@ const insertMarkers = function (elem, conf) { .attr('r', 6); marker.append('path').attr('stroke', conf.stroke).attr('fill', 'none').attr('d', 'M21,0 L21,18'); - elem - .append('defs') - .append('marker') - .attr('id', ERMarkers.ONE_OR_MORE_START) + appendMarker(elem, ERMarkers.ONE_OR_MORE_START) .attr('refX', 18) .attr('refY', 18) .attr('markerWidth', 45) @@ -96,10 +83,7 @@ const insertMarkers = function (elem, conf) { .attr('fill', 'none') .attr('d', 'M0,18 Q 18,0 36,18 Q 18,36 0,18 M42,9 L42,27'); - elem - .append('defs') - .append('marker') - .attr('id', ERMarkers.ONE_OR_MORE_END) + appendMarker(elem, ERMarkers.ONE_OR_MORE_END) .attr('refX', 27) .attr('refY', 18) .attr('markerWidth', 45) @@ -110,10 +94,7 @@ const insertMarkers = function (elem, conf) { .attr('fill', 'none') .attr('d', 'M3,9 L3,27 M9,18 Q27,0 45,18 Q27,36 9,18'); - marker = elem - .append('defs') - .append('marker') - .attr('id', ERMarkers.ZERO_OR_MORE_START) + marker = appendMarker(elem, ERMarkers.ZERO_OR_MORE_START) .attr('refX', 18) .attr('refY', 18) .attr('markerWidth', 57) @@ -132,10 +113,7 @@ const insertMarkers = function (elem, conf) { .attr('fill', 'none') .attr('d', 'M0,18 Q18,0 36,18 Q18,36 0,18'); - marker = elem - .append('defs') - .append('marker') - .attr('id', ERMarkers.ZERO_OR_MORE_END) + marker = appendMarker(elem, ERMarkers.ZERO_OR_MORE_END) .attr('refX', 39) .attr('refY', 18) .attr('markerWidth', 57) diff --git a/packages/mermaid/src/diagrams/er/erRenderer.js b/packages/mermaid/src/diagrams/er/erRenderer.js index a6277f27da..602447a134 100644 --- a/packages/mermaid/src/diagrams/er/erRenderer.js +++ b/packages/mermaid/src/diagrams/er/erRenderer.js @@ -1,4 +1,4 @@ -import graphlib from 'graphlib'; +import { Graph } from 'graphlib'; import { line, curveBasis, select } from 'd3'; import dagre from 'dagre'; import { getConfig } from '../../config'; @@ -7,6 +7,7 @@ import erMarkers from './erMarkers'; import { configureSvgSize } from '../../setupGraphViewbox'; import addSVGAccessibilityFields from '../../accessibility'; import { parseGenericTypes } from '../common/common'; +import { markerUrl } from '../../markers'; import { v4 as uuid4 } from 'uuid'; /** Regex used to remove chars from the entity name so the result can be used in an id */ @@ -470,59 +471,37 @@ const drawRelationshipFromLayout = function (svg, rel, g, insert, diagObj) { svgPath.attr('stroke-dasharray', '8,8'); } - // TODO: Understand this better - let url = ''; - if (conf.arrowMarkerAbsolute) { - url = - window.location.protocol + - '//' + - window.location.host + - window.location.pathname + - window.location.search; - url = url.replace(/\(/g, '\\('); - url = url.replace(/\)/g, '\\)'); - } - // Decide which start and end markers it needs. It may be possible to be more concise here // by reversing a start marker to make an end marker...but this will do for now // Note that the 'A' entity's marker is at the end of the relationship and the 'B' entity's marker is at the start switch (rel.relSpec.cardA) { case diagObj.db.Cardinality.ZERO_OR_ONE: - svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_END + ')'); + svgPath.attr('marker-end', markerUrl(svgPath, erMarkers.ERMarkers.ZERO_OR_ONE_END)); break; case diagObj.db.Cardinality.ZERO_OR_MORE: - svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_END + ')'); + svgPath.attr('marker-end', markerUrl(svgPath, erMarkers.ERMarkers.ZERO_OR_MORE_END)); break; case diagObj.db.Cardinality.ONE_OR_MORE: - svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_END + ')'); + svgPath.attr('marker-end', markerUrl(svgPath, erMarkers.ERMarkers.ONE_OR_MORE_END)); break; case diagObj.db.Cardinality.ONLY_ONE: - svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_END + ')'); + svgPath.attr('marker-end', markerUrl(svgPath, erMarkers.ERMarkers.ONLY_ONE_END)); break; } switch (rel.relSpec.cardB) { case diagObj.db.Cardinality.ZERO_OR_ONE: - svgPath.attr( - 'marker-start', - 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_START + ')' - ); + svgPath.attr('marker-start', markerUrl(svgPath, erMarkers.ERMarkers.ZERO_OR_ONE_STAR)); break; case diagObj.db.Cardinality.ZERO_OR_MORE: - svgPath.attr( - 'marker-start', - 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_START + ')' - ); + svgPath.attr('marker-start', markerUrl(svgPath, erMarkers.ERMarkers.ZERO_OR_MORE_STAR)); break; case diagObj.db.Cardinality.ONE_OR_MORE: - svgPath.attr( - 'marker-start', - 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_START + ')' - ); + svgPath.attr('marker-start', markerUrl(svgPath, erMarkers.ERMarkers.ONE_OR_MORE_STAR)); break; case diagObj.db.Cardinality.ONLY_ONE: - svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_START + ')'); + svgPath.attr('marker-start', markerUrl(svgPath, erMarkers.ERMarkers.ONLY_ONE_START)); break; } @@ -623,7 +602,7 @@ export const draw = function (text, id, _version, diagObj) { // the direction from parent to child in a one-to-many as this influences graphlib to // put the parent above the child (does it?), which is intuitive. Most relationships // in ER diagrams are one-to-many. - g = new graphlib.Graph({ + g = new Graph({ multigraph: true, directed: true, compound: false, diff --git a/packages/mermaid/src/diagrams/flowchart/flowRenderer.addEdges.spec.js b/packages/mermaid/src/diagrams/flowchart/flowRenderer.addEdges.spec.js index 41868e2035..49ecaa921c 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowRenderer.addEdges.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/flowRenderer.addEdges.spec.js @@ -2,6 +2,8 @@ import flowDb from './flowDb'; import flowParser from './parser/flow'; import flowRenderer from './flowRenderer'; import Diagram from '../../Diagram'; +import * as d3 from 'd3'; + import { addDiagrams } from '../../diagram-api/diagram-orchestration'; addDiagrams(); @@ -31,7 +33,6 @@ describe('when using mermaid and ', function () { it('should handle edges without text', function () { const diag = new Diagram('graph TD;A-->B;'); - diag.db.getVertices(); const edges = diag.db.getEdges(); const mockG = { @@ -62,21 +63,18 @@ describe('when using mermaid and ', function () { }); it('should handle edges with styles defined', function () { - const diag = new Diagram('graph TD;A---B; linkStyle 0 stroke:val1,stroke-width:val2;'); - diag.db.getVertices(); - const edges = diag.db.getEdges(); + const diagram = new Diagram('graph TD;A---B; linkStyle 0 stroke:val1,stroke-width:val2;'); + const edges = diagram.db.getEdges(); const mockG = { - setEdge: function (start, end, options) { - expect(start).toContain('flowchart-A-'); - expect(end).toContain('flowchart-B-'); - expect(options.arrowhead).toBe('none'); + setEdge: function (_, __, options) { expect(options.style).toBe('stroke:val1;stroke-width:val2;fill:none;'); }, }; - flowRenderer.addEdges(edges, mockG, diag); + flowRenderer.addEdges(edges, mockG, diagram); }); + it('should handle edges with interpolation defined', function () { const diag = new Diagram('graph TD;A---B; linkStyle 0 interpolate basis'); diag.db.getVertices(); @@ -84,10 +82,10 @@ describe('when using mermaid and ', function () { const mockG = { setEdge: function (start, end, options) { - expect(start).toContain('flowchart-A-'); - expect(end).toContain('flowchart-B-'); + expect(start).toMatch(/^flowchart-A-\d+$/); + expect(end).toMatch(/^flowchart-B-\d+$/); expect(options.arrowhead).toBe('none'); - expect(options.curve).toBe('basis'); // mocked as string + expect(options.curve).toBe(d3.curveBasis); }, }; diff --git a/packages/mermaid/src/diagrams/flowchart/flowRenderer.js b/packages/mermaid/src/diagrams/flowchart/flowRenderer.js index 0c3aa3623e..e6bc90426b 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowRenderer.js +++ b/packages/mermaid/src/diagrams/flowchart/flowRenderer.js @@ -9,6 +9,7 @@ import { interpolateToCurve, getStylesFromArray } from '../../utils'; import { setupGraphViewbox } from '../../setupGraphViewbox'; import flowChartShapes from './flowChartShapes'; import addSVGAccessibilityFields from '../../accessibility'; +import { appendMarker } from '../../markers'; const conf = {}; export const setConf = function (cnf) { @@ -377,9 +378,7 @@ export const draw = function (text, id, _version, diagObj) { // Add our custom arrow - an empty arrowhead render.arrows().none = function normal(parent, id, edge, type) { - const marker = parent - .append('marker') - .attr('id', id) + const marker = appendMarker(parent, id) .attr('viewBox', '0 0 10 10') .attr('refX', 9) .attr('refY', 5) @@ -394,9 +393,7 @@ export const draw = function (text, id, _version, diagObj) { // Override normal arrowhead defined in d3. Remove style & add class to allow css styling. render.arrows().normal = function normal(parent, id) { - const marker = parent - .append('marker') - .attr('id', id) + const marker = appendMarker(parent, id) .attr('viewBox', '0 0 10 10') .attr('refX', 9) .attr('refY', 5) diff --git a/packages/mermaid/src/diagrams/requirement/requirementMarkers.js b/packages/mermaid/src/diagrams/requirement/requirementMarkers.js index 96e42d78da..3bc5c679d6 100644 --- a/packages/mermaid/src/diagrams/requirement/requirementMarkers.js +++ b/packages/mermaid/src/diagrams/requirement/requirementMarkers.js @@ -1,13 +1,12 @@ +import { appendMarker } from '../../markers'; + const ReqMarkers = { CONTAINS: 'contains', ARROW: 'arrow', }; const insertLineEndings = (parentNode, conf) => { - let containsNode = parentNode - .append('defs') - .append('marker') - .attr('id', ReqMarkers.CONTAINS + '_line_ending') + let containsNode = appendMarker(parentNode, ReqMarkers.CONTAINS + '_line_ending') .attr('refX', 0) .attr('refY', conf.line_height / 2) .attr('markerWidth', conf.line_height) @@ -42,10 +41,7 @@ const insertLineEndings = (parentNode, conf) => { // .attr('stroke', conf.rect_border_color) .attr('stroke-width', 1); - parentNode - .append('defs') - .append('marker') - .attr('id', ReqMarkers.ARROW + '_line_ending') + appendMarker(parentNode, ReqMarkers.ARROW + '_line_ending') .attr('refX', conf.line_height) .attr('refY', 0.5 * conf.line_height) .attr('markerWidth', conf.line_height) diff --git a/packages/mermaid/src/diagrams/requirement/requirementRenderer.js b/packages/mermaid/src/diagrams/requirement/requirementRenderer.js index d10c43066c..fc53ef5014 100644 --- a/packages/mermaid/src/diagrams/requirement/requirementRenderer.js +++ b/packages/mermaid/src/diagrams/requirement/requirementRenderer.js @@ -3,10 +3,10 @@ import dagre from 'dagre'; import graphlib from 'graphlib'; import { log } from '../../logger'; import { configureSvgSize } from '../../setupGraphViewbox'; -import common from '../common/common'; import markers from './requirementMarkers'; import { getConfig } from '../../config'; import addSVGAccessibilityFields from '../../accessibility'; +import { markerUrl } from '../../markers'; let conf = {}; let relCnt = 0; @@ -170,21 +170,10 @@ const drawRelationshipFromLayout = function (svg, rel, g, insert, diagObj) { .attr('fill', 'none'); if (rel.type == diagObj.db.Relationships.CONTAINS) { - svgPath.attr( - 'marker-start', - 'url(' + common.getUrl(conf.arrowMarkerAbsolute) + '#' + rel.type + '_line_ending' + ')' - ); + svgPath.attr('marker-start', markerUrl(svg, rel.type + '_line_ending')); } else { svgPath.attr('stroke-dasharray', '10,7'); - svgPath.attr( - 'marker-end', - 'url(' + - common.getUrl(conf.arrowMarkerAbsolute) + - '#' + - markers.ReqMarkers.ARROW + - '_line_ending' + - ')' - ); + svgPath.attr('marker-end', markerUrl(svg, markers.ReqMarkers.ARROW + '_line_ending')); } addEdgeLabel(svg, svgPath, conf, `<<${rel.type}>>`); diff --git a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js index 5aebd1e3a5..18bbd2f0da 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js +++ b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js @@ -1808,15 +1808,6 @@ describe('when rendering a sequenceDiagram with directives', function () { mermaidAPI.initialize({ sequence: conf }); }); - let conf; - beforeEach(function () { - mermaidAPI.reset(); - // diagram.db = sequenceDb; - diagram.db.clear(); - conf = diagram.db.getConfig(); - diagram.renderer.bounds.init(); - }); - it('should handle one actor, when theme is dark and logLevel is 1 DX1 (dfg1)', function () { const str = ` %%{init: { "theme": "dark", "logLevel": 1 } }%% diff --git a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts index 19352ca723..a180202262 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts +++ b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts @@ -10,6 +10,7 @@ import assignWithDepth from '../../assignWithDepth'; import utils from '../../utils'; import { configureSvgSize } from '../../setupGraphViewbox'; import addSVGAccessibilityFields from '../../accessibility'; +import { markerUrl } from '../../markers'; let conf = {}; @@ -238,7 +239,7 @@ const drawNote = function (elem, noteModel) { const textHeight = Math.round( textElem - .map((te) => (te._groups || te)[0][0].getBBox().height) + .map((te) => (te._groups || te)[0][0]?.getBBox().height || 0) .reduce((acc, curr) => acc + curr) ); @@ -408,35 +409,23 @@ const drawMessage = function (diagram, msgModel, lineStarty, diagObj) { line.attr('class', 'messageLine0'); } - let url = ''; - if (conf.arrowMarkerAbsolute) { - url = - window.location.protocol + - '//' + - window.location.host + - window.location.pathname + - window.location.search; - url = url.replace(/\(/g, '\\('); - url = url.replace(/\)/g, '\\)'); - } - line.attr('stroke-width', 2); line.attr('stroke', 'none'); // handled by theme/css anyway line.style('fill', 'none'); // remove any fill colour if (type === diagObj.db.LINETYPE.SOLID || type === diagObj.db.LINETYPE.DOTTED) { - line.attr('marker-end', 'url(' + url + '#arrowhead)'); + line.attr('marker-end', markerUrl(line, 'arrowhead)')); } if (type === diagObj.db.LINETYPE.SOLID_POINT || type === diagObj.db.LINETYPE.DOTTED_POINT) { - line.attr('marker-end', 'url(' + url + '#filled-head)'); + line.attr('marker-end', markerUrl(line, 'ailled-head)')); } if (type === diagObj.db.LINETYPE.SOLID_CROSS || type === diagObj.db.LINETYPE.DOTTED_CROSS) { - line.attr('marker-end', 'url(' + url + '#crosshead)'); + line.attr('marker-end', markerUrl(line, 'crosshead)')); } // add node number if (sequenceVisible || conf.showSequenceNumbers) { - line.attr('marker-start', 'url(' + url + '#sequencenumber)'); + line.attr('marker-start', markerUrl(line, 'sequencenumber)')); diagram .append('text') .attr('x', startx) diff --git a/packages/mermaid/src/diagrams/sequence/svgDraw.js b/packages/mermaid/src/diagrams/sequence/svgDraw.js index fd70871e04..90e45a97dd 100644 --- a/packages/mermaid/src/diagrams/sequence/svgDraw.js +++ b/packages/mermaid/src/diagrams/sequence/svgDraw.js @@ -1,31 +1,20 @@ import common from '../common/common'; import { addFunction } from '../../interactionDb'; import { sanitizeUrl } from '@braintree/sanitize-url'; +import { appendMarker } from '../../markers'; -export const drawRect = function (elem, rectData) { - const rectElem = elem.append('rect'); - rectElem.attr('x', rectData.x); - rectElem.attr('y', rectData.y); - rectElem.attr('fill', rectData.fill); - rectElem.attr('stroke', rectData.stroke); - rectElem.attr('width', rectData.width); - rectElem.attr('height', rectData.height); - rectElem.attr('rx', rectData.rx); - rectElem.attr('ry', rectData.ry); - - if (typeof rectData.class !== 'undefined') { - rectElem.attr('class', rectData.class); - } - - return rectElem; -}; - -// const sanitizeUrl = function (s) { -// return s -// .replace(/&/g, '&') -// .replace(/ + elem + .append('rect') + .attr('x', data.x) + .attr('y', data.y) + .attr('fill', data.fill) + .attr('stroke', data.stroke) + .attr('width', data.width) + .attr('height', data.height) + .attr('rx', data.rx) + .attr('ry', data.ry) + .attr('class', data.class); const addPopupInteraction = (id, actorCnt) => { addFunction(() => { @@ -267,7 +256,7 @@ export const drawText = function (elem, textData) { typeof textData.textMargin !== 'undefined' && textData.textMargin > 0 ) { - textHeight += (textElem._groups || textElem)[0][0].getBBox().height; + textHeight += (textElem._groups || textElem)[0][0]?.getBBox().height || 0; prevTextHeight = textHeight; } @@ -400,14 +389,9 @@ const drawActorTypeParticipant = function (elem, actor, conf) { conf ); - let height = actor.height; - if (rectElem.node) { - const bounds = rectElem.node().getBBox(); - actor.height = bounds.height; - height = bounds.height; - } + actor.height = rectElem.node()?.getBBox().height || actor.height; - return height; + return actor.height; }; const drawActorTypeActor = function (elem, actor, conf) { @@ -642,6 +626,7 @@ export const drawBackgroundRect = function (elem, bounds) { class: 'rect', }); rectElem.lower(); + return rectElem; }; export const insertDatabaseIcon = function (elem) { @@ -695,10 +680,7 @@ export const insertClockIcon = function (elem) { * @param elem */ export const insertArrowHead = function (elem) { - elem - .append('defs') - .append('marker') - .attr('id', 'arrowhead') + appendMarker(elem, 'arrowhead') .attr('refX', 9) .attr('refY', 5) .attr('markerUnits', 'userSpaceOnUse') @@ -714,10 +696,7 @@ export const insertArrowHead = function (elem) { * @param {any} elem */ export const insertArrowFilledHead = function (elem) { - elem - .append('defs') - .append('marker') - .attr('id', 'filled-head') + appendMarker(elem, 'filled-head') .attr('refX', 18) .attr('refY', 7) .attr('markerWidth', 20) @@ -732,10 +711,7 @@ export const insertArrowFilledHead = function (elem) { * @param {any} elem */ export const insertSequenceNumber = function (elem) { - elem - .append('defs') - .append('marker') - .attr('id', 'sequencenumber') + appendMarker(elem, 'sequencenumber') .attr('refX', 15) .attr('refY', 15) .attr('markerWidth', 60) @@ -753,10 +729,7 @@ export const insertSequenceNumber = function (elem) { * @param {any} elem */ export const insertArrowCrossHead = function (elem) { - const defs = elem.append('defs'); - const marker = defs - .append('marker') - .attr('id', 'crosshead') + const marker = appendMarker(elem, 'crosshead') .attr('markerWidth', 15) .attr('markerHeight', 8) .attr('orient', 'auto') diff --git a/packages/mermaid/src/diagrams/sequence/svgDraw.spec.js b/packages/mermaid/src/diagrams/sequence/svgDraw.spec.js index 580dafe896..078a6ee53f 100644 --- a/packages/mermaid/src/diagrams/sequence/svgDraw.spec.js +++ b/packages/mermaid/src/diagrams/sequence/svgDraw.spec.js @@ -1,135 +1,118 @@ import svgDraw from './svgDraw'; -import { MockD3 } from 'd3'; +import { select } from 'd3'; describe('svgDraw', function () { - describe('drawRect', function () { - it('should append a rectangle', function () { - const svg = MockD3('svg'); - svgDraw.drawRect(svg, { - x: 10, - y: 10, - fill: '#ccc', - stroke: 'red', - width: '20', - height: '20', - rx: '10', - ry: '10', - class: 'unitTestRectangleClass', - }); - expect(svg.__children.length).toBe(1); - const rect = svg.__children[0]; - expect(rect.__name).toBe('rect'); - expect(rect.attr).toHaveBeenCalledWith('x', 10); - expect(rect.attr).toHaveBeenCalledWith('y', 10); - expect(rect.attr).toHaveBeenCalledWith('fill', '#ccc'); - expect(rect.attr).toHaveBeenCalledWith('stroke', 'red'); - expect(rect.attr).toHaveBeenCalledWith('width', '20'); - expect(rect.attr).toHaveBeenCalledWith('height', '20'); - expect(rect.attr).toHaveBeenCalledWith('rx', '10'); - expect(rect.attr).toHaveBeenCalledWith('ry', '10'); - expect(rect.attr).toHaveBeenCalledWith('class', 'unitTestRectangleClass'); - }); - it('should not add the class attribute if a class isn`t provided', () => { - const svg = MockD3('svg'); - svgDraw.drawRect(svg, { - x: 10, - y: 10, - fill: '#ccc', - stroke: 'red', - width: '20', - height: '20', - rx: '10', - ry: '10', + let svg; + + beforeEach(() => { + document.body.innerHTML = ''; + svg = select('svg'); + }); + + describe('drawRect', () => { + it('should append a rectangle', () => { + const rect = svgDraw.drawRect(svg, { + x: '10', + y: '20', + width: '30', + height: '40', + rx: '50', + ry: '60', + fill: 'red', + stroke: 'blue', + class: 'test-class', }); - expect(svg.__children.length).toBe(1); - const rect = svg.__children[0]; - expect(rect.__name).toBe('rect'); - expect(rect.attr).toHaveBeenCalledWith('fill', '#ccc'); - expect(rect.attr).not.toHaveBeenCalledWith('class', expect.anything()); + + expect(svg.select('rect').size()).toBe(1); + + expect(rect.attr('x')).toBe('10'); + expect(rect.attr('y')).toBe('20'); + expect(rect.attr('width')).toBe('30'); + expect(rect.attr('height')).toBe('40'); + expect(rect.attr('rx')).toBe('50'); + expect(rect.attr('ry')).toBe('60'); + expect(rect.attr('fill')).toBe('red'); + expect(rect.attr('stroke')).toBe('blue'); + expect(rect.attr('class')).toBe('test-class'); }); }); + describe('drawText', function () { - it('should append a single element', function () { - const svg = MockD3('svg'); - svgDraw.drawText(svg, { - x: 10, - y: 10, + it('should append a single text element', function () { + const texts = svgDraw.drawText(svg, { + x: '10', + y: '10', dy: '1em', text: 'One fine text message', - class: 'noteText', + class: 'test-class', fontFamily: 'courier', fontSize: '10px', fontWeight: '500', }); - expect(svg.__children.length).toBe(1); - const text = svg.__children[0]; - expect(text.__name).toBe('text'); - expect(text.attr).toHaveBeenCalledWith('x', 10); - expect(text.attr).toHaveBeenCalledWith('y', 10); - expect(text.attr).toHaveBeenCalledWith('dy', '1em'); - expect(text.attr).toHaveBeenCalledWith('class', 'noteText'); - expect(text.text).toHaveBeenCalledWith('One fine text message'); - expect(text.style).toHaveBeenCalledWith('font-family', 'courier'); - expect(text.style).toHaveBeenCalledWith('font-size', '10px'); - expect(text.style).toHaveBeenCalledWith('font-weight', '500'); + + expect(texts.length).toBe(1); + expect(svg.selectAll('text').size()).toBe(1); + + expect(texts[0].attr('x')).toBe('10'); + expect(texts[0].attr('y')).toBe('10'); + expect(texts[0].attr('dy')).toBe('1em'); + expect(texts[0].attr('class')).toBe('test-class'); + expect(texts[0].text()).toBe('One fine text message'); + expect(texts[0].style('font-family')).toBe('courier'); + expect(texts[0].style('font-size')).toBe('10px'); + expect(texts[0].style('font-weight')).toBe('500'); }); - it('should append a multiple elements', function () { - const svg = MockD3('svg'); - svgDraw.drawText(svg, { - x: 10, - y: 10, - text: 'One fine text message
with multiple
fine lines', + + it('should append multiple text elements when
present', function () { + const texts = svgDraw.drawText(svg, { + x: '10', + y: '20', + text: 'line 1
line 2
line 3', + }); + + expect(texts.length).toBe(3); + expect(svg.selectAll('text').size()).toBe(3); + + expect(texts[0].text()).toBe('line 1'); + expect(texts[1].text()).toBe('line 2'); + expect(texts[2].text()).toBe('line 3'); + + texts.forEach((text) => { + expect(text.attr('x')).toBe('10'); + expect(text.attr('y')).toBe('20'); }); - expect(svg.__children.length).toBe(3); - const text1 = svg.__children[0]; - expect(text1.__name).toBe('text'); - expect(text1.attr).toHaveBeenCalledWith('x', 10); - expect(text1.attr).toHaveBeenCalledWith('y', 10); - expect(text1.text).toHaveBeenCalledWith('One fine text message'); - - const text2 = svg.__children[1]; - expect(text2.__name).toBe('text'); - expect(text2.attr).toHaveBeenCalledWith('x', 10); - expect(text2.attr).toHaveBeenCalledWith('y', 10); - expect(text2.text).toHaveBeenCalledWith('with multiple'); - - const text3 = svg.__children[2]; - expect(text3.__name).toBe('text'); - expect(text3.attr).toHaveBeenCalledWith('x', 10); - expect(text3.attr).toHaveBeenCalledWith('y', 10); - expect(text3.text).toHaveBeenCalledWith('fine lines'); }); }); + describe('drawBackgroundRect', function () { it('should append a rect before the previous element within a given bound', function () { - const svg = MockD3('svg'); - const boundingRect = { - startx: 50, - starty: 200, - stopx: 150, - stopy: 260, + const rect = svgDraw.drawBackgroundRect(svg, { + startx: '50', + starty: '200', + stopx: '150', + stopy: '260', title: undefined, - fill: '#ccc', - }; - svgDraw.drawBackgroundRect(svg, boundingRect); - expect(svg.__children.length).toBe(1); - const rect = svg.__children[0]; - expect(rect.__name).toBe('rect'); - expect(rect.attr).toHaveBeenCalledWith('x', 50); - expect(rect.attr).toHaveBeenCalledWith('y', 200); - expect(rect.attr).toHaveBeenCalledWith('width', 100); - expect(rect.attr).toHaveBeenCalledWith('height', 60); - expect(rect.attr).toHaveBeenCalledWith('fill', '#ccc'); - expect(rect.attr).toHaveBeenCalledWith('class', 'rect'); - expect(rect.lower).toHaveBeenCalled(); + fill: 'red', + }); + + expect(svg.selectAll('rect').size()).toBe(1); + + expect(rect.attr('x')).toBe('50'); + expect(rect.attr('y')).toBe('200'); + expect(rect.attr('width')).toBe('100'); + expect(rect.attr('height')).toBe('60'); + expect(rect.attr('fill')).toBe('red'); + expect(rect.attr('class')).toBe('rect'); }); }); + describe('sanitizeUrl', function () { it('should sanitize malicious urls', function () { const maliciousStr = 'javascript:script:alert(1)'; const result = svgDraw.sanitizeUrl(maliciousStr); expect(result).not.toContain('javascript:alert(1)'); }); + it('should not sanitize non dangerous urls', function () { const maliciousStr = 'javajavascript:script:alert(1)'; const result = svgDraw.sanitizeUrl(maliciousStr); diff --git a/packages/mermaid/src/diagrams/state/shapes.js b/packages/mermaid/src/diagrams/state/shapes.js index e2286bb511..4c26025965 100644 --- a/packages/mermaid/src/diagrams/state/shapes.js +++ b/packages/mermaid/src/diagrams/state/shapes.js @@ -5,7 +5,7 @@ import utils from '../../utils'; import common from '../common/common'; import { getConfig } from '../../config'; import { log } from '../../logger'; - +import { markerUrl } from '../../markers'; /** * Draws a start state as a black circle * @@ -427,27 +427,12 @@ export const drawEdge = function (elem, path, relation) { }) .curve(curveBasis); - const svgPath = elem + elem .append('path') .attr('d', lineFunction(lineData)) .attr('id', 'edge' + edgeCount) - .attr('class', 'transition'); - let url = ''; - if (getConfig().state.arrowMarkerAbsolute) { - url = - window.location.protocol + - '//' + - window.location.host + - window.location.pathname + - window.location.search; - url = url.replace(/\(/g, '\\('); - url = url.replace(/\)/g, '\\)'); - } - - svgPath.attr( - 'marker-end', - 'url(' + url + '#' + getRelationType(stateDb.relationType.DEPENDENCY) + 'End' + ')' - ); + .attr('class', 'transition') + .attr('marker-end', markerUrl(elem, getRelationType(stateDb.relationType.DEPENDENCY) + 'End')); if (typeof relation.title !== 'undefined') { const label = elem.append('g').attr('class', 'stateLabel'); diff --git a/packages/mermaid/src/diagrams/state/stateRenderer.js b/packages/mermaid/src/diagrams/state/stateRenderer.js index 75368c5576..5e875bc8f4 100644 --- a/packages/mermaid/src/diagrams/state/stateRenderer.js +++ b/packages/mermaid/src/diagrams/state/stateRenderer.js @@ -7,6 +7,7 @@ import { drawState, addTitleAndBox, drawEdge } from './shapes'; import { getConfig } from '../../config'; import { configureSvgSize } from '../../setupGraphViewbox'; import addSVGAccessibilityFields from '../../accessibility'; +import { appendMarker } from '../../markers'; // TODO Move conf object to main conf in mermaidAPI let conf; @@ -23,17 +24,16 @@ export const setConf = function () { * @param {any} elem */ const insertMarkers = function (elem) { - elem - .append('defs') - .append('marker') - .attr('id', 'dependencyEnd') + appendMarker(elem, 'dependencyEnd') .attr('refX', 19) .attr('refY', 7) .attr('markerWidth', 20) .attr('markerHeight', 28) .attr('orient', 'auto') + .attr('class', 'marker') .append('path') - .attr('d', 'M 19,7 L9,13 L14,7 L9,1 Z'); + .attr('d', 'M 19,7 L9,13 L14,7 L9,1 Z') + .attr('class', 'arrowheadPath'); }; /** diff --git a/packages/mermaid/src/diagrams/state/styles.js b/packages/mermaid/src/diagrams/state/styles.js index 4a1c465122..4e9693e859 100644 --- a/packages/mermaid/src/diagrams/state/styles.js +++ b/packages/mermaid/src/diagrams/state/styles.js @@ -1,9 +1,5 @@ const getStyles = (options) => ` -defs #statediagram-barbEnd { - fill: ${options.transitionColor}; - stroke: ${options.transitionColor}; - } g.stateGroup text { fill: ${options.nodeBorder}; stroke: none; @@ -112,9 +108,6 @@ g.stateGroup line { stroke: ${options.stateBorder || options.nodeBorder};; stroke-width: 1px; } -#statediagram-barbEnd { - fill: ${options.lineColor}; -} .statediagram-cluster rect { fill: ${options.compositeTitleBackground}; diff --git a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts index e3ebb839cf..c76e9b250a 100644 --- a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts +++ b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts @@ -4,6 +4,7 @@ import svgDraw from './svgDraw'; import { getConfig } from '../../config'; import { configureSvgSize } from '../../setupGraphViewbox'; import addSVGAccessibilityFields from '../../accessibility'; +import { markerUrl } from '../../markers'; export const setConf = function (cnf) { const keys = Object.keys(cnf); @@ -113,7 +114,7 @@ export const draw = function (text, id, version, diagObj) { .attr('y2', conf.height * 4) .attr('stroke-width', 4) .attr('stroke', 'black') - .attr('marker-end', 'url(#arrowhead)'); + .attr('marker-end', markerUrl(diagram, 'arrowhead')); const extraVertForTitle = title ? 70 : 0; diagram.attr('viewBox', `${box.startx} -25 ${width} ${height + extraVertForTitle}`); diff --git a/packages/mermaid/src/diagrams/user-journey/svgDraw.js b/packages/mermaid/src/diagrams/user-journey/svgDraw.js index f655b9c3a9..94dd013239 100644 --- a/packages/mermaid/src/diagrams/user-journey/svgDraw.js +++ b/packages/mermaid/src/diagrams/user-journey/svgDraw.js @@ -1,4 +1,5 @@ import { arc as d3arc } from 'd3'; +import { appendMarker } from '../../markers'; export const drawRect = function (elem, rectData) { const rectElem = elem.append('rect'); @@ -451,10 +452,7 @@ const _drawTextCandidateFunc = (function () { })(); const initGraphics = function (graphics) { - graphics - .append('defs') - .append('marker') - .attr('id', 'arrowhead') + appendMarker(graphics, 'arrowhead') .attr('refX', 5) .attr('refY', 2) .attr('markerWidth', 6) diff --git a/packages/mermaid/src/markers.spec.js b/packages/mermaid/src/markers.spec.js new file mode 100644 index 0000000000..6c615fd858 --- /dev/null +++ b/packages/mermaid/src/markers.spec.js @@ -0,0 +1,84 @@ +import { select } from 'd3'; +import { appendMarker, markerUrl } from './markers'; +import { setSiteConfig } from './config'; + +describe('markers', () => { + describe('#markerUrl', () => { + const markerUrlForName = (name) => markerUrl(select('empty'), name); + + it('should use parent SVG element id as a prefix', () => { + const markerUrlForElement = (id) => markerUrl(select('#' + id), 'marker'); + + document.body.innerHTML = ` + + + + + + + + + + + + + + + + `; + + expect(markerUrlForElement('a')).toBe('url(#svg-1-marker)'); + expect(markerUrlForElement('b')).toBe('url(#svg-2-marker)'); + expect(markerUrlForElement('c')).toBe('url(#svg-3-marker)'); + expect(markerUrlForElement('d')).toBe('url(#svg-4-marker)'); + expect(markerUrlForElement('e')).toBe('url(#marker)'); + }); + + it('should support absolute urls for flowcharts', () => { + setSiteConfig({ flowchart: { arrowMarkerAbsolute: true } }); + expect(markerUrlForName('marker')).toBe('url(' + window.location.href + '#marker)'); + }); + + it('should support absolute urls for state diagrams', () => { + setSiteConfig({ state: { arrowMarkerAbsolute: true } }); + expect(markerUrlForName('marker')).toBe('url(' + window.location.href + '#marker)'); + }); + }); + + describe('#appendMarker', () => { + it('should prefix the marker id with the id of its parent SVG', () => { + document.body.innerHTML = ` + + + + `; + const g = select('g'); + + appendMarker(g, 'marker1'); + const marker1 = appendMarker(g, 'marker1'); + + expect(marker1.attr('id')).toBe('svg-1-marker1'); + }); + + it('should just use the marker name for the id if no parent SVG found', () => { + document.body.innerHTML = ''; + const g = select('g'); + + const marker1 = appendMarker(g, 'marker1'); + + expect(marker1.attr('id')).toBe('marker1'); + }); + + it('should place all markers in the same element', () => { + document.body.innerHTML = ''; + const g = select('g'); + + appendMarker(g, 'marker1'); + appendMarker(g, 'marker2'); + appendMarker(g, 'marker3'); + + expect(g.selectAll('g defs').size()).toBe(1); + expect(g.selectAll('g defs marker').size()).toBe(3); + }); + }); +}); diff --git a/packages/mermaid/src/markers.ts b/packages/mermaid/src/markers.ts new file mode 100644 index 0000000000..2861004bab --- /dev/null +++ b/packages/mermaid/src/markers.ts @@ -0,0 +1,74 @@ +import { getConfig } from './config'; + +/** + * Append a marker to a graphics element assigning it an id that includes the id of it's parent SVG + * element. + * + * The marker's id will be the concatenation of the id of g's parent SVG element (if found) and the + * given name using '-' as a separator. + * + * @param {SVGGraphicsElement} g + * @param {string} name + * @returns {SVGMarkerElement} SVG element. + */ +export const appendMarker = function (g: SVGGraphicsElement, name: string): SVGMarkerElement { + // @ts-ignore TODO Fix ts errors + let defs = g.select('defs'); + + if (defs.empty()) { + defs = g.append('defs'); + } + + return defs.append('marker').attr('id', markerId(g, name)); +}; + +/** + * Returns the url for a marker. + * + * The fragment portion of the url will be the marker's id. + * + * @param {SVGElement} elem + * @param {string} name + * @returns {string} A marker url. + */ +export const markerUrl = function (elem: SVGElement, name: string): string { + return 'url(' + url() + '#' + markerId(elem, name) + ')'; +}; + +const url = function () { + return shouldBeAbsolute() ? window.location.href : ''; +}; + +const shouldBeAbsolute = function () { + // @ts-ignore TODO Fix ts errors + return getConfig().flowchart.arrowMarkerAbsolute || getConfig().state.arrowMarkerAbsolute; +}; + +const markerId = function (elem: SVGElement, name: string) { + return diagramId(elem) ? diagramId(elem) + '-' + name : name; +}; + +const diagramId = function (elem: SVGElement): string | null { + // @ts-ignore TODO Fix ts errors + let node = elem.node(); + + while (tagName(node) !== 'svg') { + // Happens when we reach the Document object + if (!node?.tagName) { + return null; + } + + node = node.parentNode; + } + + return node?.getAttribute('id'); +}; + +const tagName = function (node: Element): string | null { + return node?.tagName?.toLowerCase(); +}; + +export default { + appendMarker, + markerUrl, +}; diff --git a/packages/mermaid/src/mermaid.spec.ts b/packages/mermaid/src/mermaid.spec.ts index df6439c825..db45ce3c45 100644 --- a/packages/mermaid/src/mermaid.spec.ts +++ b/packages/mermaid/src/mermaid.spec.ts @@ -1,62 +1,67 @@ import mermaid from './mermaid'; -import { mermaidAPI } from './mermaidAPI'; -import './diagram-api/diagram-orchestration'; -import { vi, describe, it, beforeEach, afterEach, expect } from 'vitest'; -const spyOn = vi.spyOn; +import { getConfig, setConfig } from './config'; +import { MermaidConfig } from './config.type'; +import { describe, it, beforeEach, afterEach, expect } from 'vitest'; -vi.mock('./mermaidAPI'); +describe('mermaid', () => { + const initialContent = '
graph TD;a
'; -afterEach(() => { - vi.restoreAllMocks(); -}); + beforeEach(() => { + document.body.innerHTML = initialContent; + }); -describe('when using mermaid and ', function () { - describe('when detecting chart type ', function () { - it('should not start rendering with mermaid.startOnLoad set to false', function () { - mermaid.startOnLoad = false; - document.body.innerHTML = '
graph TD;\na;
'; - spyOn(mermaid, 'init'); - mermaid.contentLoaded(); - expect(mermaid.init).not.toHaveBeenCalled(); + describe('#contentLoaded', () => { + let startOnLoad: boolean; + let initialConfig: MermaidConfig; + + beforeEach(() => { + startOnLoad = mermaid.startOnLoad; + initialConfig = getConfig(); }); - it('should start rendering with both startOnLoad set', function () { + afterEach(() => { + mermaid.startOnLoad = startOnLoad; + setConfig(initialConfig); + }); + + it('should render when mermaid.startOnLoad and config.startOnLoad are true', () => { mermaid.startOnLoad = true; - document.body.innerHTML = '
graph TD;\na;
'; - spyOn(mermaid, 'init'); + setConfig({ startOnLoad: true }); + mermaid.contentLoaded(); - expect(mermaid.init).toHaveBeenCalled(); + expect(document.body.innerHTML).not.toBe(initialContent); }); - it('should start rendering with mermaid.startOnLoad', function () { - mermaid.startOnLoad = true; - document.body.innerHTML = '
graph TD;\na;
'; - spyOn(mermaid, 'init'); + it('should not render when mermaid.startOnLoad is false', () => { + mermaid.startOnLoad = false; + mermaid.contentLoaded(); - expect(mermaid.init).toHaveBeenCalled(); + expect(document.body.innerHTML).toBe(initialContent); }); - it('should start rendering as a default with no changes performed', function () { - document.body.innerHTML = '
graph TD;\na;
'; - spyOn(mermaid, 'init'); + it('should not render when config.startOnLoad is undefined', () => { + setConfig({ startOnLoad: undefined }); + mermaid.contentLoaded(); - expect(mermaid.init).toHaveBeenCalled(); + expect(document.body.innerHTML).toBe(initialContent); }); - }); - describe('when using #initThrowsErrors', function () { - it('should accept single node', async () => { - const node = document.createElement('div'); - node.appendChild(document.createTextNode('graph TD;\na;')); + it('should not render when config.startOnLoad set to false', () => { + setConfig({ startOnLoad: false }); + + mermaid.contentLoaded(); + expect(document.body.innerHTML).toBe(initialContent); + }); + }); - mermaid.initThrowsErrors(undefined, node); - // mermaidAPI.render function has been mocked, since it doesn't yet work - // in Node.JS (only works in browser) - expect(mermaidAPI.render).toHaveBeenCalled(); + describe('#initThrowsErrors', () => { + it('should render', async () => { + mermaid.initThrowsErrors(undefined, 'div'); + expect(document.body.innerHTML).not.toBe(initialContent); }); }); - describe('checking validity of input ', function () { + describe('#parse', () => { it('should throw for an invalid definition', function () { expect(() => mermaid.parse('this is not a mermaid diagram definition')).toThrow(); }); @@ -82,7 +87,7 @@ describe('when using mermaid and ', function () { expect(() => mermaid.parse(text)).not.toThrow(); }); - it('should throw for an invalid sequenceDiagram definition', function () { + it('should throw for an invalid sequenceDiagram definition', () => { const text = 'sequenceDiagram\n' + 'Alice:->Bob: Hello Bob, how are you?\n\n' + @@ -96,7 +101,7 @@ describe('when using mermaid and ', function () { expect(() => mermaid.parse(text)).toThrow(); }); - it('should return false for invalid definition WITH a parseError() callback defined', function () { + it('should return false for invalid definition WITH a parseError() callback defined', () => { let parseErrorWasCalled = false; mermaid.setParseErrorHandler(() => { parseErrorWasCalled = true; diff --git a/packages/mermaid/src/tests/setup.ts b/packages/mermaid/src/tests/setup.ts index e8058c5179..1ede7c2b58 100644 --- a/packages/mermaid/src/tests/setup.ts +++ b/packages/mermaid/src/tests/setup.ts @@ -1,3 +1,30 @@ import { vi } from 'vitest'; -vi.mock('d3'); -vi.mock('dagre-d3'); + +const importActual = (name: string) => { + vi.mock(name, async () => { + const module: object = await vi.importActual(name); + + return { ...module, get: vi.fn() }; + }); +}; + +// Modules that we don't want mocked by default. +importActual('d3'); +importActual('dagre-d3'); + +// dagre-d3 requires that window.d3 be set. +// See https://github.com/dagrejs/dagre-d3/issues/198. +import * as d3 from 'd3'; +window.d3 = d3; + +// jsdom does not provide getBBox() so we will polyfill. +// See https://github.com/jsdom/jsdom/issues/3159. +Object.defineProperty(window.SVGElement.prototype, 'getBBox', { + writable: true, + value: () => ({ + x: 0, + y: 0, + width: 0, + height: 0, + }), +});