diff --git a/cSpell.json b/cSpell.json index e8d718316c..e67c7d48e6 100644 --- a/cSpell.json +++ b/cSpell.json @@ -22,6 +22,7 @@ "brkt", "brolin", "brotli", + "catmull", "città", "classdef", "codedoc", diff --git a/docs/config/setup/interfaces/mermaidAPI.ParseOptions.md b/docs/config/setup/interfaces/mermaidAPI.ParseOptions.md index 56f9146419..ea390899e4 100644 --- a/docs/config/setup/interfaces/mermaidAPI.ParseOptions.md +++ b/docs/config/setup/interfaces/mermaidAPI.ParseOptions.md @@ -16,4 +16,4 @@ #### Defined in -[mermaidAPI.ts:76](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L76) +[mermaidAPI.ts:59](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L59) diff --git a/docs/config/setup/interfaces/mermaidAPI.RenderResult.md b/docs/config/setup/interfaces/mermaidAPI.RenderResult.md index 2c1504285d..18ee5e4316 100644 --- a/docs/config/setup/interfaces/mermaidAPI.RenderResult.md +++ b/docs/config/setup/interfaces/mermaidAPI.RenderResult.md @@ -39,7 +39,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present. #### Defined in -[mermaidAPI.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L96) +[mermaidAPI.ts:79](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L79) --- @@ -51,4 +51,4 @@ The svg code for the rendered graph. #### Defined in -[mermaidAPI.ts:86](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L86) +[mermaidAPI.ts:69](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L69) diff --git a/docs/config/setup/modules/mermaidAPI.md b/docs/config/setup/modules/mermaidAPI.md index 1ea19fac4e..0a948b6f39 100644 --- a/docs/config/setup/modules/mermaidAPI.md +++ b/docs/config/setup/modules/mermaidAPI.md @@ -25,7 +25,7 @@ Renames and re-exports [mermaidAPI](mermaidAPI.md#mermaidapi) #### Defined in -[mermaidAPI.ts:80](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L80) +[mermaidAPI.ts:63](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L63) ## Variables @@ -96,7 +96,7 @@ mermaid.initialize(config); #### Defined in -[mermaidAPI.ts:662](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L662) +[mermaidAPI.ts:641](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L641) ## Functions @@ -127,7 +127,7 @@ Return the last node appended #### Defined in -[mermaidAPI.ts:318](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L318) +[mermaidAPI.ts:299](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L299) --- @@ -153,13 +153,13 @@ the cleaned up svgCode #### Defined in -[mermaidAPI.ts:264](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L264) +[mermaidAPI.ts:245](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L245) --- ### createCssStyles -▸ **createCssStyles**(`config`, `graphType`, `classDefs?`): `string` +▸ **createCssStyles**(`config`, `classDefs?`): `string` Create the user styles @@ -168,7 +168,6 @@ Create the user styles | Name | Type | Description | | :---------- | :------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------ | | `config` | `MermaidConfig` | configuration that has style and theme settings to use | -| `graphType` | `string` | used for checking if classDefs should be applied | | `classDefs` | `undefined` \| `null` \| `Record`<`string`, `DiagramStyleClassDef`> | the classDefs in the diagram text. Might be null if none were defined. Usually is the result of a call to getClasses(...) | #### Returns @@ -179,7 +178,7 @@ the string with all the user styles #### Defined in -[mermaidAPI.ts:193](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L193) +[mermaidAPI.ts:175](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L175) --- @@ -189,12 +188,12 @@ the string with all the user styles #### Parameters -| Name | Type | -| :---------- | :----------------------------------------- | -| `config` | `MermaidConfig` | -| `graphType` | `string` | -| `classDefs` | `Record`<`string`, `DiagramStyleClassDef`> | -| `svgId` | `string` | +| Name | Type | +| :---------- | :-------------------------------------------------------- | +| `config` | `MermaidConfig` | +| `graphType` | `string` | +| `classDefs` | `undefined` \| `Record`<`string`, `DiagramStyleClassDef`> | +| `svgId` | `string` | #### Returns @@ -202,7 +201,7 @@ the string with all the user styles #### Defined in -[mermaidAPI.ts:241](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L241) +[mermaidAPI.ts:222](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L222) --- @@ -229,7 +228,7 @@ with an enclosing block that has each of the cssClasses followed by !important; #### Defined in -[mermaidAPI.ts:177](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L177) +[mermaidAPI.ts:160](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L160) --- @@ -249,7 +248,7 @@ with an enclosing block that has each of the cssClasses followed by !important; #### Defined in -[mermaidAPI.ts:163](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L163) +[mermaidAPI.ts:146](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L146) --- @@ -269,7 +268,7 @@ with an enclosing block that has each of the cssClasses followed by !important; #### Defined in -[mermaidAPI.ts:134](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L134) +[mermaidAPI.ts:117](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L117) --- @@ -295,7 +294,7 @@ Put the svgCode into an iFrame. Return the iFrame code #### Defined in -[mermaidAPI.ts:295](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L295) +[mermaidAPI.ts:276](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L276) --- @@ -320,4 +319,4 @@ Remove any existing elements from the given document #### Defined in -[mermaidAPI.ts:368](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L368) +[mermaidAPI.ts:349](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L349) diff --git a/package.json b/package.json index 232f23be1d..7a6a032d72 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "10.2.4", "description": "Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.", "type": "module", - "packageManager": "pnpm@8.7.1", + "packageManager": "pnpm@8.7.5", "keywords": [ "diagram", "markdown", @@ -19,6 +19,7 @@ "build:mermaid": "pnpm build:vite --mermaid", "build:viz": "pnpm build:mermaid --visualize", "build:types": "tsc -p ./packages/mermaid/tsconfig.json --emitDeclarationOnly && tsc -p ./packages/mermaid-zenuml/tsconfig.json --emitDeclarationOnly && tsc -p ./packages/mermaid-example-diagram/tsconfig.json --emitDeclarationOnly", + "build:types:watch": "tsc -p ./packages/mermaid/tsconfig.json --emitDeclarationOnly --watch", "build:watch": "pnpm build:vite --watch", "build": "pnpm run -r clean && pnpm build:types && pnpm build:vite", "dev": "concurrently \"pnpm build:vite --watch\" \"ts-node-esm .vite/server.ts\"", diff --git a/packages/mermaid/package.json b/packages/mermaid/package.json index 7b4de70a98..e208d561e5 100644 --- a/packages/mermaid/package.json +++ b/packages/mermaid/package.json @@ -1,6 +1,6 @@ { "name": "mermaid", - "version": "10.4.0", + "version": "10.5.0-alpha.1", "description": "Markdown-ish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.", "type": "module", "module": "./dist/mermaid.core.mjs", diff --git a/packages/mermaid/src/dagre-wrapper/edges.js b/packages/mermaid/src/dagre-wrapper/edges.js index babb0ea6a9..1b3e172c01 100644 --- a/packages/mermaid/src/dagre-wrapper/edges.js +++ b/packages/mermaid/src/dagre-wrapper/edges.js @@ -5,6 +5,7 @@ import { line, curveBasis, select } from 'd3'; import { getConfig } from '../config.js'; import utils from '../utils.js'; import { evaluate } from '../diagrams/common/common.js'; +import { getLineFunctionsWithOffset } from '../utils/lineWithOffset.js'; let edgeLabels = {}; let terminalLabels = {}; @@ -368,20 +369,6 @@ const cutPathAtIntersect = (_points, boundryNode) => { return points; }; -/** - * Calculate the deltas and angle between two points - * @param {{x: number, y:number}} point1 - * @param {{x: number, y:number}} point2 - * @returns {{angle: number, deltaX: number, deltaY: number}} - */ -function calculateDeltaAndAngle(point1, point2) { - const [x1, y1] = [point1.x, point1.y]; - const [x2, y2] = [point2.x, point2.y]; - const deltaX = x2 - x1; - const deltaY = y2 - y1; - return { angle: Math.atan(deltaY / deltaX), deltaX, deltaY }; -} - export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph, id) { let points = edge.points; let pointsHasChanged = false; @@ -456,56 +443,8 @@ export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph curve = edge.curve; } - // We need to draw the lines a bit shorter to avoid drawing - // under any transparent markers. - // The offsets are calculated from the markers' dimensions. - const markerOffsets = { - aggregation: 18, - extension: 18, - composition: 18, - dependency: 6, - lollipop: 13.5, - arrow_point: 5.3, - }; - - const lineFunction = line() - .x(function (d, i, data) { - let offset = 0; - if (i === 0 && Object.hasOwn(markerOffsets, edge.arrowTypeStart)) { - // Handle first point - // Calculate the angle and delta between the first two points - const { angle, deltaX } = calculateDeltaAndAngle(data[0], data[1]); - // Calculate the offset based on the angle and the marker's dimensions - offset = markerOffsets[edge.arrowTypeStart] * Math.cos(angle) * (deltaX >= 0 ? 1 : -1) || 0; - } else if (i === data.length - 1 && Object.hasOwn(markerOffsets, edge.arrowTypeEnd)) { - // Handle last point - // Calculate the angle and delta between the last two points - const { angle, deltaX } = calculateDeltaAndAngle( - data[data.length - 1], - data[data.length - 2] - ); - offset = markerOffsets[edge.arrowTypeEnd] * Math.cos(angle) * (deltaX >= 0 ? 1 : -1) || 0; - } - return d.x + offset; - }) - .y(function (d, i, data) { - // Same handling as X above - let offset = 0; - if (i === 0 && Object.hasOwn(markerOffsets, edge.arrowTypeStart)) { - const { angle, deltaY } = calculateDeltaAndAngle(data[0], data[1]); - offset = - markerOffsets[edge.arrowTypeStart] * Math.abs(Math.sin(angle)) * (deltaY >= 0 ? 1 : -1); - } else if (i === data.length - 1 && Object.hasOwn(markerOffsets, edge.arrowTypeEnd)) { - const { angle, deltaY } = calculateDeltaAndAngle( - data[data.length - 1], - data[data.length - 2] - ); - offset = - markerOffsets[edge.arrowTypeEnd] * Math.abs(Math.sin(angle)) * (deltaY >= 0 ? 1 : -1); - } - return d.y + offset; - }) - .curve(curve); + const { x, y } = getLineFunctionsWithOffset(edge); + const lineFunction = line().x(x).y(y).curve(curve); // Construct stroke classes based on properties let strokeClasses; diff --git a/packages/mermaid/src/dagre-wrapper/markers.js b/packages/mermaid/src/dagre-wrapper/markers.js index a499b8f263..91f7ecc80a 100644 --- a/packages/mermaid/src/dagre-wrapper/markers.js +++ b/packages/mermaid/src/dagre-wrapper/markers.js @@ -176,7 +176,7 @@ const point = (elem, type, id) => { .attr('id', id + '_' + type + '-pointStart') .attr('class', 'marker ' + type) .attr('viewBox', '0 0 10 10') - .attr('refX', 0) + .attr('refX', 4.5) .attr('refY', 5) .attr('markerUnits', 'userSpaceOnUse') .attr('markerWidth', 12) diff --git a/packages/mermaid/src/diagram-api/diagram-orchestration.ts b/packages/mermaid/src/diagram-api/diagram-orchestration.ts index 80665cfa2e..0357ad7feb 100644 --- a/packages/mermaid/src/diagram-api/diagram-orchestration.ts +++ b/packages/mermaid/src/diagram-api/diagram-orchestration.ts @@ -43,7 +43,11 @@ export const addDiagrams = () => { }, }, styles: {}, // should never be used - renderer: {}, // should never be used + renderer: { + draw: () => { + // should never be used + }, + }, parser: { parser: { yy: {} }, parse: () => { diff --git a/packages/mermaid/src/diagram-api/diagramAPI.spec.ts b/packages/mermaid/src/diagram-api/diagramAPI.spec.ts index b82011f8de..2cafd695ba 100644 --- a/packages/mermaid/src/diagram-api/diagramAPI.spec.ts +++ b/packages/mermaid/src/diagram-api/diagramAPI.spec.ts @@ -41,7 +41,11 @@ describe('DiagramAPI', () => { }, parser: { yy: {} }, }, - renderer: {}, + renderer: { + draw: () => { + // no-op + }, + }, styles: {}, }, detector diff --git a/packages/mermaid/src/diagram-api/types.ts b/packages/mermaid/src/diagram-api/types.ts index 15aa4b0336..58d98107ea 100644 --- a/packages/mermaid/src/diagram-api/types.ts +++ b/packages/mermaid/src/diagram-api/types.ts @@ -39,9 +39,26 @@ export interface DiagramDB { bindFunctions?: (element: Element) => void; } +// This is what is returned from getClasses(...) methods. +// It is slightly renamed to ..StyleClassDef instead of just ClassDef because "class" is a greatly ambiguous and overloaded word. +// It makes it clear we're working with a style class definition, even though defining the type is currently difficult. +export interface DiagramStyleClassDef { + id: string; + styles?: string[]; + textStyles?: string[]; +} + +export interface DiagramRenderer { + draw: DrawDefinition; + getClasses?: ( + text: string, + diagram: Pick + ) => Record; +} + export interface DiagramDefinition { db: DiagramDB; - renderer: any; + renderer: DiagramRenderer; parser: ParserDefinition; styles?: any; init?: (config: MermaidConfig) => void; @@ -84,7 +101,7 @@ export type DrawDefinition = ( id: string, version: string, diagramObject: Diagram -) => void; +) => void | Promise; export interface ParserDefinition { parse: (text: string) => void; diff --git a/packages/mermaid/src/diagram.spec.ts b/packages/mermaid/src/diagram.spec.ts index 99ce4e2c66..19a65b716b 100644 --- a/packages/mermaid/src/diagram.spec.ts +++ b/packages/mermaid/src/diagram.spec.ts @@ -34,7 +34,11 @@ describe('diagram detection', () => { yy: {}, }, }, - renderer: {}, + renderer: { + draw: () => { + // no-op + }, + }, styles: {}, }, }) diff --git a/packages/mermaid/src/diagrams/class/classRenderer-v2.ts b/packages/mermaid/src/diagrams/class/classRenderer-v2.ts index b581252bfa..5abfd769a2 100644 --- a/packages/mermaid/src/diagrams/class/classRenderer-v2.ts +++ b/packages/mermaid/src/diagrams/class/classRenderer-v2.ts @@ -8,7 +8,8 @@ import utils from '../../utils.js'; import { interpolateToCurve, getStylesFromArray } from '../../utils.js'; import { setupGraphViewbox } from '../../setupGraphViewbox.js'; import common from '../common/common.js'; -import type { ClassRelation, ClassNote, ClassMap, EdgeData, NamespaceMap } from './classTypes.js'; +import type { ClassRelation, ClassNote, ClassMap, NamespaceMap } from './classTypes.js'; +import type { EdgeData } from '../../types.js'; const sanitizeText = (txt: string) => common.sanitizeText(txt, getConfig()); diff --git a/packages/mermaid/src/diagrams/class/classTypes.ts b/packages/mermaid/src/diagrams/class/classTypes.ts index aa5ec7b70d..d372feebad 100644 --- a/packages/mermaid/src/diagrams/class/classTypes.ts +++ b/packages/mermaid/src/diagrams/class/classTypes.ts @@ -137,24 +137,6 @@ export interface ClassNote { text: string; } -export interface EdgeData { - arrowheadStyle?: string; - labelpos?: string; - labelType?: string; - label?: string; - classes: string; - pattern: string; - id: string; - arrowhead: string; - startLabelRight: string; - endLabelLeft: string; - arrowTypeStart: string; - arrowTypeEnd: string; - style: string; - labelStyle: string; - curve: any; -} - export type ClassRelation = { id1: string; id2: string; diff --git a/packages/mermaid/src/diagrams/flowchart/elk/flowRenderer-elk.js b/packages/mermaid/src/diagrams/flowchart/elk/flowRenderer-elk.js index c7bfdf5246..85ec80dc6b 100644 --- a/packages/mermaid/src/diagrams/flowchart/elk/flowRenderer-elk.js +++ b/packages/mermaid/src/diagrams/flowchart/elk/flowRenderer-elk.js @@ -4,13 +4,14 @@ import insertMarkers from '../../../dagre-wrapper/markers.js'; import { insertEdgeLabel } from '../../../dagre-wrapper/edges.js'; import { findCommonAncestor } from './render-utils.js'; import { labelHelper } from '../../../dagre-wrapper/shapes/util.js'; -import { addHtmlLabel } from 'dagre-d3-es/src/dagre-js/label/add-html-label.js'; import { getConfig } from '../../../config.js'; import { log } from '../../../logger.js'; import { setupGraphViewbox } from '../../../setupGraphViewbox.js'; -import common, { evaluate } from '../../common/common.js'; +import common from '../../common/common.js'; import { interpolateToCurve, getStylesFromArray } from '../../../utils.js'; import ELK from 'elkjs/lib/elk.bundled.js'; +import { getLineFunctionsWithOffset } from '../../../utils/lineWithOffset.js'; + const elk = new ELK(); let portPos = {}; @@ -651,7 +652,7 @@ const addMarkersToEdge = function (svgPath, edgeData, diagramType, arrowMarkerAb * * @param text * @param diagObj - * @returns {object} ClassDef styles + * @returns {Record} ClassDef styles */ export const getClasses = function (text, diagObj) { log.info('Extracting classes'); @@ -705,8 +706,8 @@ const insertEdge = function (edgesEl, edge, edgeData, diagObj, parentLookupDb) { [dest.x + offset.x, dest.y + offset.y], ]; - // const curve = line().curve(curveBasis); - const curve = line().curve(curveLinear); + const { x, y } = getLineFunctionsWithOffset(edge.edgeData); + const curve = line().x(x).y(y).curve(curveLinear); const edgePath = edgesEl .insert('path') .attr('d', curve(points)) diff --git a/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js b/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js index 4a3b7a8ce5..576ee6b34d 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js +++ b/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js @@ -338,7 +338,7 @@ export const addEdges = function (edges, g, diagObj) { * * @param text * @param diagObj - * @returns {object} ClassDef styles + * @returns {Record} ClassDef styles */ export const getClasses = function (text, diagObj) { return diagObj.db.getClasses(); diff --git a/packages/mermaid/src/diagrams/flowchart/flowRenderer.js b/packages/mermaid/src/diagrams/flowchart/flowRenderer.js index fc06cacd4d..8394b41e88 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowRenderer.js +++ b/packages/mermaid/src/diagrams/flowchart/flowRenderer.js @@ -269,7 +269,7 @@ export const addEdges = function (edges, g, diagObj) { * * @param text * @param diagObj - * @returns {object} ClassDef styles + * @returns {Record} ClassDef styles */ export const getClasses = function (text, diagObj) { log.info('Extracting classes'); diff --git a/packages/mermaid/src/diagrams/quadrant-chart/quadrantBuilder.ts b/packages/mermaid/src/diagrams/quadrant-chart/quadrantBuilder.ts index 9c11627620..5b740b0e0e 100644 --- a/packages/mermaid/src/diagrams/quadrant-chart/quadrantBuilder.ts +++ b/packages/mermaid/src/diagrams/quadrant-chart/quadrantBuilder.ts @@ -4,17 +4,13 @@ import { log } from '../../logger.js'; import type { BaseDiagramConfig, QuadrantChartConfig } from '../../config.type.js'; import defaultConfig from '../../defaultConfig.js'; import { getThemeVariables } from '../../themes/theme-default.js'; +import type { Point } from '../../types.js'; const defaultThemeVariables = getThemeVariables(); export type TextVerticalPos = 'left' | 'center' | 'right'; export type TextHorizontalPos = 'top' | 'middle' | 'bottom'; -export interface Point { - x: number; - y: number; -} - export interface QuadrantPointInputType extends Point { text: string; } diff --git a/packages/mermaid/src/diagrams/state/stateRenderer-v2.js b/packages/mermaid/src/diagrams/state/stateRenderer-v2.js index 1c9b2d1d3c..0d3117b206 100644 --- a/packages/mermaid/src/diagrams/state/stateRenderer-v2.js +++ b/packages/mermaid/src/diagrams/state/stateRenderer-v2.js @@ -81,7 +81,7 @@ export const setConf = function (cnf) { * * @param {string} text - the diagram text to be parsed * @param diagramObj - * @returns {object} ClassDef styles (a Map with keys = strings, values = ) + * @returns {Record} ClassDef styles (a Map with keys = strings, values = ) */ export const getClasses = function (text, diagramObj) { diagramObj.db.extract(diagramObj.db.getRootDocV2()); diff --git a/packages/mermaid/src/docs/package.json b/packages/mermaid/src/docs/package.json index 742a28c0a5..759d1ffb17 100644 --- a/packages/mermaid/src/docs/package.json +++ b/packages/mermaid/src/docs/package.json @@ -32,7 +32,7 @@ "unplugin-vue-components": "^0.25.0", "vite": "^4.3.9", "vite-plugin-pwa": "^0.16.0", - "vitepress": "1.0.0-rc.10", + "vitepress": "1.0.0-rc.12", "workbox-window": "^7.0.0" } } diff --git a/packages/mermaid/src/mermaid.spec.ts b/packages/mermaid/src/mermaid.spec.ts index 0b4437d742..645b5b39cb 100644 --- a/packages/mermaid/src/mermaid.spec.ts +++ b/packages/mermaid/src/mermaid.spec.ts @@ -95,8 +95,10 @@ describe('when using mermaid and ', () => { let loaded = false; const dummyDiagram: DiagramDefinition = { db: {}, - renderer: () => { - // do nothing + renderer: { + draw: () => { + // no-op + }, }, parser: { parse: (_text) => { diff --git a/packages/mermaid/src/mermaid.ts b/packages/mermaid/src/mermaid.ts index caf4a2b9b3..a6d4954714 100644 --- a/packages/mermaid/src/mermaid.ts +++ b/packages/mermaid/src/mermaid.ts @@ -136,7 +136,7 @@ const runThrowsErrors = async function ( } // generate the id of the diagram - const idGenerator = new utils.initIdGenerator(conf.deterministicIds, conf.deterministicIDSeed); + const idGenerator = new utils.InitIDGenerator(conf.deterministicIds, conf.deterministicIDSeed); let txt: string; const errors: DetailedError[] = []; diff --git a/packages/mermaid/src/mermaidAPI.spec.ts b/packages/mermaid/src/mermaidAPI.spec.ts index d7c16a1cfb..a79fd44c4a 100644 --- a/packages/mermaid/src/mermaidAPI.spec.ts +++ b/packages/mermaid/src/mermaidAPI.spec.ts @@ -287,15 +287,15 @@ describe('mermaidAPI', () => { }; it('gets the cssStyles from the theme', () => { - const styles = createCssStyles(mocked_config_with_htmlLabels, 'graphType', null); + const styles = createCssStyles(mocked_config_with_htmlLabels, null); expect(styles).toMatch(/^\ndefault(.*)/); }); it('gets the fontFamily from the config', () => { - const styles = createCssStyles(mocked_config_with_htmlLabels, 'graphType', {}); + const styles = createCssStyles(mocked_config_with_htmlLabels, {}); expect(styles).toMatch(/(.*)\n:root { --mermaid-font-family: serif(.*)/); }); it('gets the alt fontFamily from the config', () => { - const styles = createCssStyles(mocked_config_with_htmlLabels, 'graphType', undefined); + const styles = createCssStyles(mocked_config_with_htmlLabels, undefined); expect(styles).toMatch(/(.*)\n:root { --mermaid-alt-font-family: sans-serif(.*)/); }); @@ -306,8 +306,6 @@ describe('mermaidAPI', () => { const classDefs = { classDef1, classDef2, classDef3 }; describe('the graph supports classDefs', () => { - const graphType = 'flowchart-v2'; - const REGEXP_SPECIALS = ['^', '$', '?', '(', '{', '[', '.', '*', '!']; // prefix any special RegExp characters in the given string with a \ so we can use the literal character in a RegExp @@ -373,7 +371,7 @@ describe('mermaidAPI', () => { // @todo TODO Can't figure out how to spy on the cssImportantStyles method. // That would be a much better approach than manually checking the result - const styles = createCssStyles(mocked_config, graphType, classDefs); + const styles = createCssStyles(mocked_config, classDefs); htmlElements.forEach((htmlElement) => { expect_styles_matchesHtmlElements(styles, htmlElement); }); @@ -411,7 +409,7 @@ describe('mermaidAPI', () => { it('creates CSS styles for every style and textStyle in every classDef', () => { // TODO Can't figure out how to spy on the cssImportantStyles method. That would be a much better approach than manually checking the result. - const styles = createCssStyles(mocked_config_no_htmlLabels, graphType, classDefs); + const styles = createCssStyles(mocked_config_no_htmlLabels, classDefs); htmlElements.forEach((htmlElement) => { expect_styles_matchesHtmlElements(styles, htmlElement); }); diff --git a/packages/mermaid/src/mermaidAPI.ts b/packages/mermaid/src/mermaidAPI.ts index 3649b50f53..5250f0b190 100644 --- a/packages/mermaid/src/mermaidAPI.ts +++ b/packages/mermaid/src/mermaidAPI.ts @@ -28,17 +28,9 @@ import type { MermaidConfig } from './config.type.js'; import { evaluate } from './diagrams/common/common.js'; import isEmpty from 'lodash-es/isEmpty.js'; import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility.js'; +import type { DiagramStyleClassDef } from './diagram-api/types.js'; import { preprocessDiagram } from './preprocess.js'; -// diagram names that support classDef statements -const CLASSDEF_DIAGRAMS = [ - 'graph', - 'flowchart', - 'flowchart-v2', - 'flowchart-elk', - 'stateDiagram', - 'stateDiagram-v2', -]; const MAX_TEXTLENGTH = 50_000; const MAX_TEXTLENGTH_EXCEEDED_MSG = 'graph TB;a[Maximum text size in diagram exceeded];style a fill:#faa'; @@ -63,15 +55,6 @@ const IFRAME_NOT_SUPPORTED_MSG = 'The "iframe" tag is not supported by your brow const DOMPURIFY_TAGS = ['foreignobject']; const DOMPURIFY_ATTR = ['dominant-baseline']; -// This is what is returned from getClasses(...) methods. -// It is slightly renamed to ..StyleClassDef instead of just ClassDef because "class" is a greatly ambiguous and overloaded word. -// It makes it clear we're working with a style class definition, even though defining the type is currently difficult. -interface DiagramStyleClassDef { - id: string; - styles?: string[]; - textStyles?: string[]; -} - export interface ParseOptions { suppressErrors?: boolean; } @@ -184,15 +167,13 @@ export const cssImportantStyles = ( /** * Create the user styles - * + * @internal * @param config - configuration that has style and theme settings to use - * @param graphType - used for checking if classDefs should be applied * @param classDefs - the classDefs in the diagram text. Might be null if none were defined. Usually is the result of a call to getClasses(...) * @returns the string with all the user styles */ export const createCssStyles = ( config: MermaidConfig, - graphType: string, classDefs: Record | null | undefined = {} ): string => { let cssStyles = ''; @@ -212,7 +193,7 @@ export const createCssStyles = ( } // classDefs defined in the diagram text - if (!isEmpty(classDefs) && CLASSDEF_DIAGRAMS.includes(graphType)) { + if (!isEmpty(classDefs)) { const htmlLabels = config.htmlLabels || config.flowchart?.htmlLabels; // TODO why specifically check the Flowchart diagram config? const cssHtmlElements = ['> *', 'span']; // TODO make a constant @@ -241,10 +222,10 @@ export const createCssStyles = ( export const createUserStyles = ( config: MermaidConfig, graphType: string, - classDefs: Record, + classDefs: Record | undefined, svgId: string ): string => { - const userCSSstyles = createCssStyles(config, graphType, classDefs); + const userCSSstyles = createCssStyles(config, classDefs); const allStyles = getStyles(graphType, userCSSstyles, config.themeVariables); // Now turn all of the styles into a (compiled) string that starts with the id @@ -481,9 +462,7 @@ const render = async function ( // Insert an element into svg. This is where we put the styles const svg = element.firstChild; const firstChild = svg.firstChild; - const diagramClassDefs = CLASSDEF_DIAGRAMS.includes(diagramType) - ? diag.renderer.getClasses(text, diag) - : {}; + const diagramClassDefs = diag.renderer.getClasses?.(text, diag); const rules = createUserStyles(config, diagramType, diagramClassDefs, idSelector); diff --git a/packages/mermaid/src/types.ts b/packages/mermaid/src/types.ts new file mode 100644 index 0000000000..13da885033 --- /dev/null +++ b/packages/mermaid/src/types.ts @@ -0,0 +1,34 @@ +export interface Point { + x: number; + y: number; +} + +export interface TextDimensionConfig { + fontSize?: number; + fontWeight?: number; + fontFamily?: string; +} + +export interface TextDimensions { + width: number; + height: number; + lineHeight?: number; +} + +export interface EdgeData { + arrowheadStyle?: string; + labelpos?: string; + labelType?: string; + label?: string; + classes: string; + pattern: string; + id: string; + arrowhead: string; + startLabelRight: string; + endLabelLeft: string; + arrowTypeStart: string; + arrowTypeEnd: string; + style: string; + labelStyle: string; + curve: any; +} diff --git a/packages/mermaid/src/utils.spec.ts b/packages/mermaid/src/utils.spec.ts index e1398efc73..3be3bc2141 100644 --- a/packages/mermaid/src/utils.spec.ts +++ b/packages/mermaid/src/utils.spec.ts @@ -1,5 +1,5 @@ import { vi } from 'vitest'; -import utils, { cleanAndMerge, detectDirective } from './utils.js'; +import utils, { calculatePoint, cleanAndMerge, detectDirective } from './utils.js'; import assignWithDepth from './assignWithDepth.js'; import { detectType } from './diagram-api/detectType.js'; import { addDiagrams } from './diagram-api/diagram-orchestration.js'; @@ -352,7 +352,7 @@ describe('when initializing the id generator', function () { }); it('should return a random number generator based on Date', function () { - const idGenerator = new utils.initIdGenerator(false); + const idGenerator = new utils.InitIDGenerator(false); expect(typeof idGenerator.next).toEqual('function'); const lastId = idGenerator.next(); vi.advanceTimersByTime(1000); @@ -360,7 +360,7 @@ describe('when initializing the id generator', function () { }); it('should return a non random number generator', function () { - const idGenerator = new utils.initIdGenerator(true); + const idGenerator = new utils.InitIDGenerator(true); expect(typeof idGenerator.next).toEqual('function'); const start = 0; const lastId = idGenerator.next(); @@ -369,7 +369,7 @@ describe('when initializing the id generator', function () { }); it('should return a non random number generator based on seed', function () { - const idGenerator = new utils.initIdGenerator(true, 'thisIsASeed'); + const idGenerator = new utils.InitIDGenerator(true, 'thisIsASeed'); expect(typeof idGenerator.next).toEqual('function'); const start = 11; const lastId = idGenerator.next(); @@ -490,3 +490,107 @@ describe('cleanAndMerge', () => { expect(inputDeep).toEqual({ a: { b: 1 } }); }); }); + +describe('calculatePoint', () => { + it('should calculate a point on a straight line', () => { + const points = [ + { x: 0, y: 0 }, + { x: 0, y: 10 }, + { x: 0, y: 20 }, + ]; + expect(calculatePoint(points, 0)).toEqual({ x: 0, y: 0 }); + expect(calculatePoint(points, 5)).toEqual({ x: 0, y: 5 }); + expect(calculatePoint(points, 10)).toEqual({ x: 0, y: 10 }); + }); + + it('should calculate a point on a straight line with slope', () => { + const points = [ + { x: 0, y: 0 }, + { x: 10, y: 10 }, + { x: 20, y: 20 }, + ]; + expect(calculatePoint(points, 0)).toMatchInlineSnapshot(` + { + "x": 0, + "y": 0, + } + `); + expect(calculatePoint(points, 5)).toMatchInlineSnapshot(` + { + "x": 3.53553, + "y": 3.53553, + } + `); + expect(calculatePoint(points, 10)).toMatchInlineSnapshot(` + { + "x": 7.07107, + "y": 7.07107, + } + `); + }); + + it('should calculate a point on a straight line with negative slope', () => { + const points = [ + { x: 20, y: 20 }, + { x: 10, y: 10 }, + { x: 15, y: 15 }, + { x: 0, y: 0 }, + ]; + expect(calculatePoint(points, 0)).toMatchInlineSnapshot(` + { + "x": 20, + "y": 20, + } + `); + expect(calculatePoint(points, 5)).toMatchInlineSnapshot(` + { + "x": 16.46447, + "y": 16.46447, + } + `); + expect(calculatePoint(points, 10)).toMatchInlineSnapshot(` + { + "x": 12.92893, + "y": 12.92893, + } + `); + }); + + it('should calculate a point on a curved line', () => { + const points = [ + { x: 0, y: 0 }, + { x: 10, y: 10 }, + { x: 20, y: 0 }, + ]; + expect(calculatePoint(points, 0)).toMatchInlineSnapshot(` + { + "x": 0, + "y": 0, + } + `); + expect(calculatePoint(points, 15)).toMatchInlineSnapshot(` + { + "x": 10.6066, + "y": 9.3934, + } + `); + expect(calculatePoint(points, 20)).toMatchInlineSnapshot(` + { + "x": 14.14214, + "y": 5.85786, + } + `); + }); + + it('should throw an error if the new point cannot be found', () => { + const points = [ + { x: 0, y: 0 }, + { x: 10, y: 10 }, + { x: 20, y: 20 }, + ]; + const distanceToTraverse = 30; + expect(() => calculatePoint(points, distanceToTraverse)).toThrow( + 'Could not find a suitable point for the given distance' + ); + }); +}); diff --git a/packages/mermaid/src/utils.ts b/packages/mermaid/src/utils.ts index 70de197dad..e706ef1227 100644 --- a/packages/mermaid/src/utils.ts +++ b/packages/mermaid/src/utils.ts @@ -1,4 +1,3 @@ -// @ts-nocheck : TODO Fix ts errors import { sanitizeUrl } from '@braintree/sanitize-url'; import type { CurveFactory } from 'd3'; import { @@ -33,6 +32,8 @@ import type { MermaidConfig } from './config.type.js'; import memoize from 'lodash-es/memoize.js'; import merge from 'lodash-es/merge.js'; import { directiveRegex } from './diagram-api/regexes.js'; +import type { D3Element } from './mermaidAPI.js'; +import type { Point, TextDimensionConfig, TextDimensions } from './types.js'; export const ZERO_WIDTH_SPACE = '\u200b'; @@ -58,7 +59,7 @@ const d3CurveTypes = { curveStep: curveStep, curveStepAfter: curveStepAfter, curveStepBefore: curveStepBefore, -}; +} as const; const directiveWithoutOpen = /\s*(?:(\w+)(?=:):|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi; @@ -101,14 +102,14 @@ export const detectInit = function ( config?: MermaidConfig ): MermaidConfig | undefined { const inits = detectDirective(text, /(?:init\b)|(?:initialize\b)/); - let results = {}; + let results: MermaidConfig & { config?: unknown } = {}; if (Array.isArray(inits)) { const args = inits.map((init) => init.args); sanitizeDirective(args); results = assignWithDepth(results, [...args]); } else { - results = inits.args; + results = inits.args as MermaidConfig; } if (!results) { @@ -116,19 +117,24 @@ export const detectInit = function ( } let type = detectType(text, config); - ['config'].forEach((prop) => { - if (results[prop] !== undefined) { - if (type === 'flowchart-v2') { - type = 'flowchart'; - } - results[type] = results[prop]; - delete results[prop]; + + // Move the `config` value to appropriate diagram type value + const prop = 'config'; + if (results[prop] !== undefined) { + if (type === 'flowchart-v2') { + type = 'flowchart'; } - }); + results[type as keyof MermaidConfig] = results[prop]; + delete results[prop]; + } return results; }; +interface Directive { + type?: string; + args?: unknown; +} /** * Detects the directive from the text. * @@ -154,8 +160,8 @@ export const detectInit = function ( */ export const detectDirective = function ( text: string, - type: string | RegExp = null -): { type?: string; args?: any } | { type?: string; args?: any }[] { + type: string | RegExp | null = null +): Directive | Directive[] { try { const commentWithoutDirectives = new RegExp( `[%]{2}(?![{]${directiveWithoutOpen.source})(?=[}][%]{2}).*\n`, @@ -165,8 +171,8 @@ export const detectDirective = function ( log.debug( `Detecting diagram directive${type !== null ? ' type:' + type : ''} based on the text:${text}` ); - let match; - const result = []; + let match: RegExpExecArray | null; + const result: Directive[] = []; while ((match = directiveRegex.exec(text)) !== null) { // This is necessary to avoid infinite loops with zero-width matches if (match.index === directiveRegex.lastIndex) { @@ -183,16 +189,17 @@ export const detectDirective = function ( } } if (result.length === 0) { - result.push({ type: text, args: null }); + return { type: text, args: null }; } return result.length === 1 ? result[0] : result; } catch (error) { log.error( - `ERROR: ${error.message} - Unable to parse directive - ${type !== null ? ' type:' + type : ''} based on the text:${text}` + `ERROR: ${ + (error as Error).message + } - Unable to parse directive type: '${type}' based on the text: '${text}'` ); - return { type: null, args: null }; + return { type: undefined, args: null }; } }; @@ -231,7 +238,9 @@ export function interpolateToCurve( return defaultCurve; } const curveName = `curve${interpolate.charAt(0).toUpperCase() + interpolate.slice(1)}`; - return d3CurveTypes[curveName] || defaultCurve; + + // @ts-ignore TODO: Fix issue with curve type + return d3CurveTypes[curveName as keyof typeof d3CurveTypes] ?? defaultCurve; } /** @@ -244,13 +253,15 @@ export function interpolateToCurve( export function formatUrl(linkStr: string, config: MermaidConfig): string | undefined { const url = linkStr.trim(); - if (url) { - if (config.securityLevel !== 'loose') { - return sanitizeUrl(url); - } + if (!url) { + return undefined; + } - return url; + if (config.securityLevel !== 'loose') { + return sanitizeUrl(url); } + + return url; } /** @@ -259,7 +270,7 @@ export function formatUrl(linkStr: string, config: MermaidConfig): string | unde * @param functionName - A dot separated path to the function relative to the `window` * @param params - Parameters to pass to the function */ -export const runFunc = (functionName: string, ...params) => { +export const runFunc = (functionName: string, ...params: unknown[]) => { const arrPaths = functionName.split('.'); const len = arrPaths.length - 1; @@ -267,23 +278,16 @@ export const runFunc = (functionName: string, ...params) => { let obj = window; for (let i = 0; i < len; i++) { - obj = obj[arrPaths[i]]; + obj = obj[arrPaths[i] as keyof typeof obj]; if (!obj) { + log.error(`Function name: ${functionName} not found in window`); return; } } - obj[fnName](...params); + obj[fnName as keyof typeof obj](...params); }; -/** A (x, y) point */ -interface Point { - /** The x value */ - x: number; - /** The y value */ - y: number; -} - /** * Finds the distance between two points using the Distance Formula * @@ -291,8 +295,11 @@ interface Point { * @param p2 - The second point * @returns The distance between the two points. */ -function distance(p1: Point, p2: Point): number { - return p1 && p2 ? Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)) : 0; +function distance(p1?: Point, p2?: Point): number { + if (!p1 || !p2) { + return 0; + } + return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); } /** @@ -301,7 +308,7 @@ function distance(p1: Point, p2: Point): number { * @param points - List of points */ function traverseEdge(points: Point[]): Point { - let prevPoint; + let prevPoint: Point | undefined; let totalDistance = 0; points.forEach((point) => { @@ -310,35 +317,8 @@ function traverseEdge(points: Point[]): Point { }); // Traverse half of total distance along points - let remainingDistance = totalDistance / 2; - let center = undefined; - prevPoint = undefined; - points.forEach((point) => { - if (prevPoint && !center) { - const vectorDistance = distance(point, prevPoint); - if (vectorDistance < remainingDistance) { - remainingDistance -= vectorDistance; - } else { - // The point is remainingDistance from prevPoint in the vector between prevPoint and point - // Calculate the coordinates - const distanceRatio = remainingDistance / vectorDistance; - if (distanceRatio <= 0) { - center = prevPoint; - } - if (distanceRatio >= 1) { - center = { x: point.x, y: point.y }; - } - if (distanceRatio > 0 && distanceRatio < 1) { - center = { - x: (1 - distanceRatio) * prevPoint.x + distanceRatio * point.x, - y: (1 - distanceRatio) * prevPoint.y + distanceRatio * point.y, - }; - } - } - } - prevPoint = point; - }); - return center; + const remainingDistance = totalDistance / 2; + return calculatePoint(points, remainingDistance); } /** @@ -351,20 +331,16 @@ function calcLabelPosition(points: Point[]): Point { return traverseEdge(points); } -const calcCardinalityPosition = (isRelationTypePresent, points, initialPosition) => { - let prevPoint; - log.info(`our points ${JSON.stringify(points)}`); - if (points[0] !== initialPosition) { - points = points.reverse(); - } - // Traverse only 25 total distance along points to find cardinality point - const distanceToCardinalityPoint = 25; +export const roundNumber = (num: number, precision = 2) => { + const factor = Math.pow(10, precision); + return Math.round(num * factor) / factor; +}; - let remainingDistance = distanceToCardinalityPoint; - let center; - prevPoint = undefined; - points.forEach((point) => { - if (prevPoint && !center) { +export const calculatePoint = (points: Point[], distanceToTraverse: number): Point => { + let prevPoint: Point | undefined = undefined; + let remainingDistance = distanceToTraverse; + for (const point of points) { + if (prevPoint) { const vectorDistance = distance(point, prevPoint); if (vectorDistance < remainingDistance) { remainingDistance -= vectorDistance; @@ -373,27 +349,42 @@ const calcCardinalityPosition = (isRelationTypePresent, points, initialPosition) // Calculate the coordinates const distanceRatio = remainingDistance / vectorDistance; if (distanceRatio <= 0) { - center = prevPoint; + return prevPoint; } if (distanceRatio >= 1) { - center = { x: point.x, y: point.y }; + return { x: point.x, y: point.y }; } if (distanceRatio > 0 && distanceRatio < 1) { - center = { - x: (1 - distanceRatio) * prevPoint.x + distanceRatio * point.x, - y: (1 - distanceRatio) * prevPoint.y + distanceRatio * point.y, + return { + x: roundNumber((1 - distanceRatio) * prevPoint.x + distanceRatio * point.x, 5), + y: roundNumber((1 - distanceRatio) * prevPoint.y + distanceRatio * point.y, 5), }; } } } prevPoint = point; - }); + } + throw new Error('Could not find a suitable point for the given distance'); +}; + +const calcCardinalityPosition = ( + isRelationTypePresent: boolean, + points: Point[], + initialPosition: Point +) => { + log.info(`our points ${JSON.stringify(points)}`); + if (points[0] !== initialPosition) { + points = points.reverse(); + } + // Traverse only 25 total distance along points to find cardinality point + const distanceToCardinalityPoint = 25; + const center = calculatePoint(points, distanceToCardinalityPoint); // if relation is present (Arrows will be added), change cardinality point off-set distance (d) const d = isRelationTypePresent ? 10 : 5; //Calculate Angle for x and y axis const angle = Math.atan2(points[0].y - center.y, points[0].x - center.x); const cardinalityPosition = { x: 0, y: 0 }; - //Calculation cardinality position using angle, center point on the line/curve but pendicular and with offset-distance + //Calculation cardinality position using angle, center point on the line/curve but perpendicular and with offset-distance cardinalityPosition.x = Math.sin(angle) * d + (points[0].x + center.x) / 2; cardinalityPosition.y = -Math.cos(angle) * d + (points[0].y + center.y) / 2; return cardinalityPosition; @@ -412,71 +403,36 @@ function calcTerminalLabelPosition( position: 'start_left' | 'start_right' | 'end_left' | 'end_right', _points: Point[] ): Point { - // Todo looking to faster cloning method - let points = JSON.parse(JSON.stringify(_points)); - let prevPoint; + const points = structuredClone(_points); log.info('our points', points); if (position !== 'start_left' && position !== 'start_right') { - points = points.reverse(); + points.reverse(); } - points.forEach((point) => { - prevPoint = point; - }); - // Traverse only 25 total distance along points to find cardinality point const distanceToCardinalityPoint = 25 + terminalMarkerSize; + const center = calculatePoint(points, distanceToCardinalityPoint); - let remainingDistance = distanceToCardinalityPoint; - let center; - prevPoint = undefined; - points.forEach((point) => { - if (prevPoint && !center) { - const vectorDistance = distance(point, prevPoint); - if (vectorDistance < remainingDistance) { - remainingDistance -= vectorDistance; - } else { - // The point is remainingDistance from prevPoint in the vector between prevPoint and point - // Calculate the coordinates - const distanceRatio = remainingDistance / vectorDistance; - if (distanceRatio <= 0) { - center = prevPoint; - } - if (distanceRatio >= 1) { - center = { x: point.x, y: point.y }; - } - if (distanceRatio > 0 && distanceRatio < 1) { - center = { - x: (1 - distanceRatio) * prevPoint.x + distanceRatio * point.x, - y: (1 - distanceRatio) * prevPoint.y + distanceRatio * point.y, - }; - } - } - } - prevPoint = point; - }); // if relation is present (Arrows will be added), change cardinality point off-set distance (d) const d = 10 + terminalMarkerSize * 0.5; //Calculate Angle for x and y axis const angle = Math.atan2(points[0].y - center.y, points[0].x - center.x); - const cardinalityPosition = { x: 0, y: 0 }; - - //Calculation cardinality position using angle, center point on the line/curve but pendicular and with offset-distance + const cardinalityPosition: Point = { x: 0, y: 0 }; + //Calculation cardinality position using angle, center point on the line/curve but perpendicular and with offset-distance - cardinalityPosition.x = Math.sin(angle) * d + (points[0].x + center.x) / 2; - cardinalityPosition.y = -Math.cos(angle) * d + (points[0].y + center.y) / 2; if (position === 'start_left') { cardinalityPosition.x = Math.sin(angle + Math.PI) * d + (points[0].x + center.x) / 2; cardinalityPosition.y = -Math.cos(angle + Math.PI) * d + (points[0].y + center.y) / 2; - } - if (position === 'end_right') { + } else if (position === 'end_right') { cardinalityPosition.x = Math.sin(angle - Math.PI) * d + (points[0].x + center.x) / 2 - 5; cardinalityPosition.y = -Math.cos(angle - Math.PI) * d + (points[0].y + center.y) / 2 - 5; - } - if (position === 'end_left') { + } else if (position === 'end_left') { cardinalityPosition.x = Math.sin(angle) * d + (points[0].x + center.x) / 2 - 5; cardinalityPosition.y = -Math.cos(angle) * d + (points[0].y + center.y) / 2 - 5; + } else { + cardinalityPosition.x = Math.sin(angle) * d + (points[0].x + center.x) / 2; + cardinalityPosition.y = -Math.cos(angle) * d + (points[0].y + center.y) / 2; } return cardinalityPosition; } @@ -502,7 +458,7 @@ export function getStylesFromArray(arr: string[]): { style: string; labelStyle: } } - return { style: style, labelStyle: labelStyle }; + return { style, labelStyle }; } let cnt = 0; @@ -514,10 +470,10 @@ export const generateId = () => { /** * Generates a random hexadecimal id of the given length. * - * @param length - Length of ID. - * @returns The generated ID. + * @param length - Length of string. + * @returns The generated string. */ -function makeid(length: number): string { +function makeRandomHex(length: number): string { let result = ''; const characters = '0123456789abcdef'; const charactersLength = characters.length; @@ -527,8 +483,8 @@ function makeid(length: number): string { return result; } -export const random = (options) => { - return makeid(options.length); +export const random = (options: { length: number }) => { + return makeRandomHex(options.length); }; export const getTextObj = function () { @@ -544,6 +500,7 @@ export const getTextObj = function () { rx: 0, ry: 0, valign: undefined, + text: '', }; }; @@ -574,7 +531,7 @@ export const drawSimpleText = function ( const [, _fontSizePx] = parseFontSize(textData.fontSize); - const textElem = elem.append('text'); + const textElem = elem.append('text') as any; textElem.attr('x', textData.x); textElem.attr('y', textData.y); textElem.style('text-anchor', textData.anchor); @@ -582,6 +539,7 @@ export const drawSimpleText = function ( textElem.style('font-size', _fontSizePx); textElem.style('font-weight', textData.fontWeight); textElem.attr('fill', textData.fill); + if (textData.class !== undefined) { textElem.attr('class', textData.class); } @@ -601,9 +559,9 @@ interface WrapLabelConfig { joinWith: string; } -export const wrapLabel: (label: string, maxWidth: string, config: WrapLabelConfig) => string = +export const wrapLabel: (label: string, maxWidth: number, config: WrapLabelConfig) => string = memoize( - (label: string, maxWidth: string, config: WrapLabelConfig): string => { + (label: string, maxWidth: number, config: WrapLabelConfig): string => { if (!label) { return label; } @@ -615,7 +573,7 @@ export const wrapLabel: (label: string, maxWidth: string, config: WrapLabelConfi return label; } const words = label.split(' '); - const completedLines = []; + const completedLines: string[] = []; let nextLine = ''; words.forEach((word, index) => { const wordLength = calculateTextWidth(`${word} `, config); @@ -700,10 +658,6 @@ export function calculateTextHeight( text: Parameters[0], config: Parameters[1] ): ReturnType['height'] { - config = Object.assign( - { fontSize: 12, fontWeight: 400, fontFamily: 'Arial', margin: 15 }, - config - ); return calculateTextDimensions(text, config).height; } @@ -719,20 +673,9 @@ export function calculateTextWidth( text: Parameters[0], config: Parameters[1] ): ReturnType['width'] { - config = Object.assign({ fontSize: 12, fontWeight: 400, fontFamily: 'Arial' }, config); return calculateTextDimensions(text, config).width; } -interface TextDimensionConfig { - fontSize?: number; - fontWeight?: number; - fontFamily?: string; -} -interface TextDimensions { - width: number; - height: number; - lineHeight?: number; -} /** * This calculates the dimensions of the given text, font size, font family, font weight, and * margins. @@ -747,8 +690,7 @@ export const calculateTextDimensions: ( config: TextDimensionConfig ) => TextDimensions = memoize( (text: string, config: TextDimensionConfig): TextDimensions => { - config = Object.assign({ fontSize: 12, fontWeight: 400, fontFamily: 'Arial' }, config); - const { fontSize, fontFamily, fontWeight } = config; + const { fontSize = 12, fontFamily = 'Arial', fontWeight = 400 } = config; if (!text) { return { width: 0, height: 0 }; } @@ -772,12 +714,14 @@ export const calculateTextDimensions: ( const g = body.append('svg'); for (const fontFamily of fontFamilies) { - let cheight = 0; + let cHeight = 0; const dim = { width: 0, height: 0, lineHeight: 0 }; for (const line of lines) { const textObj = getTextObj(); textObj.text = line || ZERO_WIDTH_SPACE; + // @ts-ignore TODO: Fix D3 types const textElem = drawSimpleText(g, textObj) + // @ts-ignore TODO: Fix D3 types .style('font-size', _fontSizePx) .style('font-weight', fontWeight) .style('font-family', fontFamily); @@ -787,9 +731,9 @@ export const calculateTextDimensions: ( throw new Error('svg element not in render tree'); } dim.width = Math.round(Math.max(dim.width, bBox.width)); - cheight = Math.round(bBox.height); - dim.height += cheight; - dim.lineHeight = Math.round(Math.max(dim.lineHeight, cheight)); + cHeight = Math.round(bBox.height); + dim.height += cHeight; + dim.lineHeight = Math.round(Math.max(dim.lineHeight, cHeight)); } dims.push(dim); } @@ -810,25 +754,18 @@ export const calculateTextDimensions: ( (text, config) => `${text}${config.fontSize}${config.fontWeight}${config.fontFamily}` ); -export const initIdGenerator = class iterator { - constructor(deterministic, seed?: any) { - this.deterministic = deterministic; +export class InitIDGenerator { + private count = 0; + public next: () => number; + constructor(deterministic = false, seed?: string) { // TODO: Seed is only used for length? - this.seed = seed; - + // v11: Use the actual value of seed string to generate an initial value for count. this.count = seed ? seed.length : 0; + this.next = deterministic ? () => this.count++ : () => Date.now(); } +} - next() { - if (!this.deterministic) { - return Date.now(); - } - - return this.count++; - } -}; - -let decoder; +let decoder: HTMLDivElement; /** * Decodes HTML, source: {@link https://github.com/shrpne/entity-decode/blob/v2.0.1/browser.js} @@ -840,20 +777,23 @@ export const entityDecode = function (html: string): string { decoder = decoder || document.createElement('div'); // Escape HTML before decoding for HTML Entities html = escape(html).replace(/%26/g, '&').replace(/%23/g, '#').replace(/%3B/g, ';'); - // decoding decoder.innerHTML = html; - return unescape(decoder.textContent); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return unescape(decoder.textContent!); }; export interface DetailedError { str: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any hash: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any error?: any; message?: string; } /** @param error - The error to check */ -export function isDetailedError(error: unknown): error is DetailedError { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isDetailedError(error: any): error is DetailedError { return 'str' in error; } @@ -874,7 +814,7 @@ export function getErrorMessage(error: unknown): string { * @param title - The title. If empty, returns immediately. */ export const insertTitle = ( - parent, + parent: D3Element, cssClass: string, titleTopMargin: number, title?: string @@ -882,7 +822,10 @@ export const insertTitle = ( if (!title) { return; } - const bounds = parent.node().getBBox(); + const bounds = parent.node()?.getBBox(); + if (!bounds) { + return; + } parent .append('text') .text(title) @@ -905,7 +848,7 @@ export const parseFontSize = (fontSize: string | number | undefined): [number?, return [fontSize, fontSize + 'px']; } - const fontSizeNumber = parseInt(fontSize, 10); + const fontSizeNumber = parseInt(fontSize ?? '', 10); if (Number.isNaN(fontSizeNumber)) { // if a number value can't be parsed, return null for both values return [undefined, undefined]; @@ -941,7 +884,7 @@ export default { random, runFunc, entityDecode, - initIdGenerator, insertTitle, parseFontSize, + InitIDGenerator, }; diff --git a/packages/mermaid/src/utils/lineWithOffset.ts b/packages/mermaid/src/utils/lineWithOffset.ts new file mode 100644 index 0000000000..f348d3eb35 --- /dev/null +++ b/packages/mermaid/src/utils/lineWithOffset.ts @@ -0,0 +1,92 @@ +import type { EdgeData, Point } from '../types.js'; + +// We need to draw the lines a bit shorter to avoid drawing +// under any transparent markers. +// The offsets are calculated from the markers' dimensions. +const markerOffsets = { + aggregation: 18, + extension: 18, + composition: 18, + dependency: 6, + lollipop: 13.5, + arrow_point: 5.3, +} as const; + +/** + * Calculate the deltas and angle between two points + * @param point1 - First point + * @param point2 - Second point + * @returns The angle, deltaX and deltaY + */ +function calculateDeltaAndAngle( + point1: Point | [number, number], + point2: Point | [number, number] +): { angle: number; deltaX: number; deltaY: number } { + point1 = pointTransformer(point1); + point2 = pointTransformer(point2); + const [x1, y1] = [point1.x, point1.y]; + const [x2, y2] = [point2.x, point2.y]; + const deltaX = x2 - x1; + const deltaY = y2 - y1; + return { angle: Math.atan(deltaY / deltaX), deltaX, deltaY }; +} + +const pointTransformer = (data: Point | [number, number]) => { + if (Array.isArray(data)) { + return { x: data[0], y: data[1] }; + } + return data; +}; + +export const getLineFunctionsWithOffset = ( + edge: Pick +) => { + return { + x: function (d: Point | [number, number], i: number, data: (Point | [number, number])[]) { + let offset = 0; + if (i === 0 && Object.hasOwn(markerOffsets, edge.arrowTypeStart)) { + // Handle first point + // Calculate the angle and delta between the first two points + const { angle, deltaX } = calculateDeltaAndAngle(data[0], data[1]); + // Calculate the offset based on the angle and the marker's dimensions + offset = + markerOffsets[edge.arrowTypeStart as keyof typeof markerOffsets] * + Math.cos(angle) * + (deltaX >= 0 ? 1 : -1); + } else if (i === data.length - 1 && Object.hasOwn(markerOffsets, edge.arrowTypeEnd)) { + // Handle last point + // Calculate the angle and delta between the last two points + const { angle, deltaX } = calculateDeltaAndAngle( + data[data.length - 1], + data[data.length - 2] + ); + offset = + markerOffsets[edge.arrowTypeEnd as keyof typeof markerOffsets] * + Math.cos(angle) * + (deltaX >= 0 ? 1 : -1); + } + return pointTransformer(d).x + offset; + }, + y: function (d: Point | [number, number], i: number, data: (Point | [number, number])[]) { + // Same handling as X above + let offset = 0; + if (i === 0 && Object.hasOwn(markerOffsets, edge.arrowTypeStart)) { + const { angle, deltaY } = calculateDeltaAndAngle(data[0], data[1]); + offset = + markerOffsets[edge.arrowTypeStart as keyof typeof markerOffsets] * + Math.abs(Math.sin(angle)) * + (deltaY >= 0 ? 1 : -1); + } else if (i === data.length - 1 && Object.hasOwn(markerOffsets, edge.arrowTypeEnd)) { + const { angle, deltaY } = calculateDeltaAndAngle( + data[data.length - 1], + data[data.length - 2] + ); + offset = + markerOffsets[edge.arrowTypeEnd as keyof typeof markerOffsets] * + Math.abs(Math.sin(angle)) * + (deltaY >= 0 ? 1 : -1); + } + return pointTransformer(d).y + offset; + }, + }; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b86be8ff17..5a04bb353b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -475,8 +475,8 @@ importers: specifier: ^0.16.0 version: 0.16.0(vite@4.3.9)(workbox-build@7.0.0)(workbox-window@7.0.0) vitepress: - specifier: 1.0.0-rc.10 - version: 1.0.0-rc.10(@algolia/client-search@4.19.1)(@types/node@18.16.0)(search-insights@2.6.0) + specifier: 1.0.0-rc.12 + version: 1.0.0-rc.12(@algolia/client-search@4.19.1)(@types/node@18.16.0)(search-insights@2.6.0) workbox-window: specifier: ^7.0.0 version: 7.0.0 @@ -13913,6 +13913,15 @@ packages: vscode-textmate: 8.0.0 dev: true + /shiki@0.14.4: + resolution: {integrity: sha512-IXCRip2IQzKwxArNNq1S+On4KPML3Yyn8Zzs/xRgcgOWIr8ntIK3IKzjFPfjy/7kt9ZMjc+FItfqHRBg8b6tNQ==} + dependencies: + ansi-sequence-parser: 1.1.0 + jsonc-parser: 3.2.0 + vscode-oniguruma: 1.7.0 + vscode-textmate: 8.0.0 + dev: true + /side-channel@1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: @@ -15456,8 +15465,8 @@ packages: - terser dev: true - /vitepress@1.0.0-rc.10(@algolia/client-search@4.19.1)(@types/node@18.16.0)(search-insights@2.6.0): - resolution: {integrity: sha512-+MsahIWqq5WUEmj6MR4obcKYbT7im07jZPCQPdNJExkeOSbOAJ4xypSLx88x7rvtzWHhHc5aXbOhCRvGEGjFrw==} + /vitepress@1.0.0-rc.12(@algolia/client-search@4.19.1)(@types/node@18.16.0)(search-insights@2.6.0): + resolution: {integrity: sha512-mZknN5l9lgbBjXwumwdOQQDM+gPivswFEykEQeenY0tv7eocS+bb801IpFZT3mFV6YRhSddmbutHlFgPPADjEg==} hasBin: true dependencies: '@docsearch/css': 3.5.2 @@ -15468,7 +15477,7 @@ packages: focus-trap: 7.5.2 mark.js: 8.11.1 minisearch: 6.1.0 - shiki: 0.14.3 + shiki: 0.14.4 vite: 4.4.9(@types/node@18.16.0) vue: 3.3.4 transitivePeerDependencies: