Merge branch 'develop' into pr/rhysd/4359

* develop: (37 commits)
  fix: Arrow markers in flowchart-elk
  Bump version
  chore: Fix type in 'getLineFunctionsWithOffset'
  Update cypress/platform/marker_unique_id.html
  refactor: Add getLineFunctionsWithOffset function
  refactor: Move EdgeData to types
  fix: PointStart marker refX
  Added cypress test
  chore(deps): update all patch dependencies
  refactor: Fix typings in utils.ts
  Give markers unique id's per graph
  chore: Add @internal to createCSSStyles
  chore: Bump version
  refactor: Remove unused variables
  fix: #4818 support `getClasses` in external diagrams.
  Remove unnecessary tests
  Remove optional chaining
  chore: Update docs
  refactor: Use `||` instead of `??`
  Update flowchart.md (#4798)
  ...
This commit is contained in:
Sidharth Vinod 2023-09-14 23:33:39 +05:30
commit 0dbebe953b
No known key found for this signature in database
GPG Key ID: FB5CCD378D3907CD
93 changed files with 1270 additions and 1586 deletions

View File

@ -165,13 +165,7 @@ class Class10 {
int id
size()
}
namespace Namespace01 {
class Class11
class Class12 {
int id
size()
}
}
```
```mermaid
@ -191,13 +185,7 @@ class Class10 {
int id
size()
}
namespace Namespace01 {
class Class11
class Class12 {
int id
size()
}
}
```
### State diagram [<a href="https://mermaid-js.github.io/mermaid/#/stateDiagram">docs</a> - <a href="https://mermaid.live/edit#pako:eNpdkEFvgzAMhf8K8nEqpYSNthx22Xbcqcexg0sCiZQQlDhIFeK_L8A6TfXp6fOz9ewJGssFVOAJSbwr7ByadGR1n8T6evpO0vQ1uZDSekOrXGFsPqJPO6q-2-imH8f_0TeHXm50lfelsAMjnEHFY6xpMdRAUhhRQxUlFy0GTTXU_RytYeAx-AdXZB1ULWovdoCB7OXWN1CRC-Ju-r3uz6UtchGHJqDbsPygU57iysb2reoWHpyOWBINvsqypb3vFMlw3TfWZF5xiY7keC6zkpUnZIUojwW-FAVvrvn51LLnvOXHQ84Q5nn-AVtLcwk">live editor</a>]

View File

@ -22,6 +22,7 @@
"brkt",
"brolin",
"brotli",
"catmull",
"città",
"classdef",
"codedoc",

View File

@ -52,29 +52,21 @@ export const imgSnapshotTest = (
api = false,
validation?: any
): void => {
cy.log(JSON.stringify(_options));
const options: CypressMermaidConfig = Object.assign(_options);
if (!options.fontFamily) {
options.fontFamily = 'courier';
}
if (!options.sequence) {
options.sequence = {};
}
if (!options.sequence || (options.sequence && !options.sequence.actorFontFamily)) {
options.sequence.actorFontFamily = 'courier';
}
if (options.sequence && !options.sequence.noteFontFamily) {
options.sequence.noteFontFamily = 'courier';
}
options.sequence.actorFontFamily = 'courier';
options.sequence.noteFontFamily = 'courier';
options.sequence.messageFontFamily = 'courier';
if (options.sequence && !options.sequence.actorFontFamily) {
options.sequence.actorFontFamily = 'courier';
}
if (!options.fontSize) {
options.fontSize = 16;
}
const options: CypressMermaidConfig = {
..._options,
fontFamily: _options.fontFamily || 'courier',
// @ts-ignore TODO: Fix type of fontSize
fontSize: _options.fontSize || '16px',
sequence: {
...(_options.sequence || {}),
actorFontFamily: 'courier',
noteFontFamily:
_options.sequence && _options.sequence.noteFontFamily
? _options.sequence.noteFontFamily
: 'courier',
messageFontFamily: 'courier',
},
};
const url: string = mermaidUrl(graphStr, options, api);
openURLAndVerifyRendering(url, options, validation);
@ -82,11 +74,10 @@ export const imgSnapshotTest = (
export const urlSnapshotTest = (
url: string,
_options: CypressMermaidConfig,
options: CypressMermaidConfig,
_api = false,
validation?: any
): void => {
const options: CypressMermaidConfig = Object.assign(_options);
openURLAndVerifyRendering(url, options, validation);
};

View File

@ -330,6 +330,48 @@ describe('Gantt diagram', () => {
);
});
it('should render a gantt diagram with tick is 2 milliseconds', () => {
imgSnapshotTest(
`
gantt
title A Gantt Diagram
dateFormat SSS
axisFormat %Lms
tickInterval 2millisecond
excludes weekends
section Section
A task : a1, 000, 6ms
Another task : after a1, 6ms
section Another
Task in sec : a2, 006, 3ms
another task : 3ms
`,
{}
);
});
it('should render a gantt diagram with tick is 2 seconds', () => {
imgSnapshotTest(
`
gantt
title A Gantt Diagram
dateFormat ss
axisFormat %Ss
tickInterval 2second
excludes weekends
section Section
A task : a1, 00, 6s
Another task : after a1, 6s
section Another
Task in sec : 06, 3s
another task : 3s
`,
{}
);
});
it('should render a gantt diagram with tick is 15 minutes', () => {
imgSnapshotTest(
`

View File

@ -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 },
});
});
});

View File

@ -0,0 +1,52 @@
<html>
<head> </head>
<body>
<h1>Example</h1>
<pre class="mermaid">
%%{init:{"theme":"base", "themeVariables": {"lineColor":"red"}}}%%
flowchart LR
subgraph red
A --> B
end
</pre>
<pre class="mermaid">
%%{init:{"theme":"base", "themeVariables": {"lineColor":"blue"}}}%%
flowchart LR
subgraph black
A --> B
end
</pre>
<pre class="mermaid">
---
config:
theme: base
themeVariables:
lineColor: yellow
---
flowchart LR
subgraph red
A --> B
end
</pre>
<pre class="mermaid">
---
config:
theme: base
themeVariables:
lineColor: green
---
flowchart LR
subgraph black
A --> B
end
</pre>
<script type="module">
import mermaid from './mermaid.esm.mjs';
mermaid.initialize({ startOnLoad: true, logLevel: 0 });
if (window.Cypress) {
window.rendered = true;
}
</script>
</body>
</html>

View File

@ -62,10 +62,10 @@ from IPython.display import Image, display
import matplotlib.pyplot as plt
def mm(graph):
graphbytes = graph.encode("ascii")
base64_bytes = base64.b64encode(graphbytes)
base64_string = base64_bytes.decode("ascii")
display(Image(url="https://mermaid.ink/img/" + base64_string))
graphbytes = graph.encode("utf8")
base64_bytes = base64.b64encode(graphbytes)
base64_string = base64_bytes.decode("ascii")
display(Image(url="https://mermaid.ink/img/" + base64_string))
mm("""
graph LR;

View File

@ -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:59](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L59)

View File

@ -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: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:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L88)
[mermaidAPI.ts:69](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L69)

View File

@ -25,13 +25,13 @@ 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:63](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L63)
## Variables
### mermaidAPI
`Const` **mermaidAPI**: `Readonly`<{ `defaultConfig`: `MermaidConfig` = configApi.defaultConfig; `getConfig`: () => `MermaidConfig` = configApi.getConfig; `getDiagramFromText`: (`text`: `string`) => `Promise`<`Diagram`> ; `getSiteConfig`: () => `MermaidConfig` = configApi.getSiteConfig; `globalReset`: () => `void` ; `initialize`: (`options`: `MermaidConfig`) => `void` ; `parse`: (`text`: `string`, `parseOptions?`: [`ParseOptions`](../interfaces/mermaidAPI.ParseOptions.md)) => `Promise`<`boolean`> ; `parseDirective`: (`p`: `any`, `statement`: `string`, `context`: `string`, `type`: `string`) => `void` ; `render`: (`id`: `string`, `text`: `string`, `svgContainingElement?`: `Element`) => `Promise`<[`RenderResult`](../interfaces/mermaidAPI.RenderResult.md)> ; `reset`: () => `void` ; `setConfig`: (`conf`: `MermaidConfig`) => `MermaidConfig` = configApi.setConfig; `updateSiteConfig`: (`conf`: `MermaidConfig`) => `MermaidConfig` = configApi.updateSiteConfig }>
`Const` **mermaidAPI**: `Readonly`<{ `defaultConfig`: `MermaidConfig` = configApi.defaultConfig; `getConfig`: () => `MermaidConfig` = configApi.getConfig; `getDiagramFromText`: (`text`: `string`, `metadata`: `Pick`<`DiagramMetadata`, `"title"`>) => `Promise`<`Diagram`> ; `getSiteConfig`: () => `MermaidConfig` = configApi.getSiteConfig; `globalReset`: () => `void` ; `initialize`: (`options`: `MermaidConfig`) => `void` ; `parse`: (`text`: `string`, `parseOptions?`: [`ParseOptions`](../interfaces/mermaidAPI.ParseOptions.md)) => `Promise`<`boolean`> ; `render`: (`id`: `string`, `text`: `string`, `svgContainingElement?`: `Element`) => `Promise`<[`RenderResult`](../interfaces/mermaidAPI.RenderResult.md)> ; `reset`: () => `void` ; `setConfig`: (`conf`: `MermaidConfig`) => `MermaidConfig` = configApi.setConfig; `updateSiteConfig`: (`conf`: `MermaidConfig`) => `MermaidConfig` = configApi.updateSiteConfig }>
## mermaidAPI configuration defaults
@ -97,7 +97,7 @@ mermaid.initialize(config);
#### Defined in
[mermaidAPI.ts:685](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L685)
[mermaidAPI.ts:653](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L653)
## Functions
@ -128,7 +128,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:299](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L299)
---
@ -154,13 +154,13 @@ 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: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
@ -169,7 +169,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
@ -180,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:175](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L175)
---
@ -190,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
@ -203,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:222](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L222)
---
@ -230,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:160](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L160)
---
@ -250,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:146](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L146)
---
@ -270,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:117](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L117)
---
@ -296,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:276](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L276)
---
@ -321,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:349](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L349)

View File

@ -860,8 +860,8 @@ flowchart LR
C-->D
click A callback "Tooltip for a callback"
click B "https://www.github.com" "This is a tooltip for a link"
click A call callback() "Tooltip for a callback"
click B href "https://www.github.com" "This is a tooltip for a link"
click C call callback() "Tooltip for a callback"
click D href "https://www.github.com" "This is a tooltip for a link"
```
```mermaid
@ -871,13 +871,13 @@ flowchart LR
C-->D
click A callback "Tooltip for a callback"
click B "https://www.github.com" "This is a tooltip for a link"
click A call callback() "Tooltip for a callback"
click B href "https://www.github.com" "This is a tooltip for a link"
click C call callback() "Tooltip for a callback"
click D href "https://www.github.com" "This is a tooltip for a link"
```
> **Success** The tooltip functionality and the ability to link to urls are available from version 0.5.2.
?> Due to limitations with how Docsify handles JavaScript callback functions, an alternate working demo for the above code can be viewed at [this jsfiddle](https://jsfiddle.net/s37cjoau/3/).
?> Due to limitations with how Docsify handles JavaScript callback functions, an alternate working demo for the above code can be viewed at [this jsfiddle](https://jsfiddle.net/Ogglas/2o73vdez/7).
Links are opened in the same browser tab/window by default. It is possible to change this by adding a link target to the click definition (`_self`, `_blank`, `_parent` and `_top` are supported):

View File

@ -241,7 +241,7 @@ The following formatting strings are supported:
More info in: <https://github.com/d3/d3-time-format/tree/v4.0.0#locale_format>
### Axis ticks
### Axis ticks (v10.3.0+)
The default output ticks are auto. You can custom your `tickInterval`, like `1day` or `1week`.
@ -252,7 +252,7 @@ tickInterval 1day
The pattern is:
```javascript
/^([1-9][0-9]*)(minute|hour|day|week|month)$/;
/^([1-9][0-9]*)(millisecond|second|minute|hour|day|week|month)$/;
```
More info in: <https://github.com/d3/d3-time#interval_every>
@ -271,7 +271,7 @@ gantt
weekday monday
```
Support: v10.3.0+
> **Warning** > `millisecond` and `second` support was added in vMERMAID_RELEASE_VERSION
## Output in compact mode

View File

@ -8,9 +8,8 @@
> A sankey diagram is a visualization used to depict a flow from one set of values to another.
::: warning
This is an experimental diagram. Its syntax are very close to plain CSV, but it is to be extended in the nearest future.
:::
> **Warning**
> This is an experimental diagram. Its syntax are very close to plain CSV, but it is to be extended in the nearest future.
The things being connected are called nodes and the connections are called links.
@ -19,6 +18,11 @@ The things being connected are called nodes and the connections are called links
This example taken from [observable](https://observablehq.com/@d3/sankey/2?collection=@d3/d3-sankey). It may be rendered a little bit differently, though, in terms of size and colors.
```mermaid-example
---
config:
sankey:
showValues: false
---
sankey-beta
Agricultural 'waste',Bio-conversion,124.729
@ -92,6 +96,11 @@ Wind,Electricity grid,289.366
```
```mermaid
---
config:
sankey:
showValues: false
---
sankey-beta
Agricultural 'waste',Bio-conversion,124.729

View File

@ -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\"",

View File

@ -1,6 +1,6 @@
{
"name": "mermaid",
"version": "10.4.0",
"version": "10.5.0-rc.1",
"description": "Markdown-ish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.",
"type": "module",
"module": "./dist/mermaid.core.mjs",

View File

@ -2,11 +2,9 @@ import * as configApi from './config.js';
import { log } from './logger.js';
import { getDiagram, registerDiagram } from './diagram-api/diagramAPI.js';
import { detectType, getDiagramLoader } from './diagram-api/detectType.js';
import { extractFrontMatter } from './diagram-api/frontmatter.js';
import { UnknownDiagramError } from './errors.js';
import { cleanupComments } from './diagram-api/comments.js';
import type { DetailedError } from './utils.js';
import type { DiagramDefinition } from './diagram-api/types.js';
import type { DiagramDefinition, DiagramMetadata } from './diagram-api/types.js';
export type ParseErrorFunction = (err: string | DetailedError | unknown, hash?: any) => void;
@ -22,7 +20,7 @@ export class Diagram {
private init?: DiagramDefinition['init'];
private detectError?: UnknownDiagramError;
constructor(public text: string) {
constructor(public text: string, public metadata: Pick<DiagramMetadata, 'title'> = {}) {
this.text += '\n';
const cnf = configApi.getConfig();
try {
@ -37,19 +35,6 @@ export class Diagram {
this.db = diagram.db;
this.renderer = diagram.renderer;
this.parser = diagram.parser;
const originalParse = this.parser.parse.bind(this.parser);
// Wrap the jison parse() method to handle extracting frontmatter.
//
// This can't be done in this.parse() because some code
// directly calls diagram.parser.parse(), bypassing this.parse().
//
// Similarly, we can't do this in getDiagramFromText() because some code
// calls diagram.db.clear(), which would reset anything set by
// extractFrontMatter().
this.parser.parse = (text: string) =>
originalParse(cleanupComments(extractFrontMatter(text, this.db, configApi.addDirective)));
this.parser.parser.yy = this.db;
this.init = diagram.init;
this.parse();
@ -60,7 +45,12 @@ export class Diagram {
throw this.detectError;
}
this.db.clear?.();
this.init?.(configApi.getConfig());
const config = configApi.getConfig();
this.init?.(config);
// This block was added for legacy compatibility. Use frontmatter instead of adding more special cases.
if (this.metadata.title) {
this.db.setDiagramTitle?.(this.metadata.title);
}
this.parser.parse(this.text);
}
@ -82,11 +72,15 @@ export class Diagram {
* **Warning:** This function may be changed in the future.
* @alpha
* @param text - The mermaid diagram definition.
* @param metadata - Diagram metadata, defined in YAML.
* @returns A the Promise of a Diagram object.
* @throws {@link UnknownDiagramError} if the diagram type can not be found.
* @privateRemarks This is exported as part of the public mermaidAPI.
*/
export const getDiagramFromText = async (text: string): Promise<Diagram> => {
export const getDiagramFromText = async (
text: string,
metadata: Pick<DiagramMetadata, 'title'> = {}
): Promise<Diagram> => {
const type = detectType(text, configApi.getConfig());
try {
// Trying to find the diagram
@ -101,5 +95,5 @@ export const getDiagramFromText = async (text: string): Promise<Diagram> => {
const { id, diagram } = await loader();
registerDiagram(id, diagram);
}
return new Diagram(text);
return new Diagram(text, metadata);
};

View File

@ -13,7 +13,6 @@ export const mermaidAPI = {
svg: '<svg></svg>',
}),
parse: mAPI.parse,
parseDirective: vi.fn(),
initialize: vi.fn(),
getConfig: configApi.getConfig,
setConfig: configApi.setConfig,

View File

@ -3,7 +3,7 @@ import { log } from './logger.js';
import theme from './themes/index.js';
import config from './defaultConfig.js';
import type { MermaidConfig } from './config.type.js';
import { sanitizeDirective } from './utils.js';
import { sanitizeDirective } from './utils/sanitizeDirective.js';
export const defaultConfig: MermaidConfig = Object.freeze(config);

View File

@ -1054,7 +1054,7 @@ export interface GanttDiagramConfig extends BaseDiagramConfig {
* Pattern is:
*
* ```javascript
* /^([1-9][0-9]*)(minute|hour|day|week|month)$/
* /^([1-9][0-9]*)(millisecond|second|minute|hour|day|week|month)$/
* ```
*
*/

View File

@ -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,21 +369,7 @@ 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) {
export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph, id) {
let points = edge.points;
let pointsHasChanged = false;
const tail = graph.node(e.v);
@ -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;
@ -569,61 +508,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:
}

View File

@ -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 = {};

View File

@ -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,10 +173,10 @@ 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)
.attr('refX', 4.5)
.attr('refY', 5)
.attr('markerUnits', 'userSpaceOnUse')
.attr('markerWidth', 12)
@ -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)

View File

@ -4,5 +4,5 @@
* @returns cleaned text
*/
export const cleanupComments = (text: string): string => {
return text.trimStart().replace(/^\s*%%(?!{)[^\n]+\n?/gm, '');
return text.replace(/^\s*%%(?!{)[^\n]+\n?/gm, '').trimStart();
};

View File

@ -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: () => {

View File

@ -41,7 +41,11 @@ describe('DiagramAPI', () => {
},
parser: { yy: {} },
},
renderer: {},
renderer: {
draw: () => {
// no-op
},
},
styles: {},
},
detector

View File

@ -6,7 +6,6 @@ import { setupGraphViewbox as _setupGraphViewbox } from '../setupGraphViewbox.js
import { addStylesForDiagram } from '../styles.js';
import type { DiagramDefinition, DiagramDetector } from './types.js';
import * as _commonDb from '../diagrams/common/commonDb.js';
import { parseDirective as _parseDirective } from '../directiveUtils.js';
/*
Packaging and exposing resources for external diagrams so that they can import
@ -21,8 +20,6 @@ export const setupGraphViewbox = _setupGraphViewbox;
export const getCommonDb = () => {
return _commonDb;
};
export const parseDirective = (p: any, statement: string, context: string, type: string) =>
_parseDirective(p, statement, context, type);
const diagrams: Record<string, DiagramDefinition> = {};
export interface Detectors {
@ -52,17 +49,18 @@ export const registerDiagram = (
}
addStylesForDiagram(id, diagram.styles);
if (diagram.injectUtils) {
diagram.injectUtils(
log,
setLogLevel,
getConfig,
sanitizeText,
setupGraphViewbox,
getCommonDb(),
parseDirective
);
}
diagram.injectUtils?.(
log,
setLogLevel,
getConfig,
sanitizeText,
setupGraphViewbox,
getCommonDb(),
() => {
// parseDirective is removed in https://github.com/mermaid-js/mermaid/pull/4759.
// This is a no-op for legacy support.
}
);
};
export const getDiagram = (name: string): DiagramDefinition => {

View File

@ -1,84 +1,139 @@
import { vi } from 'vitest';
import { extractFrontMatter } from './frontmatter.js';
const dbMock = () => ({ setDiagramTitle: vi.fn() });
const setConfigMock = vi.fn();
describe('extractFrontmatter', () => {
beforeEach(() => {
setConfigMock.mockClear();
});
it('returns text unchanged if no frontmatter', () => {
expect(extractFrontMatter('diagram', dbMock())).toEqual('diagram');
expect(extractFrontMatter('diagram')).toMatchInlineSnapshot(`
{
"metadata": {},
"text": "diagram",
}
`);
});
it('returns text unchanged if frontmatter lacks closing delimiter', () => {
const text = `---\ntitle: foo\ndiagram`;
expect(extractFrontMatter(text, dbMock())).toEqual(text);
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
{
"metadata": {},
"text": "---
title: foo
diagram",
}
`);
});
it('handles empty frontmatter', () => {
const db = dbMock();
const text = `---\n\n---\ndiagram`;
expect(extractFrontMatter(text, db)).toEqual('diagram');
expect(db.setDiagramTitle).not.toHaveBeenCalled();
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
{
"metadata": {},
"text": "diagram",
}
`);
});
it('handles frontmatter without mappings', () => {
const db = dbMock();
const text = `---\n1\n---\ndiagram`;
expect(extractFrontMatter(text, db)).toEqual('diagram');
expect(db.setDiagramTitle).not.toHaveBeenCalled();
expect(extractFrontMatter(`---\n1\n---\ndiagram`)).toMatchInlineSnapshot(`
{
"metadata": {},
"text": "diagram",
}
`);
expect(extractFrontMatter(`---\n-1\n-2\n---\ndiagram`)).toMatchInlineSnapshot(`
{
"metadata": {},
"text": "diagram",
}
`);
expect(extractFrontMatter(`---\nnull\n---\ndiagram`)).toMatchInlineSnapshot(`
{
"metadata": {},
"text": "diagram",
}
`);
});
it('does not try to parse frontmatter at the end', () => {
const db = dbMock();
const text = `diagram\n---\ntitle: foo\n---\n`;
expect(extractFrontMatter(text, db)).toEqual(text);
expect(db.setDiagramTitle).not.toHaveBeenCalled();
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
{
"metadata": {},
"text": "diagram
---
title: foo
---
",
}
`);
});
it('handles frontmatter with multiple delimiters', () => {
const db = dbMock();
const text = `---\ntitle: foo---bar\n---\ndiagram\n---\ntest`;
expect(extractFrontMatter(text, db)).toEqual('diagram\n---\ntest');
expect(db.setDiagramTitle).toHaveBeenCalledWith('foo---bar');
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
{
"metadata": {
"title": "foo---bar",
},
"text": "diagram
---
test",
}
`);
});
it('handles frontmatter with multi-line string and multiple delimiters', () => {
const db = dbMock();
const text = `---\ntitle: |\n multi-line string\n ---\n---\ndiagram`;
expect(extractFrontMatter(text, db)).toEqual('diagram');
expect(db.setDiagramTitle).toHaveBeenCalledWith('multi-line string\n---\n');
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
{
"metadata": {
"title": "multi-line string
---
",
},
"text": "diagram",
}
`);
});
it('handles frontmatter with title', () => {
const db = dbMock();
const text = `---\ntitle: foo\n---\ndiagram`;
expect(extractFrontMatter(text, db)).toEqual('diagram');
expect(db.setDiagramTitle).toHaveBeenCalledWith('foo');
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
{
"metadata": {
"title": "foo",
},
"text": "diagram",
}
`);
});
it('handles booleans in frontmatter properly', () => {
const db = dbMock();
const text = `---\ntitle: true\n---\ndiagram`;
expect(extractFrontMatter(text, db)).toEqual('diagram');
expect(db.setDiagramTitle).toHaveBeenCalledWith('true');
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
{
"metadata": {
"title": "true",
},
"text": "diagram",
}
`);
});
it('ignores unspecified frontmatter keys', () => {
const db = dbMock();
const text = `---\ninvalid: true\ntitle: foo\ntest: bar\n---\ndiagram`;
expect(extractFrontMatter(text, db)).toEqual('diagram');
expect(db.setDiagramTitle).toHaveBeenCalledWith('foo');
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
{
"metadata": {
"title": "foo",
},
"text": "diagram",
}
`);
});
it('throws exception for invalid YAML syntax', () => {
const text = `---\n!!!\n---\ndiagram`;
expect(() => extractFrontMatter(text, dbMock())).toThrow(
'tag suffix cannot contain exclamation marks'
);
expect(() => extractFrontMatter(text)).toThrow('tag suffix cannot contain exclamation marks');
});
it('handles frontmatter with config', () => {
@ -92,9 +147,25 @@ config:
array: [1, 2, 3]
---
diagram`;
expect(extractFrontMatter(text, {}, setConfigMock)).toEqual('diagram');
expect(setConfigMock).toHaveBeenCalledWith({
graph: { string: 'hello', number: 14, boolean: false, array: [1, 2, 3] },
});
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
{
"metadata": {
"config": {
"graph": {
"array": [
1,
2,
3,
],
"boolean": false,
"number": 14,
"string": "hello",
},
},
"title": "hello",
},
"text": "diagram",
}
`);
});
});

View File

@ -1,6 +1,5 @@
import type { MermaidConfig } from '../config.type.js';
import { frontMatterRegex } from './regexes.js';
import type { DiagramDB } from './types.js';
// The "* as yaml" part is necessary for tree-shaking
import * as yaml from 'js-yaml';
@ -11,43 +10,51 @@ interface FrontMatterMetadata {
config?: MermaidConfig;
}
export interface FrontMatterResult {
text: string;
metadata: FrontMatterMetadata;
}
/**
* Extract and parse frontmatter from text, if present, and sets appropriate
* properties in the provided db.
* @param text - The text that may have a YAML frontmatter.
* @param db - Diagram database, could be of any diagram.
* @param setDiagramConfig - Optional function to set diagram config.
* @returns text with frontmatter stripped out
*/
export function extractFrontMatter(
text: string,
db: DiagramDB,
setDiagramConfig?: (config: MermaidConfig) => void
): string {
export function extractFrontMatter(text: string): FrontMatterResult {
const matches = text.match(frontMatterRegex);
if (!matches) {
return text;
return {
text,
metadata: {},
};
}
const parsed: FrontMatterMetadata = yaml.load(matches[1], {
// To support config, we need JSON schema.
// https://www.yaml.org/spec/1.2/spec.html#id2803231
schema: yaml.JSON_SCHEMA,
}) as FrontMatterMetadata;
let parsed: FrontMatterMetadata =
yaml.load(matches[1], {
// To support config, we need JSON schema.
// https://www.yaml.org/spec/1.2/spec.html#id2803231
schema: yaml.JSON_SCHEMA,
}) ?? {};
if (parsed?.title) {
// toString() is necessary because YAML could parse the title as a number/boolean
db.setDiagramTitle?.(parsed.title.toString());
// To handle runtime data type changes
parsed = typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
const metadata: FrontMatterMetadata = {};
// Only add properties that are explicitly supported, if they exist
if (parsed.displayMode) {
metadata.displayMode = parsed.displayMode.toString();
}
if (parsed.title) {
metadata.title = parsed.title.toString();
}
if (parsed.config) {
metadata.config = parsed.config;
}
if (parsed?.displayMode) {
// toString() is necessary because YAML could parse the title as a number/boolean
db.setDisplayMode?.(parsed.displayMode.toString());
}
if (parsed?.config) {
setDiagramConfig?.(parsed.config);
}
return text.slice(matches[0].length);
return {
text: text.slice(matches[0].length),
metadata,
};
}

View File

@ -1,7 +1,13 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Diagram } from '../Diagram.js';
import type { BaseDiagramConfig, MermaidConfig } from '../config.type.js';
import type * as d3 from 'd3';
export interface DiagramMetadata {
title?: string;
config?: MermaidConfig;
}
export interface InjectUtils {
_log: any;
_setLogLevel: any;
@ -9,6 +15,7 @@ export interface InjectUtils {
_sanitizeText: any;
_setupGraphViewbox: any;
_commonDb: any;
/** @deprecated as directives will be pre-processed since https://github.com/mermaid-js/mermaid/pull/4759 */
_parseDirective: any;
}
@ -32,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<DiagramDefinition, 'db'>
) => Record<string, DiagramStyleClassDef>;
}
export interface DiagramDefinition {
db: DiagramDB;
renderer: any;
renderer: DiagramRenderer;
parser: ParserDefinition;
styles?: any;
init?: (config: MermaidConfig) => void;
@ -45,6 +69,7 @@ export interface DiagramDefinition {
_sanitizeText: InjectUtils['_sanitizeText'],
_setupGraphViewbox: InjectUtils['_setupGraphViewbox'],
_commonDb: InjectUtils['_commonDb'],
/** @deprecated as directives will be pre-processed since https://github.com/mermaid-js/mermaid/pull/4759 */
_parseDirective: InjectUtils['_parseDirective']
) => void;
}
@ -76,22 +101,13 @@ export type DrawDefinition = (
id: string,
version: string,
diagramObject: Diagram
) => void;
) => void | Promise<void>;
export interface ParserDefinition {
parse: (text: string) => void;
parser: { yy: DiagramDB };
}
/**
* Type for function parse directive from diagram code.
*
* @param statement -
* @param context -
* @param type -
*/
export type ParseDirectiveDefinition = (statement: string, context: string, type: string) => void;
export type HTML = d3.Selection<HTMLIFrameElement, unknown, Element | null, unknown>;
export type SVG = d3.Selection<SVGSVGElement, unknown, Element | null, unknown>;

View File

@ -34,7 +34,11 @@ describe('diagram detection', () => {
yy: {},
},
},
renderer: {},
renderer: {
draw: () => {
// no-op
},
},
styles: {},
},
})

View File

@ -1,4 +1,3 @@
import mermaidAPI from '../../mermaidAPI.js';
import * as configApi from '../../config.js';
import { sanitizeText } from '../common/common.js';
import {
@ -38,10 +37,6 @@ export const setC4Type = function (c4TypeParam) {
c4Type = sanitizedText;
};
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
//type, from, to, label, ?techn, ?descr, ?sprite, ?tags, $link
export const addRel = function (type, from, to, label, techn, descr, sprite, tags, link) {
// Don't allow label nulling
@ -821,7 +816,6 @@ export default {
getAccTitle,
getAccDescription,
setAccDescription,
parseDirective,
getConfig: () => configApi.getConfig().c4,
clear,
LINETYPE,

View File

@ -1,17 +1,18 @@
// @ts-ignore: JISON doesn't support types
import c4Parser from './parser/c4Diagram.jison';
import c4Db from './c4Db.js';
import c4Renderer from './c4Renderer.js';
import c4Styles from './styles.js';
import parser from './parser/c4Diagram.jison';
import db from './c4Db.js';
import renderer from './c4Renderer.js';
import styles from './styles.js';
import type { MermaidConfig } from '../../config.type.js';
import type { DiagramDefinition } from '../../diagram-api/types.js';
export const diagram: DiagramDefinition = {
parser: c4Parser,
db: c4Db,
renderer: c4Renderer,
styles: c4Styles,
init: (cnf: MermaidConfig) => {
c4Renderer.setConf(cnf.c4);
parser,
db,
renderer,
styles,
init: ({ c4, wrap }: MermaidConfig) => {
renderer.setConf(c4);
db.setWrap(wrap);
},
};

View File

@ -72,25 +72,16 @@
%x string_kv_key
%x string_kv_value
%x open_directive
%x type_directive
%x arg_directive
%x close_directive
%x acc_title
%x acc_descr
%x acc_descr_multiline
%%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
.*direction\s+TB[^\n]* return 'direction_tb';
.*direction\s+BT[^\n]* return 'direction_bt';
.*direction\s+RL[^\n]* return 'direction_rl';
.*direction\s+LR[^\n]* return 'direction_lr';
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
"title"\s[^#\n;]+ return 'title';
@ -207,7 +198,6 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
start
: mermaidDoc
| direction
| directive start
;
direction
@ -225,26 +215,6 @@ mermaidDoc
: graphConfig
;
directive
: openDirective typeDirective closeDirective NEWLINE
| openDirective typeDirective ':' argDirective closeDirective NEWLINE
;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
;
typeDirective
: type_directive { }
;
argDirective
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
;
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'c4Context'); }
;
graphConfig
: C4_CONTEXT NEWLINE statements EOF {yy.setC4Type($1)}

View File

@ -4,7 +4,6 @@ import { log } from '../../logger.js';
import * as configApi from '../../config.js';
import common from '../common/common.js';
import utils from '../../utils.js';
import mermaidAPI from '../../mermaidAPI.js';
import {
setAccTitle,
getAccTitle,
@ -37,11 +36,6 @@ let functions: any[] = [];
const sanitizeText = (txt: string) => common.sanitizeText(txt, configApi.getConfig());
export const parseDirective = function (statement: string, context: string, type: string) {
// @ts-ignore Don't wanna mess it up
mermaidAPI.parseDirective(this, statement, context, type);
};
const splitClassNameAndType = function (id: string) {
let genericType = '';
let className = id;
@ -460,7 +454,6 @@ export const addClassesToNamespace = function (id: string, classNames: string[])
};
export default {
parseDirective,
setAccTitle,
getAccTitle,
getAccDescription,

View File

@ -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());

View File

@ -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;

View File

@ -13,9 +13,6 @@
%x href
%x callback_name
%x callback_args
%x open_directive
%x type_directive
%x arg_directive
%x acc_title
%x acc_descr
%x acc_descr_multiline
@ -24,15 +21,10 @@
%x namespace
%x namespace-body
%%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
.*direction\s+TB[^\n]* return 'direction_tb';
.*direction\s+BT[^\n]* return 'direction_bt';
.*direction\s+RL[^\n]* return 'direction_rl';
.*direction\s+LR[^\n]* return 'direction_lr';
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
\%\%(?!\{)*[^\n]*(\r?\n?)+ /* skip comments */
\%\%[^\n]*(\r?\n)* /* skip comments */
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
@ -220,7 +212,6 @@ line was introduced with 'click'.
start
: mermaidDoc
| directive start
| statements
;
@ -232,27 +223,6 @@ graphConfig
: CLASS_DIAGRAM NEWLINE statements EOF
;
directive
: openDirective typeDirective closeDirective NEWLINE
| openDirective typeDirective ':' argDirective closeDirective NEWLINE
;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
;
typeDirective
: type_directive { yy.parseDirective($1, 'type_directive'); }
;
argDirective
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
;
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'class'); }
;
statements
: statement
| statement NEWLINE

View File

@ -1,5 +1,4 @@
import { log } from '../../logger.js';
import mermaidAPI from '../../mermaidAPI.js';
import * as configApi from '../../config.js';
import {
@ -28,10 +27,6 @@ const Identification = {
IDENTIFYING: 'IDENTIFYING',
};
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
const addEntity = function (name, alias = undefined) {
if (entities[name] === undefined) {
entities[name] = { attributes: [], alias: alias };
@ -88,7 +83,6 @@ const clear = function () {
export default {
Cardinality,
Identification,
parseDirective,
getConfig: () => configApi.getConfig().er,
addEntity,
addAttributes,

View File

@ -1,7 +1,7 @@
%lex
%options case-insensitive
%x open_directive type_directive arg_directive block
%x block
%x acc_title
%x acc_descr
%x acc_descr_multiline
@ -14,11 +14,6 @@ accDescr\s*":"\s* { this.begin("ac
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
<acc_descr_multiline>[\}] { this.popState(); }
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
[\n]+ return 'NEWLINE';
\s+ /* skip whitespace */
[\s]+ return 'SPACE';
@ -77,7 +72,6 @@ o\{ return 'ZERO_OR_MORE';
start
: 'ER_DIAGRAM' document 'EOF' { /*console.log('finished parsing');*/ }
| directive start
;
document
@ -92,14 +86,9 @@ line
| EOF { $$=[];}
;
directive
: openDirective typeDirective closeDirective 'NEWLINE'
| openDirective typeDirective ':' argDirective closeDirective 'NEWLINE'
;
statement
: directive
| entityName relSpec entityName ':' role
: entityName relSpec entityName ':' role
{
yy.addEntity($1);
yy.addEntity($3);
@ -191,20 +180,4 @@ role
| 'ALPHANUM' { $$ = $1; }
;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
;
typeDirective
: type_directive { yy.parseDirective($1, 'type_directive'); }
;
argDirective
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
;
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'er'); }
;
%%

View File

@ -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 = {};
@ -568,8 +569,9 @@ export const addEdges = function (edges, diagObj, graph, svg) {
* @param edgeData
* @param diagramType
* @param arrowMarkerAbsolute
* @param id
*/
const addMarkersToEdge = function (svgPath, edgeData, diagramType, arrowMarkerAbsolute) {
const addMarkersToEdge = function (svgPath, edgeData, diagramType, arrowMarkerAbsolute, id) {
let url = '';
// Check configuration for absolute path
if (arrowMarkerAbsolute) {
@ -586,61 +588,103 @@ const addMarkersToEdge = function (svgPath, edgeData, diagramType, arrowMarkerAb
// look in edge data and decide which marker to use
switch (edgeData.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 (edgeData.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:
}
@ -651,7 +695,7 @@ const addMarkersToEdge = function (svgPath, edgeData, diagramType, arrowMarkerAb
*
* @param text
* @param diagObj
* @returns {object} ClassDef styles
* @returns {Record<string, import('../../../diagram-api/types.js').DiagramStyleClassDef>} ClassDef styles
*/
export const getClasses = function (text, diagObj) {
log.info('Extracting classes');
@ -691,7 +735,7 @@ const calcOffset = function (src, dest, parentLookupDb) {
return { x: ancestorOffset.posX, y: ancestorOffset.posY };
};
const insertEdge = function (edgesEl, edge, edgeData, diagObj, parentLookupDb) {
const insertEdge = function (edgesEl, edge, edgeData, diagObj, parentLookupDb, id) {
const offset = calcOffset(edge.sourceId, edge.targetId, parentLookupDb);
const src = edge.sections[0].startPoint;
@ -705,8 +749,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))
@ -722,7 +766,7 @@ const insertEdge = function (edgesEl, edge, edgeData, diagObj, parentLookupDb) {
'transform',
`translate(${edge.labels[0].x + offset.x}, ${edge.labels[0].y + offset.y})`
);
addMarkersToEdge(edgePath, edgeData, diagObj.type, diagObj.arrowMarkerAbsolute);
addMarkersToEdge(edgePath, edgeData, diagObj.type, diagObj.arrowMarkerAbsolute, id);
};
/**
@ -815,7 +859,7 @@ export const draw = async function (text, id, _version, diagObj) {
const markers = ['point', 'circle', 'cross'];
// Add the marker definitions to the svg as marker tags
insertMarkers(svg, markers, diagObj.type, diagObj.arrowMarkerAbsolute);
insertMarkers(svg, markers, diagObj.type, id);
// Fetch the vertices/nodes and edges/links from the parsed graph definition
const vert = diagObj.db.getVertices();
@ -894,7 +938,7 @@ export const draw = async function (text, id, _version, diagObj) {
drawNodes(0, 0, g.children, svg, subGraphsEl, diagObj, 0);
log.info('after layout', g);
g.edges?.map((edge) => {
insertEdge(edgesEl, edge, edge.edgeData, diagObj, parentLookupDb);
insertEdge(edgesEl, edge, edge.edgeData, diagObj, parentLookupDb, id);
});
setupGraphViewbox({}, svg, conf.diagramPadding, conf.useMaxWidth);
// Remove element after layout

View File

@ -2,7 +2,6 @@ import { select } from 'd3';
import utils from '../../utils.js';
import * as configApi from '../../config.js';
import common from '../common/common.js';
import mermaidAPI from '../../mermaidAPI.js';
import { log } from '../../logger.js';
import {
setAccTitle,
@ -34,10 +33,6 @@ let funs = [];
const sanitizeText = (txt) => common.sanitizeText(txt, config);
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
/**
* Function to lookup domId from id in the graph definition.
*
@ -771,7 +766,6 @@ export const lex = {
firstGraph,
};
export default {
parseDirective,
defaultConfig: () => configApi.defaultConfig.flowchart,
setAccTitle,
getAccTitle,

View File

@ -338,7 +338,7 @@ export const addEdges = function (edges, g, diagObj) {
*
* @param text
* @param diagObj
* @returns {object} ClassDef styles
* @returns {Record<string, import('../../diagram-api/types.js').DiagramStyleClassDef>} ClassDef styles
*/
export const getClasses = function (text, diagObj) {
return diagObj.db.getClasses();

View File

@ -269,7 +269,7 @@ export const addEdges = function (edges, g, diagObj) {
*
* @param text
* @param diagObj
* @returns {object} ClassDef styles
* @returns {Record<string, import('../../diagram-api/types.js').DiagramStyleClassDef>} ClassDef styles
*/
export const getClasses = function (text, diagObj) {
log.info('Extracting classes');

View File

@ -23,17 +23,8 @@
%x href
%x callbackname
%x callbackargs
%x open_directive
%x type_directive
%x arg_directive
%x close_directive
%%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; }
@ -272,35 +263,10 @@ that id.
%% /* language grammar */
start
: mermaidDoc
| directive start
;
directive
: openDirective typeDirective closeDirective separator
| openDirective typeDirective ':' argDirective closeDirective separator
;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
;
typeDirective
: type_directive { yy.parseDirective($type_directive, 'type_directive'); }
;
argDirective
: arg_directive { $arg_directive = $arg_directive.trim().replace(/'/g, '"'); yy.parseDirective($arg_directive, 'arg_directive'); }
;
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'flowchart'); }
;
mermaidDoc
: graphConfig document
;
document
: /* empty */
{ $$ = [];}

View File

@ -6,7 +6,6 @@ import dayjsAdvancedFormat from 'dayjs/plugin/advancedFormat.js';
import { log } from '../../logger.js';
import * as configApi from '../../config.js';
import utils from '../../utils.js';
import mermaidAPI from '../../mermaidAPI.js';
import {
setAccTitle,
@ -42,10 +41,6 @@ let weekday = 'sunday';
// The serial order of the task in the script
let lastOrder = 0;
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
export const clear = function () {
sections = [];
tasks = [];
@ -730,7 +725,6 @@ export const bindFunctions = function (element) {
};
export default {
parseDirective,
getConfig: () => configApi.getConfig().gantt,
clear,
setDateFormat,

View File

@ -10,6 +10,8 @@ import {
axisBottom,
axisTop,
timeFormat,
timeMillisecond,
timeSecond,
timeMinute,
timeHour,
timeDay,
@ -573,7 +575,7 @@ export const draw = function (text, id, version, diagObj) {
.tickSize(-h + theTopPad + conf.gridLineStartPadding)
.tickFormat(timeFormat(diagObj.db.getAxisFormat() || conf.axisFormat || '%Y-%m-%d'));
const reTickInterval = /^([1-9]\d*)(minute|hour|day|week|month)$/;
const reTickInterval = /^([1-9]\d*)(millisecond|second|minute|hour|day|week|month)$/;
const resultTickInterval = reTickInterval.exec(
diagObj.db.getTickInterval() || conf.tickInterval
);
@ -584,6 +586,12 @@ export const draw = function (text, id, version, diagObj) {
const weekday = diagObj.db.getWeekday() || conf.weekday;
switch (interval) {
case 'millisecond':
bottomXAxis.ticks(timeMillisecond.every(every));
break;
case 'second':
bottomXAxis.ticks(timeSecond.every(every));
break;
case 'minute':
bottomXAxis.ticks(timeMinute.every(every));
break;
@ -625,6 +633,12 @@ export const draw = function (text, id, version, diagObj) {
const weekday = diagObj.db.getWeekday() || conf.weekday;
switch (interval) {
case 'millisecond':
topXAxis.ticks(timeMillisecond.every(every));
break;
case 'second':
topXAxis.ticks(timeSecond.every(every));
break;
case 'minute':
topXAxis.ticks(timeMinute.every(every));
break;

View File

@ -11,19 +11,11 @@
%x href
%x callbackname
%x callbackargs
%x open_directive
%x type_directive
%x arg_directive
%x close_directive
%x acc_title
%x acc_descr
%x acc_descr_multiline
%%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
@ -112,8 +104,7 @@ weekday\s+sunday return 'weekday_sunday'
%% /* language grammar */
start
: directive start
| gantt document 'EOF' { return $2; }
: gantt document 'EOF' { return $2; }
;
document
@ -155,13 +146,8 @@ statement
| section { yy.addSection($1.substr(8));$$=$1.substr(8); }
| clickStatement
| taskTxt taskData {yy.addTask($1,$2);$$='task';}
| directive
;
directive
: openDirective typeDirective closeDirective 'NL'
| openDirective typeDirective ':' argDirective closeDirective 'NL'
;
/*
click allows any combination of href and call.
@ -192,20 +178,4 @@ clickStatementDebug
| click href {$$=$1 + ' ' + $2;}
;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
;
typeDirective
: type_directive { yy.parseDirective($1, 'type_directive'); }
;
argDirective
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
;
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'gantt'); }
;
%%

View File

@ -1,6 +1,5 @@
import { log } from '../../logger.js';
import { random } from '../../utils.js';
import mermaidAPI from '../../mermaidAPI.js';
import * as configApi from '../../config.js';
import { getConfig } from '../../config.js';
import common from '../common/common.js';
@ -33,10 +32,6 @@ function getId() {
return random({ length: 7 });
}
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
// /**
// * @param currentCommit
// * @param otherCommit
@ -507,7 +502,6 @@ export const commitType = {
};
export default {
parseDirective,
getConfig: () => configApi.getConfig().gitGraph,
setDirection,
setOptions,

View File

@ -1,87 +1,72 @@
import gitGraphAst from './gitGraphAst.js';
import { parser } from './parser/gitGraph.jison';
// Todo reintroduce without cryptoRandomString
import gitGraphAst from './gitGraphAst';
import { parser } from './parser/gitGraph';
import randomString from 'crypto-random-string';
import cryptoRandomString from 'crypto-random-string';
jest.mock('crypto-random-string');
describe('when parsing a gitGraph', function() {
let randomNumber;
beforeEach(function() {
describe('when parsing a gitGraph', function () {
beforeEach(function () {
parser.yy = gitGraphAst;
parser.yy.clear();
randomNumber = 0;
cryptoRandomString.mockImplementation(() => {
randomNumber = randomNumber + 1;
return String(randomNumber);
});
});
afterEach(function() {
cryptoRandomString.mockReset();
});
it('should handle a gitGraph definition', function() {
it('should handle a gitGraph definition', function () {
const str = 'gitGraph:\n' + 'commit\n';
parser.parse(str);
const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(1);
expect(parser.yy.getCurrentBranch()).toBe('master');
expect(parser.yy.getCurrentBranch()).toBe('main');
expect(parser.yy.getDirection()).toBe('LR');
expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
});
it('should handle a gitGraph definition with empty options', function() {
const str = 'gitGraph:\n' + 'options\n' + 'end\n' + 'commit\n';
it('should handle a gitGraph definition with empty options', function () {
const str = 'gitGraph:\n' + 'options\n' + ' end\n' + 'commit\n';
parser.parse(str);
const commits = parser.yy.getCommits();
expect(parser.yy.getOptions()).toEqual({});
expect(Object.keys(commits).length).toBe(1);
expect(parser.yy.getCurrentBranch()).toBe('master');
expect(parser.yy.getCurrentBranch()).toBe('main');
expect(parser.yy.getDirection()).toBe('LR');
expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
});
it('should handle a gitGraph definition with valid options', function() {
it('should handle a gitGraph definition with valid options', function () {
const str = 'gitGraph:\n' + 'options\n' + '{"key": "value"}\n' + 'end\n' + 'commit\n';
parser.parse(str);
const commits = parser.yy.getCommits();
expect(parser.yy.getOptions()['key']).toBe('value');
expect(Object.keys(commits).length).toBe(1);
expect(parser.yy.getCurrentBranch()).toBe('master');
expect(parser.yy.getCurrentBranch()).toBe('main');
expect(parser.yy.getDirection()).toBe('LR');
expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
});
it('should not fail on a gitGraph with malformed json', function() {
it('should not fail on a gitGraph with malformed json', function () {
const str = 'gitGraph:\n' + 'options\n' + '{"key": "value"\n' + 'end\n' + 'commit\n';
parser.parse(str);
const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(1);
expect(parser.yy.getCurrentBranch()).toBe('master');
expect(parser.yy.getCurrentBranch()).toBe('main');
expect(parser.yy.getDirection()).toBe('LR');
expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
});
it('should handle set direction', function() {
const str = 'gitGraph BT:\n' + 'commit\n';
it('should handle set direction', function () {
const str = 'gitGraph TB:\n' + 'commit\n';
parser.parse(str);
const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(1);
expect(parser.yy.getCurrentBranch()).toBe('master');
expect(parser.yy.getDirection()).toBe('BT');
expect(parser.yy.getCurrentBranch()).toBe('main');
expect(parser.yy.getDirection()).toBe('TB');
expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
});
it('should checkout a branch', function() {
it('should checkout a branch', function () {
const str = 'gitGraph:\n' + 'branch new\n' + 'checkout new\n';
parser.parse(str);
@ -91,7 +76,7 @@ describe('when parsing a gitGraph', function() {
expect(parser.yy.getCurrentBranch()).toBe('new');
});
it('should add commits to checked out branch', function() {
it('should add commits to checked out branch', function () {
const str = 'gitGraph:\n' + 'branch new\n' + 'checkout new\n' + 'commit\n' + 'commit\n';
parser.parse(str);
@ -103,7 +88,7 @@ describe('when parsing a gitGraph', function() {
expect(branchCommit).not.toBeNull();
expect(commits[branchCommit].parent).not.toBeNull();
});
it('should handle commit with args', function() {
it('should handle commit with args', function () {
const str = 'gitGraph:\n' + 'commit "a commit"\n';
parser.parse(str);
@ -112,10 +97,11 @@ describe('when parsing a gitGraph', function() {
expect(Object.keys(commits).length).toBe(1);
const key = Object.keys(commits)[0];
expect(commits[key].message).toBe('a commit');
expect(parser.yy.getCurrentBranch()).toBe('master');
expect(parser.yy.getCurrentBranch()).toBe('main');
});
it('should reset a branch', function() {
// Reset has been commented out in JISON
it.skip('should reset a branch', function () {
const str =
'gitGraph:\n' +
'commit\n' +
@ -123,18 +109,18 @@ describe('when parsing a gitGraph', function() {
'branch newbranch\n' +
'checkout newbranch\n' +
'commit\n' +
'reset master\n';
'reset main\n';
parser.parse(str);
const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(3);
expect(parser.yy.getCurrentBranch()).toBe('newbranch');
expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['master']);
expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['main']);
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['newbranch']);
});
it('reset can take an argument', function() {
it.skip('reset can take an argument', function () {
const str =
'gitGraph:\n' +
'commit\n' +
@ -142,18 +128,18 @@ describe('when parsing a gitGraph', function() {
'branch newbranch\n' +
'checkout newbranch\n' +
'commit\n' +
'reset master^\n';
'reset main^\n';
parser.parse(str);
const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(3);
expect(parser.yy.getCurrentBranch()).toBe('newbranch');
const master = commits[parser.yy.getBranches()['master']];
expect(parser.yy.getHead().id).toEqual(master.parent);
const main = commits[parser.yy.getBranches()['main']];
expect(parser.yy.getHead().id).toEqual(main.parent);
});
it('should handle fast forwardable merges', function() {
it.skip('should handle fast forwardable merges', function () {
const str =
'gitGraph:\n' +
'commit\n' +
@ -161,19 +147,19 @@ describe('when parsing a gitGraph', function() {
'checkout newbranch\n' +
'commit\n' +
'commit\n' +
'checkout master\n' +
'checkout main\n' +
'merge newbranch\n';
parser.parse(str);
const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(3);
expect(parser.yy.getCurrentBranch()).toBe('master');
expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['master']);
expect(Object.keys(commits).length).toBe(4);
expect(parser.yy.getCurrentBranch()).toBe('main');
expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['main']);
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['newbranch']);
});
it('should handle cases when merge is a noop', function() {
it('should handle cases when merge is a noop', function () {
const str =
'gitGraph:\n' +
'commit\n' +
@ -181,18 +167,18 @@ describe('when parsing a gitGraph', function() {
'checkout newbranch\n' +
'commit\n' +
'commit\n' +
'merge master\n';
'merge main\n';
parser.parse(str);
const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(3);
expect(Object.keys(commits).length).toBe(4);
expect(parser.yy.getCurrentBranch()).toBe('newbranch');
expect(parser.yy.getBranches()['newbranch']).not.toEqual(parser.yy.getBranches()['master']);
expect(parser.yy.getBranches()['newbranch']).not.toEqual(parser.yy.getBranches()['main']);
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['newbranch']);
});
it('should handle merge with 2 parents', function() {
it('should handle merge with 2 parents', function () {
const str =
'gitGraph:\n' +
'commit\n' +
@ -200,7 +186,7 @@ describe('when parsing a gitGraph', function() {
'checkout newbranch\n' +
'commit\n' +
'commit\n' +
'checkout master\n' +
'checkout main\n' +
'commit\n' +
'merge newbranch\n';
@ -208,12 +194,12 @@ describe('when parsing a gitGraph', function() {
const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(5);
expect(parser.yy.getCurrentBranch()).toBe('master');
expect(parser.yy.getBranches()['newbranch']).not.toEqual(parser.yy.getBranches()['master']);
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['master']);
expect(parser.yy.getCurrentBranch()).toBe('main');
expect(parser.yy.getBranches()['newbranch']).not.toEqual(parser.yy.getBranches()['main']);
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['main']);
});
it('should handle ff merge when history walk has two parents (merge commit)', function() {
it.skip('should handle ff merge when history walk has two parents (merge commit)', function () {
const str =
'gitGraph:\n' +
'commit\n' +
@ -221,53 +207,25 @@ describe('when parsing a gitGraph', function() {
'checkout newbranch\n' +
'commit\n' +
'commit\n' +
'checkout master\n' +
'checkout main\n' +
'commit\n' +
'merge newbranch\n' +
'commit\n' +
'checkout newbranch\n' +
'merge master\n';
'merge main\n';
parser.parse(str);
const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(6);
expect(Object.keys(commits).length).toBe(7);
expect(parser.yy.getCurrentBranch()).toBe('newbranch');
expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['master']);
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['master']);
expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['main']);
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['main']);
parser.yy.prettyPrint();
});
it('should generate a secure random ID for commits', function() {
const str = 'gitGraph:\n' + 'commit\n' + 'commit\n';
const EXPECTED_LENGTH = 7;
const EXPECTED_CHARACTERS = '0123456789abcdef';
let idCount = 0;
randomString.mockImplementation(options => {
if (
options.length === EXPECTED_LENGTH &&
options.characters === EXPECTED_CHARACTERS &&
Object.keys(options).length === 2
) {
const id = `abcdef${idCount}`;
idCount += 1;
return id;
}
return 'unexpected-ID';
});
parser.parse(str);
const commits = parser.yy.getCommits();
expect(Object.keys(commits)).toEqual(['abcdef0', 'abcdef1']);
Object.keys(commits).forEach(key => {
expect(commits[key].id).toEqual(key);
});
});
it('should generate an array of known branches', function() {
it('should generate an array of known branches', function () {
const str =
'gitGraph:\n' +
'commit\n' +
@ -281,7 +239,7 @@ describe('when parsing a gitGraph', function() {
const branches = gitGraphAst.getBranchesAsObjArray();
expect(branches).toHaveLength(3);
expect(branches[0]).toHaveProperty('name', 'master');
expect(branches[0]).toHaveProperty('name', 'main');
expect(branches[1]).toHaveProperty('name', 'b1');
expect(branches[2]).toHaveProperty('name', 'b2');
});

View File

@ -1,22 +1,11 @@
/* eslint-env jasmine */
// Todo reintroduce without cryptoRandomString
import gitGraphAst from './gitGraphAst.js';
import { parser } from './parser/gitGraph.jison';
//import randomString from 'crypto-random-string';
//import cryptoRandomString from 'crypto-random-string';
//jest.mock('crypto-random-string');
describe('when parsing a gitGraph', function () {
let randomNumber;
beforeEach(function () {
parser.yy = gitGraphAst;
parser.yy.clear();
randomNumber = 0;
});
// afterEach(function() {
// cryptoRandomString.mockReset();
// });
it('should handle a gitGraph commit with NO pararms, get auto-genrated reandom ID', function () {
const str = `gitGraph:
commit

View File

@ -9,10 +9,6 @@
%x string
%x options
%x open_directive
%x type_directive
%x arg_directive
%x close_directive
%x acc_title
%x acc_descr
%x acc_descr_multiline
@ -20,11 +16,6 @@
%%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; }
@ -76,7 +67,6 @@ checkout(?=\s|$) return 'CHECKOUT';
start
: eol start
| directive start
| GG document EOF{ return $3; }
| GG ':' document EOF{ return $3; }
| GG DIR ':' document EOF {yy.setDirection($2); return $4;}
@ -240,27 +230,6 @@ commitType
| HIGHLIGHT { $$=yy.commitType.HIGHLIGHT;}
;
directive
: openDirective typeDirective closeDirective
| openDirective typeDirective ':' argDirective closeDirective
;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
;
typeDirective
: type_directive { yy.parseDirective($1, 'type_directive'); }
;
argDirective
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
;
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'gitGraph'); }
;
ref
: ID
| STR

View File

@ -8,19 +8,10 @@
%x string
%x title
%x open_directive
%x type_directive
%x arg_directive
%x close_directive
%x acc_title
%x acc_descr
%x acc_descr_multiline
%%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
\%\%(?!\{)[^\n]* /* skip comments */
[^\}]\%\%[^\n]* /* skip comments */{ /*console.log('');*/ }
[\n\r]+ return 'NEWLINE';
@ -52,7 +43,6 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
start
: eol start
| directive start
| PIE document
| PIE showData document {yy.setShowData(true);}
;
@ -73,34 +63,12 @@ statement
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
| acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); } | section {yy.addSection($1.substr(8));$$=$1.substr(8);}
| directive
;
directive
: openDirective typeDirective closeDirective
| openDirective typeDirective ':' argDirective closeDirective
;
eol
: NEWLINE
| ';'
| EOF
;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
;
typeDirective
: type_directive { yy.parseDirective($1, 'type_directive'); }
;
argDirective
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
;
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'pie'); }
;
%%

View File

@ -62,17 +62,6 @@ describe('pie', () => {
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with a directive', () => {
parser.parse(`%%{init: {'logLevel':0}}%%
pie
"ash" : 60
"bat" : 40
`);
const sections = db.getSections();
expect(sections['ash']).toBe(60);
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with a title', () => {
parser.parse(`pie title a 60/40 pie
"ash" : 60

View File

@ -1,5 +1,4 @@
import { log } from '../../logger.js';
import { parseDirective as _parseDirective } from '../../directiveUtils.js';
import { getConfig as commonGetConfig } from '../../config.js';
import { sanitizeText } from '../common/common.js';
import {
@ -11,7 +10,6 @@ import {
setAccDescription,
clear as commonClear,
} from '../common/commonDb.js';
import type { ParseDirectiveDefinition } from '../../diagram-api/types.js';
import type { PieFields, PieDB, Sections } from './pieTypes.js';
import type { RequiredDeep } from 'type-fest';
import type { PieDiagramConfig } from '../../config.type.js';
@ -31,10 +29,6 @@ const config: Required<PieDiagramConfig> = structuredClone(DEFAULT_PIE_CONFIG);
const getConfig = (): Required<PieDiagramConfig> => structuredClone(config);
const parseDirective: ParseDirectiveDefinition = (statement, context, type) => {
_parseDirective(this, statement, context, type);
};
const clear = (): void => {
sections = structuredClone(DEFAULT_PIE_DB.sections);
showData = DEFAULT_PIE_DB.showData;
@ -67,7 +61,6 @@ const getShowData = (): boolean => showData;
export const db: PieDB = {
getConfig,
parseDirective,
clear,
setDiagramTitle,
getDiagramTitle,

View File

@ -1,5 +1,5 @@
import type { PieDiagramConfig } from '../../config.type.js';
import type { DiagramDB, ParseDirectiveDefinition } from '../../diagram-api/types.js';
import type { DiagramDB } from '../../diagram-api/types.js';
export interface PieFields {
sections: Sections;
@ -46,7 +46,6 @@ export interface PieDB extends DiagramDB {
getConfig: () => Required<PieDiagramConfig>;
// common db
parseDirective: ParseDirectiveDefinition;
clear: () => void;
setDiagramTitle: (title: string) => void;
getDiagramTitle: () => string;

View File

@ -5,10 +5,6 @@
%x string
%x md_string
%x title
%x open_directive
%x type_directive
%x arg_directive
%x close_directive
%x acc_title
%x acc_descr
%x acc_descr_multiline
@ -16,11 +12,6 @@
%x point_x
%x point_y
%%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
\%\%(?!\{)[^\n]* /* skip comments */
[^\}]\%\%[^\n]* /* skip comments */
[\n\r]+ return 'NEWLINE';
@ -87,7 +78,6 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
start
: eol start
| SPACE start
| directive start
| QUADRANT document
;
@ -110,7 +100,6 @@ statement
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
| acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); } | section {yy.addSection($1.substr(8));$$=$1.substr(8);}
| directive
;
points
@ -133,33 +122,12 @@ quadrantDetails
| QUADRANT_4 text {yy.setQuadrant4Text($2)}
;
directive
: openDirective typeDirective closeDirective
| openDirective typeDirective ':' argDirective closeDirective
;
eol
: NEWLINE
| SEMI
| EOF
;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
;
typeDirective
: type_directive { yy.parseDirective($1, 'type_directive'); }
;
argDirective
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
;
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'quadrantChart'); }
;
text: alphaNumToken
{ $$={text:$1, type: 'text'};}
| text textNoTagsToken

View File

@ -19,7 +19,6 @@ const mockDB: Record<string, Mock<any, any>> = {
setYAxisTopText: vi.fn(),
setYAxisBottomText: vi.fn(),
setDiagramTitle: vi.fn(),
parseDirective: vi.fn(),
addPoint: vi.fn(),
};
@ -45,23 +44,6 @@ describe('Testing quadrantChart jison file', () => {
expect(parserFnConstructor(str)).not.toThrow();
});
it('should be able to parse directive', () => {
const str =
'%%{init: {"quadrantChart": {"chartWidth": 600, "chartHeight": 600} } }%% \n quadrantChart';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.parseDirective.mock.calls[0]).toEqual(['%%{', 'open_directive']);
expect(mockDB.parseDirective.mock.calls[1]).toEqual(['init', 'type_directive']);
expect(mockDB.parseDirective.mock.calls[2]).toEqual([
'{"quadrantChart": {"chartWidth": 600, "chartHeight": 600} }',
'arg_directive',
]);
expect(mockDB.parseDirective.mock.calls[3]).toEqual([
'}%%',
'close_directive',
'quadrantChart',
]);
});
it('should be able to parse xAxis text', () => {
let str = 'quadrantChart\nx-axis urgent --> not urgent';
expect(parserFnConstructor(str)).not.toThrow();
@ -243,8 +225,7 @@ describe('Testing quadrantChart jison file', () => {
});
it('should be able to parse the whole chart', () => {
const str = `%%{init: {"quadrantChart": {"chartWidth": 600, "chartHeight": 600} } }%%
quadrantChart
const str = `quadrantChart
title Analytics and Business Intelligence Platforms
x-axis "Completeness of Vision ❤" --> "x-axis-2"
y-axis Ability to Execute --> "y-axis-2"
@ -258,17 +239,6 @@ describe('Testing quadrantChart jison file', () => {
Incorta: [0.20, 0.30]`;
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.parseDirective.mock.calls[0]).toEqual(['%%{', 'open_directive']);
expect(mockDB.parseDirective.mock.calls[1]).toEqual(['init', 'type_directive']);
expect(mockDB.parseDirective.mock.calls[2]).toEqual([
'{"quadrantChart": {"chartWidth": 600, "chartHeight": 600} }',
'arg_directive',
]);
expect(mockDB.parseDirective.mock.calls[3]).toEqual([
'}%%',
'close_directive',
'quadrantChart',
]);
expect(mockDB.setXAxisLeftText).toHaveBeenCalledWith({
text: 'Completeness of Vision ❤',
type: 'text',

View File

@ -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;
}

View File

@ -1,5 +1,3 @@
import { log } from '../../logger.js';
import mermaidAPI from '../../mermaidAPI.js';
import * as configApi from '../../config.js';
import { sanitizeText } from '../common/common.js';
import {
@ -94,11 +92,6 @@ function getQuadrantData() {
return quadrantBuilder.build();
}
export const parseDirective = function (statement: string, context: string, type: string) {
// @ts-ignore: TODO Fix ts errors
mermaidAPI.parseDirective(this, statement, context, type);
};
const clear = function () {
quadrantBuilder.clear();
commonClear();
@ -117,7 +110,6 @@ export default {
setYAxisBottomText,
addPoint,
getQuadrantData,
parseDirective,
clear,
setAccTitle,
getAccTitle,

View File

@ -9,19 +9,10 @@
%x string
%x token
%x unqString
%x open_directive
%x type_directive
%x arg_directive
%x close_directive
%x acc_title
%x acc_descr
%x acc_descr_multiline
%%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
"title"\s[^#\n;]+ return 'title';
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
@ -99,23 +90,10 @@ start
| RD NEWLINE diagram EOF;
directive
: openDirective typeDirective closeDirective
| openDirective typeDirective ':' argDirective closeDirective
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
: acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
| acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); }
;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); };
typeDirective
: type_directive { yy.parseDirective($1, 'type_directive'); };
argDirective
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); };
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'pie'); };
diagram
: /* empty */ { $$ = [] }

View File

@ -1,6 +1,5 @@
import * as configApi from '../../config.js';
import { log } from '../../logger.js';
import mermaidAPI from '../../mermaidAPI.js';
import {
setAccTitle,
@ -48,10 +47,6 @@ const Relationships = {
TRACES: 'traces',
};
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
const addRequirement = (name, type) => {
if (requirements[name] === undefined) {
requirements[name] = {
@ -149,7 +144,6 @@ export default {
VerifyType,
Relationships,
parseDirective,
getConfig: () => configApi.getConfig().req,
addRequirement,

View File

@ -16,22 +16,15 @@
// A special state for grabbing text up to the first comment/newline
%x ID ALIAS LINE
// Directive states
%x open_directive type_directive arg_directive
%x acc_title
%x acc_descr
%x acc_descr_multiline
%%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
[\n]+ return 'NEWLINE';
\s+ /* skip all whitespace */
<ID,ALIAS,LINE>((?!\n)\s)+ /* skip same-line whitespace */
<INITIAL,ID,ALIAS,LINE,arg_directive,type_directive,open_directive>\#[^\n]* /* skip comments */
<INITIAL,ID,ALIAS,LINE>\#[^\n]* /* skip comments */
\%%(?!\{)[^\n]* /* skip comments */
[^\}]\%\%[^\n]* /* skip comments */
[0-9]+(?=[ \n]+) return 'NUM';
@ -106,7 +99,6 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
start
: SPACE start
| NEWLINE start
| directive start
| SD document { yy.apply($2);return $2; }
;
@ -133,11 +125,6 @@ box_line
;
directive
: openDirective typeDirective closeDirective 'NEWLINE'
| openDirective typeDirective ':' argDirective closeDirective 'NEWLINE'
;
statement
: participant_statement
| 'create' participant_statement {$2.type='createParticipant'; $$=$2;}
@ -215,7 +202,6 @@ statement
$3.unshift({type: 'breakStart', breakText:yy.parseMessage($2), signalType: yy.LINETYPE.BREAK_START});
$3.push({type: 'breakEnd', optText:yy.parseMessage($2), signalType: yy.LINETYPE.BREAK_END});
$$=$3;}
| directive
;
option_sections
@ -335,20 +321,4 @@ text2
: TXT {$$ = yy.parseMessage($1.trim().substring(1)) }
;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
;
typeDirective
: type_directive { yy.parseDirective($1, 'type_directive'); }
;
argDirective
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
;
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'sequence'); }
;
%%

View File

@ -1,4 +1,3 @@
import mermaidAPI from '../../mermaidAPI.js';
import * as configApi from '../../config.js';
import { log } from '../../logger.js';
import { sanitizeText } from '../common/common.js';
@ -25,10 +24,6 @@ let currentBox = undefined;
let lastCreated = undefined;
let lastDestroyed = undefined;
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
export const addBox = function (data) {
boxes.push({
name: data.text,
@ -634,7 +629,6 @@ export default {
getBoxes,
getDiagramTitle,
setDiagramTitle,
parseDirective,
getConfig: () => configApi.getConfig().sequence,
clear,
parseMessage,

View File

@ -1,5 +1,4 @@
import { vi } from 'vitest';
import * as configApi from '../../config.js';
import mermaidAPI from '../../mermaidAPI.js';
import { Diagram, getDiagramFromText } from '../../Diagram.js';
@ -225,6 +224,7 @@ Bob-->Alice: I am good thanks!`;
diagram.renderer.draw(str, 'tst', '1.2.3', diagram); // needs to be rendered for the correct value of visibility auto numbers
expect(diagram.db.showSequenceNumbers()).toBe(true);
});
it('should handle a sequenceDiagram definition with a title:', async () => {
const str = `
sequenceDiagram
@ -2034,90 +2034,3 @@ participant Alice`;
});
});
});
describe('when rendering a sequenceDiagram with directives', () => {
beforeAll(function () {
let conf = {
diagramMarginX: 50,
diagramMarginY: 10,
actorMargin: 50,
width: 150,
height: 65,
boxMargin: 10,
messageMargin: 40,
boxTextMargin: 15,
noteMargin: 25,
};
mermaidAPI.initialize({ sequence: conf });
});
beforeEach(function () {
mermaidAPI.reset();
diagram.renderer.bounds.init();
});
it('should handle one actor, when theme is dark and logLevel is 1 DX1 (dfg1)', async () => {
const str = `
%%{init: { "theme": "dark", "logLevel": 1 } }%%
sequenceDiagram
%%{wrap}%%
participant Alice
`;
diagram = new Diagram(str);
diagram.renderer.bounds.init();
diagram.renderer.draw(str, 'tst', '1.2.3', diagram);
const { bounds, models } = diagram.renderer.bounds.getBounds();
const mermaid = mermaidAPI.getConfig();
expect(mermaid.theme).toBe('dark');
expect(mermaid.logLevel).toBe(1);
expect(bounds.startx).toBe(0);
expect(bounds.startx).toBe(0);
expect(bounds.starty).toBe(0);
expect(bounds.stopy).toBe(
models.lastActor().stopy + models.lastActor().height + mermaid.sequence.boxMargin
);
});
it('should handle one actor, when logLevel is 3 (dfg0)', async () => {
const str = `
%%{initialize: { "logLevel": 3 }}%%
sequenceDiagram
participant Alice
`;
diagram = new Diagram(str);
diagram.renderer.draw(str, 'tst', '1.2.3', diagram);
const { bounds, models } = diagram.renderer.bounds.getBounds();
const mermaid = mermaidAPI.getConfig();
expect(mermaid.logLevel).toBe(3);
expect(bounds.startx).toBe(0);
expect(bounds.startx).toBe(0);
expect(bounds.starty).toBe(0);
expect(bounds.stopy).toBe(
models.lastActor().stopy + models.lastActor().height + mermaid.sequence.boxMargin
);
});
it('should hide sequence numbers when autonumber is removed when autonumber is enabled', async () => {
const str1 = `
sequenceDiagram
autonumber
Alice->Bob:Hello Bob, how are you?
Note right of Bob: Bob thinks
Bob-->Alice: I am good thanks!`;
diagram = new Diagram(str1);
diagram.renderer.draw(str1, 'tst', '1.2.3', diagram); // needs to be rendered for the correct value of visibility auto numbers
expect(diagram.db.showSequenceNumbers()).toBe(true);
const str2 = `
sequenceDiagram
Alice->Bob:Hello Bob, how are you?
Note right of Bob: Bob thinks
Bob-->Alice: I am good thanks!`;
diagram = new Diagram(str2);
diagram.renderer.draw(str2, 'tst', '1.2.3', diagram);
expect(diagram.db.showSequenceNumbers()).toBe(false);
});
});

View File

@ -10,4 +10,7 @@ export const diagram: DiagramDefinition = {
db,
renderer,
styles,
init: ({ wrap }) => {
db.setWrap(wrap);
},
};

View File

@ -33,10 +33,6 @@
%x FLOATING_NOTE
%x FLOATING_NOTE_ID
%x struct
%x open_directive
%x type_directive
%x arg_directive
%x close_directive
// A special state for grabbing text up to the first comment/newline
%x LINE
@ -50,18 +46,13 @@
.*direction\s+RL[^\n]* return 'direction_rl';
.*direction\s+LR[^\n]* return 'direction_lr';
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
\%\%(?!\{)[^\n]* /* skip comments */
[^\}]\%\%[^\n]* /* skip comments */{ /*console.log('Crap after close');*/ }
[\n]+ return 'NL';
[\s]+ /* skip all whitespace */
<ID,STATE,struct,LINE,open_directive,type_directive,arg_directive,close_directive>((?!\n)\s)+ /* skip same-line whitespace */
<INITIAL,ID,STATE,struct,LINE,open_directive,type_directive,arg_directive,close_directive>\#[^\n]* /* skip comments */
<ID,STATE,struct,LINE>((?!\n)\s)+ /* skip same-line whitespace */
<INITIAL,ID,STATE,struct,LINE>\#[^\n]* /* skip comments */
\%%[^\n]* /* skip comments */
"scale"\s+ { this.pushState('SCALE'); /* console.log('Got scale', yytext);*/ return 'scale'; }
<SCALE>\d+ return 'WIDTH';
@ -155,7 +146,6 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
start
: SPACE start
| NL start
| directive start
| SD document { /* console.log('--> Root document', $2); */ yy.setRootDoc($2); return $2; }
;
@ -241,7 +231,6 @@ statement
$$={ stmt: 'state', id: $3.trim(), note:{position: $2.trim(), text: $4.trim()}};
}
| note NOTE_TEXT AS ID
| directive
| direction
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
@ -264,10 +253,6 @@ cssClassStatement
}
;
directive
: openDirective typeDirective closeDirective
| openDirective typeDirective ':' argDirective closeDirective
;
direction
: direction_tb
{ yy.setDirection('TB');$$={stmt:'dir', value:'TB'};}
@ -308,20 +293,4 @@ notePosition
| right_of
;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
;
typeDirective
: type_directive { yy.parseDirective($1, 'type_directive'); }
;
argDirective
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
;
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'state'); }
;
%%

View File

@ -1,6 +1,5 @@
import { log } from '../../logger.js';
import { generateId } from '../../utils.js';
import mermaidAPI from '../../mermaidAPI.js';
import common from '../common/common.js';
import * as configApi from '../../config.js';
import {
@ -77,10 +76,6 @@ export const relationType = {
const clone = (o) => JSON.parse(JSON.stringify(o));
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
const setRootDoc = (o) => {
log.info('Setting root doc', o);
// rootDoc = { id: 'root', doc: o };
@ -547,7 +542,6 @@ const setDirection = (dir) => {
const trimColon = (str) => (str && str[0] === ':' ? str.substr(1).trim() : str.trim());
export default {
parseDirective,
getConfig: () => configApi.getConfig().state,
addState,
clear,

View File

@ -55,16 +55,6 @@ describe('state diagram V2, ', function () {
const title = stateDb.getAccTitle();
expect(title).toBe('a simple title of the diagram');
});
it('simple with directive', function () {
const str = `%%{init: {'logLevel': 0 }}%%
stateDiagram-v2\n
State1 : this is another string
[*] --> State1
State1 --> [*]
`;
parser.parse(str);
});
it('should handle relation definitions', function () {
const str = `stateDiagram-v2\n
[*] --> State1

View File

@ -66,16 +66,6 @@ describe('state diagram, ', function () {
const title = stateDb.getAccTitle();
expect(title).toBe('a simple title of the diagram');
});
it('simple with directive', function () {
const str = `%%{init: {'logLevel': 0 }}%%
stateDiagram\n
State1 : this is another string
[*] --> State1
State1 --> [*]
`;
parser.parse(str);
});
it('should handle relation definitions', function () {
const str = `stateDiagram\n
[*] --> State1

View File

@ -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<string, import('../../diagram-api/types.js').DiagramStyleClassDef>} ClassDef styles (a Map with keys = strings, values = )
*/
export const getClasses = function (text, diagramObj) {
diagramObj.db.extract(diagramObj.db.getRootDocV2());

View File

@ -9,17 +9,8 @@
%x acc_descr
%x acc_descr_multiline
// Directive states
%x open_directive type_directive arg_directive
%%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
\%%(?!\{)[^\n]* /* skip comments */
[^\}]\%\%[^\n]* /* skip comments */
[\n]+ return 'NEWLINE';
@ -55,7 +46,6 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
start
: timeline document 'EOF' { return $2; }
| directive start
;
document
@ -70,11 +60,6 @@ line
| EOF { $$=[];}
;
directive
: openDirective typeDirective closeDirective 'NEWLINE'
| openDirective typeDirective ':' argDirective closeDirective 'NEWLINE'
;
statement
: title {yy.getCommonDb().setDiagramTitle($1.substr(6));$$=$1.substr(6);}
| acc_title acc_title_value { $$=$2.trim();yy.getCommonDb().setAccTitle($$); }
@ -83,7 +68,6 @@ statement
| section {yy.addSection($1.substr(8));$$=$1.substr(8);}
| period_statement
| event_statement
| directive
;
period_statement
: period {yy.addTask($1,0,'');$$=$1;}
@ -92,21 +76,4 @@ event_statement
: event {yy.addEvent($1.substr(2));$$=$1;}
;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
;
typeDirective
: type_directive { yy.parseDirective($1, 'type_directive'); }
;
argDirective
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
;
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'timeline'); }
;
%%

View File

@ -1,24 +1,6 @@
import { parser as timeline } from './parser/timeline.jison';
import * as timelineDB from './timelineDb.js';
// import { injectUtils } from './mermaidUtils.js';
import { parseDirective as _parseDirective } from '../../directiveUtils.js';
import {
log,
setLogLevel,
getConfig,
sanitizeText,
setupGraphViewBox,
} from '../../diagram-api/diagramAPI.js';
// injectUtils(
// log,
// setLogLevel,
// getConfig,
// sanitizeText,
// setupGraphViewBox,
// _parseDirective
// );
import { setLogLevel } from '../../diagram-api/diagramAPI.js';
describe('when parsing a timeline ', function () {
beforeEach(function () {

View File

@ -1,4 +1,3 @@
import { parseDirective as _parseDirective } from '../../directiveUtils.js';
import * as commonDb from '../common/commonDb.js';
let currentSection = '';
let currentTaskId = 0;
@ -9,10 +8,6 @@ const rawTasks = [];
export const getCommonDb = () => commonDb;
export const parseDirective = (statement, context, type) => {
_parseDirective(this, statement, context, type);
};
export const clear = function () {
sections.length = 0;
tasks.length = 0;
@ -104,5 +99,4 @@ export default {
addTask,
addTaskOrg,
addEvent,
parseDirective,
};

View File

@ -1,4 +1,3 @@
import mermaidAPI from '../../mermaidAPI.js';
import * as configApi from '../../config.js';
import {
setAccTitle,
@ -16,10 +15,6 @@ const sections = [];
const tasks = [];
const rawTasks = [];
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
export const clear = function () {
sections.length = 0;
tasks.length = 0;
@ -118,7 +113,6 @@ const getActors = function () {
};
export default {
parseDirective,
getConfig: () => configApi.getConfig().journey,
clear,
setDiagramTitle,

View File

@ -9,17 +9,8 @@
%x acc_descr
%x acc_descr_multiline
// Directive states
%x open_directive type_directive arg_directive
%%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
\%%(?!\{)[^\n]* /* skip comments */
[^\}]\%\%[^\n]* /* skip comments */
[\n]+ return 'NEWLINE';
@ -52,7 +43,6 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
start
: journey document 'EOF' { return $2; }
| directive start
;
document
@ -67,11 +57,6 @@ line
| EOF { $$=[];}
;
directive
: openDirective typeDirective closeDirective 'NEWLINE'
| openDirective typeDirective ':' argDirective closeDirective 'NEWLINE'
;
statement
: title {yy.setDiagramTitle($1.substr(6));$$=$1.substr(6);}
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
@ -79,23 +64,6 @@ statement
| acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); }
| section {yy.addSection($1.substr(8));$$=$1.substr(8);}
| taskName taskData {yy.addTask($1, $2);$$='task';}
| directive
;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
;
typeDirective
: type_directive { yy.parseDirective($1, 'type_directive'); }
;
argDirective
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
;
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'journey'); }
;
%%

View File

@ -1,82 +0,0 @@
import * as configApi from './config.js';
import { log } from './logger.js';
let currentDirective: { type?: string; args?: any } | undefined = {};
export const parseDirective = function (
p: any,
statement: string,
context: string,
type: string
): void {
log.debug('parseDirective is being called', statement, context, type);
try {
if (statement !== undefined) {
statement = statement.trim();
switch (context) {
case 'open_directive':
currentDirective = {};
break;
case 'type_directive':
if (!currentDirective) {
throw new Error('currentDirective is undefined');
}
currentDirective.type = statement.toLowerCase();
break;
case 'arg_directive':
if (!currentDirective) {
throw new Error('currentDirective is undefined');
}
currentDirective.args = JSON.parse(statement);
break;
case 'close_directive':
handleDirective(p, currentDirective, type);
currentDirective = undefined;
break;
}
}
} catch (error) {
log.error(
`Error while rendering sequenceDiagram directive: ${statement} jison context: ${context}`
);
// @ts-ignore: TODO Fix ts errors
log.error(error.message);
}
};
const handleDirective = function (p: any, directive: any, type: string): void {
log.info(`Directive type=${directive.type} with args:`, directive.args);
switch (directive.type) {
case 'init':
case 'initialize': {
['config'].forEach((prop) => {
if (directive.args[prop] !== undefined) {
if (type === 'flowchart-v2') {
type = 'flowchart';
}
directive.args[type] = directive.args[prop];
delete directive.args[prop];
}
});
configApi.addDirective(directive.args);
break;
}
case 'wrap':
case 'nowrap':
if (p && p['setWrap']) {
p.setWrap(directive.type === 'wrap');
}
break;
case 'themeCss':
log.warn('themeCss encountered');
break;
default:
log.warn(
`Unhandled directive: source: '%%{${directive.type}: ${JSON.stringify(
directive.args ? directive.args : {}
)}}%%`,
directive
);
break;
}
};

View File

@ -56,10 +56,10 @@ from IPython.display import Image, display
import matplotlib.pyplot as plt
def mm(graph):
graphbytes = graph.encode("ascii")
base64_bytes = base64.b64encode(graphbytes)
base64_string = base64_bytes.decode("ascii")
display(Image(url="https://mermaid.ink/img/" + base64_string))
graphbytes = graph.encode("utf8")
base64_bytes = base64.b64encode(graphbytes)
base64_string = base64_bytes.decode("ascii")
display(Image(url="https://mermaid.ink/img/" + base64_string))
mm("""
graph LR;

View File

@ -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"
}
}

View File

@ -554,13 +554,13 @@ flowchart LR
C-->D
click A callback "Tooltip for a callback"
click B "https://www.github.com" "This is a tooltip for a link"
click A call callback() "Tooltip for a callback"
click B href "https://www.github.com" "This is a tooltip for a link"
click C call callback() "Tooltip for a callback"
click D href "https://www.github.com" "This is a tooltip for a link"
```
> **Success** The tooltip functionality and the ability to link to urls are available from version 0.5.2.
?> Due to limitations with how Docsify handles JavaScript callback functions, an alternate working demo for the above code can be viewed at [this jsfiddle](https://jsfiddle.net/s37cjoau/3/).
?> Due to limitations with how Docsify handles JavaScript callback functions, an alternate working demo for the above code can be viewed at [this jsfiddle](https://jsfiddle.net/Ogglas/2o73vdez/7).
Links are opened in the same browser tab/window by default. It is possible to change this by adding a link target to the click definition (`_self`, `_blank`, `_parent` and `_top` are supported):

View File

@ -173,7 +173,7 @@ The following formatting strings are supported:
More info in: [https://github.com/d3/d3-time-format/tree/v4.0.0#locale_format](https://github.com/d3/d3-time-format/tree/v4.0.0#locale_format)
### Axis ticks
### Axis ticks (v10.3.0+)
The default output ticks are auto. You can custom your `tickInterval`, like `1day` or `1week`.
@ -184,7 +184,7 @@ tickInterval 1day
The pattern is:
```javascript
/^([1-9][0-9]*)(minute|hour|day|week|month)$/;
/^([1-9][0-9]*)(millisecond|second|minute|hour|day|week|month)$/;
```
More info in: [https://github.com/d3/d3-time#interval_every](https://github.com/d3/d3-time#interval_every)
@ -197,7 +197,9 @@ gantt
weekday monday
```
Support: v10.3.0+
```warning
`millisecond` and `second` support was added in vMERMAID_RELEASE_VERSION
```
## Output in compact mode

View File

@ -2,9 +2,9 @@
> A sankey diagram is a visualization used to depict a flow from one set of values to another.
::: warning
```warning
This is an experimental diagram. Its syntax are very close to plain CSV, but it is to be extended in the nearest future.
:::
```
The things being connected are called nodes and the connections are called links.
@ -13,6 +13,11 @@ The things being connected are called nodes and the connections are called links
This example taken from [observable](https://observablehq.com/@d3/sankey/2?collection=@d3/d3-sankey). It may be rendered a little bit differently, though, in terms of size and colors.
```mermaid-example
---
config:
sankey:
showValues: false
---
sankey-beta
Agricultural 'waste',Bio-conversion,124.729

View File

@ -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) => {

View File

@ -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[] = [];

View File

@ -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);
});

View File

@ -23,24 +23,14 @@ import { attachFunctions } from './interactionDb.js';
import { log, setLogLevel } from './logger.js';
import getStyles from './styles.js';
import theme from './themes/index.js';
import utils from './utils.js';
import DOMPurify from 'dompurify';
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 { parseDirective } from './directiveUtils.js';
import { extractFrontMatter } from './diagram-api/frontmatter.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';
@ -65,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;
}
@ -98,6 +79,13 @@ export interface RenderResult {
bindFunctions?: (element: Element) => void;
}
function processAndSetConfigs(text: string) {
const processed = preprocessDiagram(text);
configApi.reset();
configApi.addDirective(processed.config ?? {});
return processed;
}
/**
* Parse the text and validate the syntax.
* @param text - The mermaid diagram definition.
@ -108,6 +96,9 @@ export interface RenderResult {
async function parse(text: string, parseOptions?: ParseOptions): Promise<boolean> {
addDiagrams();
text = processAndSetConfigs(text).code;
try {
await getDiagramFromText(text);
} catch (error) {
@ -176,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<string, DiagramStyleClassDef> | null | undefined = {}
): string => {
let cssStyles = '';
@ -204,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
@ -233,10 +222,10 @@ export const createCssStyles = (
export const createUserStyles = (
config: MermaidConfig,
graphType: string,
classDefs: Record<string, DiagramStyleClassDef>,
classDefs: Record<string, DiagramStyleClassDef> | 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
@ -384,18 +373,8 @@ const render = async function (
): Promise<RenderResult> {
addDiagrams();
configApi.reset();
// We need to add the directives before creating the diagram.
// So extractFrontMatter is called twice. Once here and once in the diagram parser.
// This can be fixed in a future refactor.
extractFrontMatter(text, {}, configApi.addDirective);
// Add Directives.
const graphInit = utils.detectInit(text);
if (graphInit) {
configApi.addDirective(graphInit);
}
const processed = processAndSetConfigs(text);
text = processed.code;
const config = configApi.getConfig();
log.debug(config);
@ -405,15 +384,6 @@ const render = async function (
text = MAX_TEXTLENGTH_EXCEEDED_MSG;
}
// clean up text CRLFs
text = text.replace(/\r\n?/g, '\n'); // parser problems on CRLF ignore all CR and leave LF;;
// clean up html tags so that all attributes use single quotes, parser throws error on double quotes
text = text.replace(
/<(\w+)([^>]*)>/g,
(match, tag, attributes) => '<' + tag + attributes.replace(/="([^"]*)"/g, "='$1'") + '>'
);
const idSelector = '#' + id;
const iFrameID = 'i' + id;
const iFrameID_selector = '#' + iFrameID;
@ -486,7 +456,7 @@ const render = async function (
let parseEncounteredException;
try {
diag = await getDiagramFromText(text);
diag = await getDiagramFromText(text, { title: processed.title });
} catch (error) {
if (config.suppressErrorRendering) {
removeTempElements();
@ -506,9 +476,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);
@ -685,7 +653,6 @@ function addA11yInfo(
export const mermaidAPI = Object.freeze({
render,
parse,
parseDirective,
getDiagramFromText,
initialize,
getConfig: configApi.getConfig,

View File

@ -0,0 +1,65 @@
import { cleanupComments } from './diagram-api/comments.js';
import { extractFrontMatter } from './diagram-api/frontmatter.js';
import type { DiagramMetadata } from './diagram-api/types.js';
import utils, { cleanAndMerge, removeDirectives } from './utils.js';
const cleanupText = (code: string) => {
return (
code
// parser problems on CRLF ignore all CR and leave LF;;
.replace(/\r\n?/g, '\n')
// clean up html tags so that all attributes use single quotes, parser throws error on double quotes
.replace(
/<(\w+)([^>]*)>/g,
(match, tag, attributes) => '<' + tag + attributes.replace(/="([^"]*)"/g, "='$1'") + '>'
)
);
};
const processFrontmatter = (code: string) => {
const { text, metadata } = extractFrontMatter(code);
const { displayMode, title, config = {} } = metadata;
if (displayMode) {
// Needs to be supported for legacy reasons
if (!config.gantt) {
config.gantt = {};
}
config.gantt.displayMode = displayMode;
}
return { title, config, text };
};
const processDirectives = (code: string) => {
const initDirective = utils.detectInit(code) ?? {};
const wrapDirectives = utils.detectDirective(code, 'wrap');
if (Array.isArray(wrapDirectives)) {
initDirective.wrap = wrapDirectives.some(({ type }) => {
type === 'wrap';
});
} else if (wrapDirectives?.type === 'wrap') {
initDirective.wrap = true;
}
return {
text: removeDirectives(code),
directive: initDirective,
};
};
/**
* Preprocess the given code by cleaning it up, extracting front matter and directives,
* cleaning and merging configuration, and removing comments.
* @param code - The code to preprocess.
* @returns The object containing the preprocessed code, title, and configuration.
*/
export function preprocessDiagram(code: string): DiagramMetadata & { code: string } {
const cleanedCode = cleanupText(code);
const frontMatterResult = processFrontmatter(cleanedCode);
const directiveResult = processDirectives(frontMatterResult.text);
const config = cleanAndMerge(frontMatterResult.config, directiveResult.directive);
code = cleanupComments(directiveResult.text);
return {
code,
title: frontMatterResult.title,
config,
};
}

View File

@ -1530,10 +1530,10 @@ $defs: # JSON Schema definition (maybe we should move these to a seperate file)
Pattern is:
```javascript
/^([1-9][0-9]*)(minute|hour|day|week|month)$/
/^([1-9][0-9]*)(millisecond|second|minute|hour|day|week|month)$/
```
type: string
pattern: ^([1-9][0-9]*)(minute|hour|day|week|month)$
pattern: ^([1-9][0-9]*)(millisecond|second|minute|hour|day|week|month)$
topAxis:
description: |
When this flag is set, date labels will be added to the top of the chart

View File

@ -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;
}

View File

@ -1,10 +1,11 @@
import { vi } from 'vitest';
import utils, { cleanAndMerge } 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';
import memoize from 'lodash-es/memoize.js';
import { MockedD3 } from './tests/MockedD3.js';
import { preprocessDiagram } from './preprocess.js';
addDiagrams();
@ -158,13 +159,38 @@ describe('when detecting chart type ', function () {
const type = detectType(str);
expect(type).toBe('flowchart');
});
it('should handle a wrap directive', () => {
const wrap = { type: 'wrap', args: null };
expect(detectDirective('%%{wrap}%%', 'wrap')).toEqual(wrap);
expect(
detectDirective(
`%%{
wrap
}%%`,
'wrap'
)
).toEqual(wrap);
expect(
detectDirective(
`%%{
wrap
}%%`,
'wrap'
)
).toEqual(wrap);
expect(detectDirective('%%{wrap:}%%', 'wrap')).toEqual(wrap);
expect(detectDirective('%%{wrap: }%%', 'wrap')).toEqual(wrap);
expect(detectDirective('graph', 'wrap')).not.toEqual(wrap);
});
it('should handle an initialize definition', function () {
const str = `
%%{initialize: { 'logLevel': 0, 'theme': 'dark' }}%%
sequenceDiagram
Alice->Bob: hi`;
const type = detectType(str);
const init = utils.detectInit(str);
const init = preprocessDiagram(str).config;
expect(type).toBe('sequence');
expect(init).toEqual({ logLevel: 0, theme: 'dark' });
});
@ -174,7 +200,7 @@ Alice->Bob: hi`;
sequenceDiagram
Alice->Bob: hi`;
const type = detectType(str);
const init = utils.detectInit(str);
const init = preprocessDiagram(str).config;
expect(type).toBe('sequence');
expect(init).toEqual({ logLevel: 0, theme: 'dark' });
});
@ -184,7 +210,7 @@ Alice->Bob: hi`;
sequenceDiagram
Alice->Bob: hi`;
const type = detectType(str);
const init = utils.detectInit(str);
const init = preprocessDiagram(str).config;
expect(type).toBe('sequence');
expect(init).toEqual({ logLevel: 0, theme: 'dark', sequence: { wrap: true } });
});
@ -199,7 +225,7 @@ Alice->Bob: hi`;
sequenceDiagram
Alice->Bob: hi`;
const type = detectType(str);
const init = utils.detectInit(str);
const init = preprocessDiagram(str).config;
expect(type).toBe('sequence');
expect(init).toEqual({ logLevel: 0, theme: 'dark' });
});
@ -214,7 +240,7 @@ Alice->Bob: hi`;
sequenceDiagram
Alice->Bob: hi`;
const type = detectType(str);
const init = utils.detectInit(str);
const init = preprocessDiagram(str).config;
expect(type).toBe('sequence');
expect(init).toEqual({ logLevel: 0, theme: 'dark' });
});
@ -326,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);
@ -334,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();
@ -343,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();
@ -464,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'
);
});
});

View File

@ -1,4 +1,3 @@
// @ts-nocheck : TODO Fix ts errors
import { sanitizeUrl } from '@braintree/sanitize-url';
import type { CurveFactory } from 'd3';
import {
@ -25,7 +24,7 @@ import {
select,
} from 'd3';
import common from './diagrams/common/common.js';
import { configKeys } from './defaultConfig.js';
import { sanitizeDirective } from './utils/sanitizeDirective.js';
import { log } from './logger.js';
import { detectType } from './diagram-api/detectType.js';
import assignWithDepth from './assignWithDepth.js';
@ -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,11 +59,10 @@ const d3CurveTypes = {
curveStep: curveStep,
curveStepAfter: curveStepAfter,
curveStepBefore: curveStepBefore,
};
} as const;
const directiveWithoutOpen =
/\s*(?:(\w+)(?=:):|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi;
/**
* Detects the init config object from the text
*
@ -102,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) {
@ -117,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.
*
@ -155,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`,
@ -166,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) {
@ -184,19 +189,24 @@ 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 };
}
};
export const removeDirectives = function (text: string): string {
return text.replace(directiveRegex, '');
};
/**
* Detects whether a substring in present in a given array
*
@ -228,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;
}
/**
@ -241,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);
}
return url;
if (!url) {
return undefined;
}
if (config.securityLevel !== 'loose') {
return sanitizeUrl(url);
}
return url;
}
/**
@ -256,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;
@ -264,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
*
@ -288,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));
}
/**
@ -298,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) => {
@ -307,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);
}
/**
@ -348,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;
@ -370,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;
@ -409,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 };
const cardinalityPosition: Point = { x: 0, y: 0 };
//Calculation cardinality position using angle, center point on the line/curve but perpendicular and with offset-distance
//Calculation cardinality position using angle, center point on the line/curve but pendicular 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;
}
@ -499,7 +458,7 @@ export function getStylesFromArray(arr: string[]): { style: string; labelStyle:
}
}
return { style: style, labelStyle: labelStyle };
return { style, labelStyle };
}
let cnt = 0;
@ -511,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;
@ -524,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 () {
@ -541,6 +500,7 @@ export const getTextObj = function () {
rx: 0,
ry: 0,
valign: undefined,
text: '',
};
};
@ -571,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);
@ -579,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);
}
@ -598,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;
}
@ -612,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);
@ -697,10 +658,6 @@ export function calculateTextHeight(
text: Parameters<typeof calculateTextDimensions>[0],
config: Parameters<typeof calculateTextDimensions>[1]
): ReturnType<typeof calculateTextDimensions>['height'] {
config = Object.assign(
{ fontSize: 12, fontWeight: 400, fontFamily: 'Arial', margin: 15 },
config
);
return calculateTextDimensions(text, config).height;
}
@ -716,20 +673,9 @@ export function calculateTextWidth(
text: Parameters<typeof calculateTextDimensions>[0],
config: Parameters<typeof calculateTextDimensions>[1]
): ReturnType<typeof calculateTextDimensions>['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.
@ -744,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 };
}
@ -769,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);
@ -784,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);
}
@ -807,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}
@ -837,102 +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);
};
/**
* Sanitizes directive objects
*
* @param args - Directive's JSON
*/
export const sanitizeDirective = (args: unknown): void => {
log.debug('sanitizeDirective called with', args);
// Return if not an object
if (typeof args !== 'object' || args == null) {
return;
}
// Sanitize each element if an array
if (Array.isArray(args)) {
args.forEach((arg) => sanitizeDirective(arg));
return;
}
// Sanitize each key if an object
for (const key of Object.keys(args)) {
log.debug('Checking key', key);
if (
key.startsWith('__') ||
key.includes('proto') ||
key.includes('constr') ||
!configKeys.has(key) ||
args[key] == null
) {
log.debug('sanitize deleting key: ', key);
delete args[key];
continue;
}
// Recurse if an object
if (typeof args[key] === 'object') {
log.debug('sanitizing object', key);
sanitizeDirective(args[key]);
continue;
}
const cssMatchers = ['themeCSS', 'fontFamily', 'altFontFamily'];
for (const cssKey of cssMatchers) {
if (key.includes(cssKey)) {
log.debug('sanitizing css option', key);
args[key] = sanitizeCss(args[key]);
}
}
}
if (args.themeVariables) {
for (const k of Object.keys(args.themeVariables)) {
const val = args.themeVariables[k];
if (val?.match && !val.match(/^[\d "#%(),.;A-Za-z]+$/)) {
args.themeVariables[k] = '';
}
}
}
log.debug('After sanitization', args);
};
export const sanitizeCss = (str: string): string => {
let startCnt = 0;
let endCnt = 0;
for (const element of str) {
if (startCnt < endCnt) {
return '{ /* ERROR: Unbalanced CSS */ }';
}
if (element === '{') {
startCnt++;
} else if (element === '}') {
endCnt++;
}
}
if (startCnt !== endCnt) {
return '{ /* ERROR: Unbalanced CSS */ }';
}
// Todo add more checks here
return str;
// 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;
}
@ -953,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
@ -961,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)
@ -984,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];
@ -1020,9 +884,7 @@ export default {
random,
runFunc,
entityDecode,
initIdGenerator,
sanitizeDirective,
sanitizeCss,
insertTitle,
parseFontSize,
InitIDGenerator,
};

View File

@ -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<EdgeData, 'arrowTypeStart' | 'arrowTypeEnd'>
) => {
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;
},
};
};

View File

@ -0,0 +1,84 @@
import { configKeys } from '../defaultConfig.js';
import { log } from '../logger.js';
/**
* Sanitizes directive objects
*
* @param args - Directive's JSON
*/
export const sanitizeDirective = (args: any): void => {
log.debug('sanitizeDirective called with', args);
// Return if not an object
if (typeof args !== 'object' || args == null) {
return;
}
// Sanitize each element if an array
if (Array.isArray(args)) {
args.forEach((arg) => sanitizeDirective(arg));
return;
}
// Sanitize each key if an object
for (const key of Object.keys(args)) {
log.debug('Checking key', key);
if (
key.startsWith('__') ||
key.includes('proto') ||
key.includes('constr') ||
!configKeys.has(key) ||
args[key] == null
) {
log.debug('sanitize deleting key: ', key);
delete args[key];
continue;
}
// Recurse if an object
if (typeof args[key] === 'object') {
log.debug('sanitizing object', key);
sanitizeDirective(args[key]);
continue;
}
const cssMatchers = ['themeCSS', 'fontFamily', 'altFontFamily'];
for (const cssKey of cssMatchers) {
if (key.includes(cssKey)) {
log.debug('sanitizing css option', key);
args[key] = sanitizeCss(args[key]);
}
}
}
if (args.themeVariables) {
for (const k of Object.keys(args.themeVariables)) {
const val = args.themeVariables[k];
if (val?.match && !val.match(/^[\d "#%(),.;A-Za-z]+$/)) {
args.themeVariables[k] = '';
}
}
}
log.debug('After sanitization', args);
};
export const sanitizeCss = (str: string): string => {
let startCnt = 0;
let endCnt = 0;
for (const element of str) {
if (startCnt < endCnt) {
return '{ /* ERROR: Unbalanced CSS */ }';
}
if (element === '{') {
startCnt++;
} else if (element === '}') {
endCnt++;
}
}
if (startCnt !== endCnt) {
return '{ /* ERROR: Unbalanced CSS */ }';
}
// Todo add more checks here
return str;
};

19
pnpm-lock.yaml generated
View File

@ -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: