From 38123d2f2175eed1403b47560e9fcd2498cd81ec Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Thu, 7 Sep 2023 12:01:22 +0530 Subject: [PATCH 01/14] fix: #4818 support `getClasses` in external diagrams. --- .../interfaces/mermaidAPI.ParseOptions.md | 2 +- .../interfaces/mermaidAPI.RenderResult.md | 4 +-- docs/config/setup/modules/mermaidAPI.md | 34 +++++++++---------- .../src/diagram-api/diagram-orchestration.ts | 6 +++- .../src/diagram-api/diagramAPI.spec.ts | 6 +++- packages/mermaid/src/diagram-api/types.ts | 21 ++++++++++-- packages/mermaid/src/diagram.spec.ts | 6 +++- .../flowchart/elk/flowRenderer-elk.js | 5 ++- .../src/diagrams/flowchart/flowRenderer-v2.js | 2 +- .../src/diagrams/flowchart/flowRenderer.js | 2 +- .../src/diagrams/state/stateRenderer-v2.js | 2 +- packages/mermaid/src/mermaid.spec.ts | 6 ++-- packages/mermaid/src/mermaidAPI.ts | 27 +++------------ 13 files changed, 67 insertions(+), 56 deletions(-) diff --git a/docs/config/setup/interfaces/mermaidAPI.ParseOptions.md b/docs/config/setup/interfaces/mermaidAPI.ParseOptions.md index 2082a081ea..f7e2c2ed7c 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:78](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L78) +[mermaidAPI.ts:61](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L61) diff --git a/docs/config/setup/interfaces/mermaidAPI.RenderResult.md b/docs/config/setup/interfaces/mermaidAPI.RenderResult.md index f84a51b87b..2f039b2eb9 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:98](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L98) +[mermaidAPI.ts:81](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L81) --- @@ -51,4 +51,4 @@ The svg code for the rendered graph. #### Defined in -[mermaidAPI.ts:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L88) +[mermaidAPI.ts:71](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L71) diff --git a/docs/config/setup/modules/mermaidAPI.md b/docs/config/setup/modules/mermaidAPI.md index d5d4a1cbc1..0d3501576f 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:82](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L82) +[mermaidAPI.ts:65](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L65) ## Variables @@ -96,7 +96,7 @@ mermaid.initialize(config); #### Defined in -[mermaidAPI.ts:673](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L673) +[mermaidAPI.ts:654](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L654) ## Functions @@ -127,7 +127,7 @@ Return the last node appended #### Defined in -[mermaidAPI.ts:310](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L310) +[mermaidAPI.ts:293](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L293) --- @@ -153,7 +153,7 @@ the cleaned up svgCode #### Defined in -[mermaidAPI.ts:256](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L256) +[mermaidAPI.ts:239](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L239) --- @@ -179,7 +179,7 @@ the string with all the user styles #### Defined in -[mermaidAPI.ts:185](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L185) +[mermaidAPI.ts:168](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L168) --- @@ -189,12 +189,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 +202,7 @@ the string with all the user styles #### Defined in -[mermaidAPI.ts:233](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L233) +[mermaidAPI.ts:216](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L216) --- @@ -229,7 +229,7 @@ with an enclosing block that has each of the cssClasses followed by !important; #### Defined in -[mermaidAPI.ts:169](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L169) +[mermaidAPI.ts:152](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L152) --- @@ -249,7 +249,7 @@ with an enclosing block that has each of the cssClasses followed by !important; #### Defined in -[mermaidAPI.ts:155](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L155) +[mermaidAPI.ts:138](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L138) --- @@ -269,7 +269,7 @@ with an enclosing block that has each of the cssClasses followed by !important; #### Defined in -[mermaidAPI.ts:126](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L126) +[mermaidAPI.ts:109](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L109) --- @@ -295,7 +295,7 @@ Put the svgCode into an iFrame. Return the iFrame code #### Defined in -[mermaidAPI.ts:287](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L287) +[mermaidAPI.ts:270](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L270) --- @@ -320,4 +320,4 @@ Remove any existing elements from the given document #### Defined in -[mermaidAPI.ts:360](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L360) +[mermaidAPI.ts:343](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L343) 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 2ac7fba127..45f75fb08e 100644 --- a/packages/mermaid/src/diagram-api/types.ts +++ b/packages/mermaid/src/diagram-api/types.ts @@ -32,9 +32,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; @@ -76,7 +93,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/flowchart/elk/flowRenderer-elk.js b/packages/mermaid/src/diagrams/flowchart/elk/flowRenderer-elk.js index c7bfdf5246..f1ad22f675 100644 --- a/packages/mermaid/src/diagrams/flowchart/elk/flowRenderer-elk.js +++ b/packages/mermaid/src/diagrams/flowchart/elk/flowRenderer-elk.js @@ -4,11 +4,10 @@ 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'; const elk = new ELK(); @@ -651,7 +650,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'); 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/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/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/mermaidAPI.ts b/packages/mermaid/src/mermaidAPI.ts index bb75700348..6ea27a809a 100644 --- a/packages/mermaid/src/mermaidAPI.ts +++ b/packages/mermaid/src/mermaidAPI.ts @@ -31,16 +31,8 @@ import isEmpty from 'lodash-es/isEmpty.js'; import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility.js'; import { parseDirective } from './directiveUtils.js'; import { extractFrontMatter } from './diagram-api/frontmatter.js'; +import type { DiagramStyleClassDef } from './diagram-api/types.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'; @@ -65,15 +57,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; } @@ -204,7 +187,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 @@ -233,7 +216,7 @@ export const createCssStyles = ( export const createUserStyles = ( config: MermaidConfig, graphType: string, - classDefs: Record, + classDefs: Record | undefined, svgId: string ): string => { const userCSSstyles = createCssStyles(config, graphType, classDefs); @@ -492,9 +475,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); From ef959196919da256662774111e8f81c8b526cca7 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Thu, 7 Sep 2023 12:40:56 +0530 Subject: [PATCH 02/14] refactor: Remove unused variables --- docs/config/setup/modules/mermaidAPI.md | 17 ++++++++--------- packages/mermaid/src/mermaidAPI.spec.ts | 12 +++++------- packages/mermaid/src/mermaidAPI.ts | 4 +--- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/docs/config/setup/modules/mermaidAPI.md b/docs/config/setup/modules/mermaidAPI.md index 0d3501576f..b24a849bd7 100644 --- a/docs/config/setup/modules/mermaidAPI.md +++ b/docs/config/setup/modules/mermaidAPI.md @@ -96,7 +96,7 @@ mermaid.initialize(config); #### Defined in -[mermaidAPI.ts:654](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L654) +[mermaidAPI.ts:652](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L652) ## Functions @@ -127,7 +127,7 @@ Return the last node appended #### Defined in -[mermaidAPI.ts:293](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L293) +[mermaidAPI.ts:291](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L291) --- @@ -153,13 +153,13 @@ the cleaned up svgCode #### Defined in -[mermaidAPI.ts:239](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L239) +[mermaidAPI.ts:237](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L237) --- ### 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:168](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L168) +[mermaidAPI.ts:167](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L167) --- @@ -202,7 +201,7 @@ the string with all the user styles #### Defined in -[mermaidAPI.ts:216](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L216) +[mermaidAPI.ts:214](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L214) --- @@ -295,7 +294,7 @@ Put the svgCode into an iFrame. Return the iFrame code #### Defined in -[mermaidAPI.ts:270](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L270) +[mermaidAPI.ts:268](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L268) --- @@ -320,4 +319,4 @@ Remove any existing elements from the given document #### Defined in -[mermaidAPI.ts:343](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L343) +[mermaidAPI.ts:341](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L341) 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 6ea27a809a..da884fde73 100644 --- a/packages/mermaid/src/mermaidAPI.ts +++ b/packages/mermaid/src/mermaidAPI.ts @@ -161,13 +161,11 @@ export const cssImportantStyles = ( * Create the user styles * * @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 = ''; @@ -219,7 +217,7 @@ export const createUserStyles = ( 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 From e75af86ef25c3af7ee54ba04f39d38be5c7beea1 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Thu, 7 Sep 2023 12:46:58 +0530 Subject: [PATCH 03/14] chore: Bump version --- packages/mermaid/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", From 6eb3337d20d3f1d91d1167cb74d10dd782a2b040 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Fri, 8 Sep 2023 08:20:03 +0530 Subject: [PATCH 04/14] chore: Add @internal to createCSSStyles --- packages/mermaid/src/mermaidAPI.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mermaid/src/mermaidAPI.ts b/packages/mermaid/src/mermaidAPI.ts index 12dea484b7..5250f0b190 100644 --- a/packages/mermaid/src/mermaidAPI.ts +++ b/packages/mermaid/src/mermaidAPI.ts @@ -167,7 +167,7 @@ export const cssImportantStyles = ( /** * Create the user styles - * + * @internal * @param config - configuration that has style and theme settings to use * @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 From 552d3ec2e1198f41fa8701ff2cc0a7d4dbe939bf Mon Sep 17 00:00:00 2001 From: Chad Fawcett Date: Fri, 8 Sep 2023 15:28:41 -0700 Subject: [PATCH 05/14] Give markers unique id's per graph --- packages/mermaid/src/dagre-wrapper/edges.js | 80 ++++++++++++++----- packages/mermaid/src/dagre-wrapper/index.js | 8 +- packages/mermaid/src/dagre-wrapper/markers.js | 50 ++++++------ 3 files changed, 90 insertions(+), 48 deletions(-) diff --git a/packages/mermaid/src/dagre-wrapper/edges.js b/packages/mermaid/src/dagre-wrapper/edges.js index f89b4422be..babb0ea6a9 100644 --- a/packages/mermaid/src/dagre-wrapper/edges.js +++ b/packages/mermaid/src/dagre-wrapper/edges.js @@ -382,7 +382,7 @@ function calculateDeltaAndAngle(point1, point2) { return { angle: Math.atan(deltaY / deltaX), deltaX, deltaY }; } -export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph) { +export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph, id) { let points = edge.points; let pointsHasChanged = false; const tail = graph.node(e.v); @@ -569,61 +569,103 @@ export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph switch (edge.arrowTypeStart) { case 'arrow_cross': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-crossStart' + ')'); + svgPath.attr( + 'marker-start', + 'url(' + url + '#' + id + '_' + diagramType + '-crossStart' + ')' + ); break; case 'arrow_point': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-pointStart' + ')'); + svgPath.attr( + 'marker-start', + 'url(' + url + '#' + id + '_' + diagramType + '-pointStart' + ')' + ); break; case 'arrow_barb': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-barbStart' + ')'); + svgPath.attr( + 'marker-start', + 'url(' + url + '#' + id + '_' + diagramType + '-barbStart' + ')' + ); break; case 'arrow_circle': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-circleStart' + ')'); + svgPath.attr( + 'marker-start', + 'url(' + url + '#' + id + '_' + diagramType + '-circleStart' + ')' + ); break; case 'aggregation': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-aggregationStart' + ')'); + svgPath.attr( + 'marker-start', + 'url(' + url + '#' + id + '_' + diagramType + '-aggregationStart' + ')' + ); break; case 'extension': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-extensionStart' + ')'); + svgPath.attr( + 'marker-start', + 'url(' + url + '#' + id + '_' + diagramType + '-extensionStart' + ')' + ); break; case 'composition': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-compositionStart' + ')'); + svgPath.attr( + 'marker-start', + 'url(' + url + '#' + id + '_' + diagramType + '-compositionStart' + ')' + ); break; case 'dependency': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-dependencyStart' + ')'); + svgPath.attr( + 'marker-start', + 'url(' + url + '#' + id + '_' + diagramType + '-dependencyStart' + ')' + ); break; case 'lollipop': - svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-lollipopStart' + ')'); + svgPath.attr( + 'marker-start', + 'url(' + url + '#' + id + '_' + diagramType + '-lollipopStart' + ')' + ); break; default: } switch (edge.arrowTypeEnd) { case 'arrow_cross': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-crossEnd' + ')'); + svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-crossEnd' + ')'); break; case 'arrow_point': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-pointEnd' + ')'); + svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-pointEnd' + ')'); break; case 'arrow_barb': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-barbEnd' + ')'); + svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-barbEnd' + ')'); break; case 'arrow_circle': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-circleEnd' + ')'); + svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-circleEnd' + ')'); break; case 'aggregation': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-aggregationEnd' + ')'); + svgPath.attr( + 'marker-end', + 'url(' + url + '#' + id + '_' + diagramType + '-aggregationEnd' + ')' + ); break; case 'extension': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-extensionEnd' + ')'); + svgPath.attr( + 'marker-end', + 'url(' + url + '#' + id + '_' + diagramType + '-extensionEnd' + ')' + ); break; case 'composition': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-compositionEnd' + ')'); + svgPath.attr( + 'marker-end', + 'url(' + url + '#' + id + '_' + diagramType + '-compositionEnd' + ')' + ); break; case 'dependency': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-dependencyEnd' + ')'); + svgPath.attr( + 'marker-end', + 'url(' + url + '#' + id + '_' + diagramType + '-dependencyEnd' + ')' + ); break; case 'lollipop': - svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-lollipopEnd' + ')'); + svgPath.attr( + 'marker-end', + 'url(' + url + '#' + id + '_' + diagramType + '-lollipopEnd' + ')' + ); break; default: } diff --git a/packages/mermaid/src/dagre-wrapper/index.js b/packages/mermaid/src/dagre-wrapper/index.js index 279c5d9ddd..9843adb8b0 100644 --- a/packages/mermaid/src/dagre-wrapper/index.js +++ b/packages/mermaid/src/dagre-wrapper/index.js @@ -14,7 +14,7 @@ import { insertCluster, clear as clearClusters } from './clusters.js'; import { insertEdgeLabel, positionEdgeLabel, insertEdge, clear as clearEdges } from './edges.js'; import { log } from '../logger.js'; -const recursiveRender = async (_elem, graph, diagramtype, parentCluster) => { +const recursiveRender = async (_elem, graph, diagramtype, id, parentCluster) => { log.info('Graph in recursive render: XXX', graphlibJson.write(graph), parentCluster); const dir = graph.graph().rankdir; log.trace('Dir in recursive render - dir:', dir); @@ -52,7 +52,7 @@ const recursiveRender = async (_elem, graph, diagramtype, parentCluster) => { if (node && node.clusterNode) { // const children = graph.children(v); log.info('Cluster identified', v, node.width, graph.node(v)); - const o = await recursiveRender(nodes, node.graph, diagramtype, graph.node(v)); + const o = await recursiveRender(nodes, node.graph, diagramtype, id, graph.node(v)); const newEl = o.elem; updateNodeBounds(node, newEl); node.diff = o.diff || 0; @@ -134,7 +134,7 @@ const recursiveRender = async (_elem, graph, diagramtype, parentCluster) => { const edge = graph.edge(e); log.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(edge), edge); - const paths = insertEdge(edgePaths, e, edge, clusterDb, diagramtype, graph); + const paths = insertEdge(edgePaths, e, edge, clusterDb, diagramtype, graph, id); positionEdgeLabel(edge, paths); }); @@ -159,7 +159,7 @@ export const render = async (elem, graph, markers, diagramtype, id) => { adjustClustersAndEdges(graph); log.warn('Graph after:', JSON.stringify(graphlibJson.write(graph))); // log.warn('Graph ever after:', graphlibJson.write(graph.node('A').graph)); - await recursiveRender(elem, graph, diagramtype); + await recursiveRender(elem, graph, diagramtype, id); }; // const shapeDefinitions = {}; diff --git a/packages/mermaid/src/dagre-wrapper/markers.js b/packages/mermaid/src/dagre-wrapper/markers.js index 051c987f62..a499b8f263 100644 --- a/packages/mermaid/src/dagre-wrapper/markers.js +++ b/packages/mermaid/src/dagre-wrapper/markers.js @@ -14,7 +14,7 @@ const extension = (elem, type, id) => { elem .append('defs') .append('marker') - .attr('id', type + '-extensionStart') + .attr('id', id + '_' + type + '-extensionStart') .attr('class', 'marker extension ' + type) .attr('refX', 18) .attr('refY', 7) @@ -27,7 +27,7 @@ const extension = (elem, type, id) => { elem .append('defs') .append('marker') - .attr('id', type + '-extensionEnd') + .attr('id', id + '_' + type + '-extensionEnd') .attr('class', 'marker extension ' + type) .attr('refX', 1) .attr('refY', 7) @@ -38,11 +38,11 @@ const extension = (elem, type, id) => { .attr('d', 'M 1,1 V 13 L18,7 Z'); // this is actual shape for arrowhead }; -const composition = (elem, type) => { +const composition = (elem, type, id) => { elem .append('defs') .append('marker') - .attr('id', type + '-compositionStart') + .attr('id', id + '_' + type + '-compositionStart') .attr('class', 'marker composition ' + type) .attr('refX', 18) .attr('refY', 7) @@ -55,7 +55,7 @@ const composition = (elem, type) => { elem .append('defs') .append('marker') - .attr('id', type + '-compositionEnd') + .attr('id', id + '_' + type + '-compositionEnd') .attr('class', 'marker composition ' + type) .attr('refX', 1) .attr('refY', 7) @@ -65,11 +65,11 @@ const composition = (elem, type) => { .append('path') .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z'); }; -const aggregation = (elem, type) => { +const aggregation = (elem, type, id) => { elem .append('defs') .append('marker') - .attr('id', type + '-aggregationStart') + .attr('id', id + '_' + type + '-aggregationStart') .attr('class', 'marker aggregation ' + type) .attr('refX', 18) .attr('refY', 7) @@ -82,7 +82,7 @@ const aggregation = (elem, type) => { elem .append('defs') .append('marker') - .attr('id', type + '-aggregationEnd') + .attr('id', id + '_' + type + '-aggregationEnd') .attr('class', 'marker aggregation ' + type) .attr('refX', 1) .attr('refY', 7) @@ -92,11 +92,11 @@ const aggregation = (elem, type) => { .append('path') .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z'); }; -const dependency = (elem, type) => { +const dependency = (elem, type, id) => { elem .append('defs') .append('marker') - .attr('id', type + '-dependencyStart') + .attr('id', id + '_' + type + '-dependencyStart') .attr('class', 'marker dependency ' + type) .attr('refX', 6) .attr('refY', 7) @@ -109,7 +109,7 @@ const dependency = (elem, type) => { elem .append('defs') .append('marker') - .attr('id', type + '-dependencyEnd') + .attr('id', id + '_' + type + '-dependencyEnd') .attr('class', 'marker dependency ' + type) .attr('refX', 13) .attr('refY', 7) @@ -119,11 +119,11 @@ const dependency = (elem, type) => { .append('path') .attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z'); }; -const lollipop = (elem, type) => { +const lollipop = (elem, type, id) => { elem .append('defs') .append('marker') - .attr('id', type + '-lollipopStart') + .attr('id', id + '_' + type + '-lollipopStart') .attr('class', 'marker lollipop ' + type) .attr('refX', 13) .attr('refY', 7) @@ -140,7 +140,7 @@ const lollipop = (elem, type) => { elem .append('defs') .append('marker') - .attr('id', type + '-lollipopEnd') + .attr('id', id + '_' + type + '-lollipopEnd') .attr('class', 'marker lollipop ' + type) .attr('refX', 1) .attr('refY', 7) @@ -154,10 +154,10 @@ const lollipop = (elem, type) => { .attr('cy', 7) .attr('r', 6); }; -const point = (elem, type) => { +const point = (elem, type, id) => { elem .append('marker') - .attr('id', type + '-pointEnd') + .attr('id', id + '_' + type + '-pointEnd') .attr('class', 'marker ' + type) .attr('viewBox', '0 0 10 10') .attr('refX', 6) @@ -173,7 +173,7 @@ const point = (elem, type) => { .style('stroke-dasharray', '1,0'); elem .append('marker') - .attr('id', type + '-pointStart') + .attr('id', id + '_' + type + '-pointStart') .attr('class', 'marker ' + type) .attr('viewBox', '0 0 10 10') .attr('refX', 0) @@ -188,10 +188,10 @@ const point = (elem, type) => { .style('stroke-width', 1) .style('stroke-dasharray', '1,0'); }; -const circle = (elem, type) => { +const circle = (elem, type, id) => { elem .append('marker') - .attr('id', type + '-circleEnd') + .attr('id', id + '_' + type + '-circleEnd') .attr('class', 'marker ' + type) .attr('viewBox', '0 0 10 10') .attr('refX', 11) @@ -210,7 +210,7 @@ const circle = (elem, type) => { elem .append('marker') - .attr('id', type + '-circleStart') + .attr('id', id + '_' + type + '-circleStart') .attr('class', 'marker ' + type) .attr('viewBox', '0 0 10 10') .attr('refX', -1) @@ -227,10 +227,10 @@ const circle = (elem, type) => { .style('stroke-width', 1) .style('stroke-dasharray', '1,0'); }; -const cross = (elem, type) => { +const cross = (elem, type, id) => { elem .append('marker') - .attr('id', type + '-crossEnd') + .attr('id', id + '_' + type + '-crossEnd') .attr('class', 'marker cross ' + type) .attr('viewBox', '0 0 11 11') .attr('refX', 12) @@ -248,7 +248,7 @@ const cross = (elem, type) => { elem .append('marker') - .attr('id', type + '-crossStart') + .attr('id', id + '_' + type + '-crossStart') .attr('class', 'marker cross ' + type) .attr('viewBox', '0 0 11 11') .attr('refX', -1) @@ -264,11 +264,11 @@ const cross = (elem, type) => { .style('stroke-width', 2) .style('stroke-dasharray', '1,0'); }; -const barb = (elem, type) => { +const barb = (elem, type, id) => { elem .append('defs') .append('marker') - .attr('id', type + '-barbEnd') + .attr('id', id + '_' + type + '-barbEnd') .attr('refX', 19) .attr('refY', 7) .attr('markerWidth', 20) From f3e0d5a20ab98eccab1751b9f9c8a5724b65ba0c Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Sat, 9 Sep 2023 11:15:55 +0530 Subject: [PATCH 06/14] refactor: Fix typings in utils.ts --- cSpell.json | 1 + package.json | 1 + .../quadrant-chart/quadrantBuilder.ts | 6 +- packages/mermaid/src/mermaid.ts | 2 +- packages/mermaid/src/types.ts | 16 + packages/mermaid/src/utils.spec.ts | 112 ++++++- packages/mermaid/src/utils.ts | 317 +++++++----------- 7 files changed, 258 insertions(+), 197 deletions(-) create mode 100644 packages/mermaid/src/types.ts 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/package.json b/package.json index 232f23be1d..bc081f36ed 100644 --- a/package.json +++ b/package.json @@ -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/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/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/types.ts b/packages/mermaid/src/types.ts new file mode 100644 index 0000000000..4b9eedad6c --- /dev/null +++ b/packages/mermaid/src/types.ts @@ -0,0 +1,16 @@ +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; +} 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, }; From 11aaee043fea35dcfb1703c006dcfde7e310ff10 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 13:44:53 +0000 Subject: [PATCH 07/14] chore(deps): update all patch dependencies --- package.json | 2 +- packages/mermaid/src/docs/package.json | 2 +- pnpm-lock.yaml | 19 ++++++++++++++----- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 232f23be1d..b0031f4daf 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", 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/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: From 924c9e913b665f22f9a3401dbf31857579833ca4 Mon Sep 17 00:00:00 2001 From: Chad Fawcett Date: Mon, 11 Sep 2023 11:59:28 -0700 Subject: [PATCH 08/14] Added cypress test --- .../rendering/marker_unique_id.spec.js | 10 +++++++ cypress/platform/marker_unique_id.html | 28 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 cypress/integration/rendering/marker_unique_id.spec.js create mode 100644 cypress/platform/marker_unique_id.html diff --git a/cypress/integration/rendering/marker_unique_id.spec.js b/cypress/integration/rendering/marker_unique_id.spec.js new file mode 100644 index 0000000000..617189db05 --- /dev/null +++ b/cypress/integration/rendering/marker_unique_id.spec.js @@ -0,0 +1,10 @@ +import { urlSnapshotTest } from '../../helpers/util.ts'; + +describe('Marker Unique IDs Per Diagram', () => { + it('should render a blue arrow tip in second digram', () => { + urlSnapshotTest('http://localhost:9000/marker_unique_id.html', { + logLevel: 1, + flowchart: { htmlLabels: false }, + }); + }); +}); diff --git a/cypress/platform/marker_unique_id.html b/cypress/platform/marker_unique_id.html new file mode 100644 index 0000000000..eff04dbb69 --- /dev/null +++ b/cypress/platform/marker_unique_id.html @@ -0,0 +1,28 @@ + + + +

Example

+
+      %%{init:{"theme":"base", "themeVariables": {"lineColor":"red"}}}%%
+      flowchart LR
+      subgraph red
+      A --> B
+      end
+    
+
+      %%{init:{"theme":"base", "themeVariables": {"lineColor":"blue"}}}%%
+      flowchart LR
+      subgraph black
+      A --> B
+      end
+    
+ + + From 6b7a0e1d8e0e34f9628f223ccc4967cb9cf43931 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Tue, 12 Sep 2023 14:04:54 +0530 Subject: [PATCH 09/14] fix: PointStart marker refX --- packages/mermaid/src/dagre-wrapper/markers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mermaid/src/dagre-wrapper/markers.js b/packages/mermaid/src/dagre-wrapper/markers.js index 051c987f62..223e682540 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) => { .attr('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) From 78346943a2abc88b37071ecc64afa5c274169f52 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Tue, 12 Sep 2023 14:06:53 +0530 Subject: [PATCH 10/14] refactor: Move EdgeData to types --- .../src/diagrams/class/classRenderer-v2.ts | 3 ++- .../mermaid/src/diagrams/class/classTypes.ts | 18 ------------------ packages/mermaid/src/types.ts | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+), 19 deletions(-) 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/types.ts b/packages/mermaid/src/types.ts index 4b9eedad6c..13da885033 100644 --- a/packages/mermaid/src/types.ts +++ b/packages/mermaid/src/types.ts @@ -14,3 +14,21 @@ export interface TextDimensions { 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; +} From 3c34fbaacdc0bd5bc09c21378d46c0962d496ecf Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Tue, 12 Sep 2023 14:08:02 +0530 Subject: [PATCH 11/14] refactor: Add getLineFunctionsWithOffset function --- packages/mermaid/src/dagre-wrapper/edges.js | 67 +------------- .../flowchart/elk/flowRenderer-elk.js | 9 +- packages/mermaid/src/utils/lineWithOffset.ts | 90 +++++++++++++++++++ 3 files changed, 98 insertions(+), 68 deletions(-) create mode 100644 packages/mermaid/src/utils/lineWithOffset.ts diff --git a/packages/mermaid/src/dagre-wrapper/edges.js b/packages/mermaid/src/dagre-wrapper/edges.js index f89b4422be..a47d5f4741 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) { 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/diagrams/flowchart/elk/flowRenderer-elk.js b/packages/mermaid/src/diagrams/flowchart/elk/flowRenderer-elk.js index c7bfdf5246..2f576025f6 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 = {}; @@ -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/utils/lineWithOffset.ts b/packages/mermaid/src/utils/lineWithOffset.ts new file mode 100644 index 0000000000..9b31767835 --- /dev/null +++ b/packages/mermaid/src/utils/lineWithOffset.ts @@ -0,0 +1,90 @@ +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: EdgeData) => { + 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; + }, + }; +}; From bceae92d3021498543f6eb90aff0c5755b0f3117 Mon Sep 17 00:00:00 2001 From: Chad Fawcett Date: Tue, 12 Sep 2023 10:29:39 -0700 Subject: [PATCH 12/14] Update cypress/platform/marker_unique_id.html Co-authored-by: Sidharth Vinod --- cypress/platform/marker_unique_id.html | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/cypress/platform/marker_unique_id.html b/cypress/platform/marker_unique_id.html index eff04dbb69..e49169c556 100644 --- a/cypress/platform/marker_unique_id.html +++ b/cypress/platform/marker_unique_id.html @@ -16,6 +16,30 @@

Example

A --> B end +
+      ---
+      config:
+        theme: base
+        themeVariables:
+          lineColor: yellow
+      ---
+      flowchart LR
+      subgraph red
+      A --> B
+      end
+    
+
+      ---
+      config:
+        theme: base
+        themeVariables:
+          lineColor: green
+      ---
+      flowchart LR
+      subgraph black
+      A --> B
+      end
+