Merge branch 'develop' into fix-connection-gaps-in-flowchart-shapes

This commit is contained in:
omkarht
2025-08-04 17:06:03 +05:30
committed by GitHub
73 changed files with 5308 additions and 2096 deletions

View File

@@ -0,0 +1,14 @@
# @mermaid-js/examples
## 1.0.0
### Minor Changes
- [#6453](https://github.com/mermaid-js/mermaid/pull/6453) [`4936ef5`](https://github.com/mermaid-js/mermaid/commit/4936ef5c306d2f892cca9a95a5deac4af6d4882b) Thanks [@sidharthv96](https://github.com/sidharthv96)! - feat: Add examples for diagrams in the `@mermaid-js/examples` package
### Patch Changes
- [#6510](https://github.com/mermaid-js/mermaid/pull/6510) [`7a38eb7`](https://github.com/mermaid-js/mermaid/commit/7a38eb715d795cd5c66cb59357d64ec197b432e6) Thanks [@sidharthv96](https://github.com/sidharthv96)! - chore: Move packet diagram out of beta
- Updated dependencies [[`5acbd7e`](https://github.com/mermaid-js/mermaid/commit/5acbd7e762469d9d89a9c77faf6617ee13367f3a), [`d90634b`](https://github.com/mermaid-js/mermaid/commit/d90634bf2b09e586b055729e07e9a1a31b21827c), [`7a38eb7`](https://github.com/mermaid-js/mermaid/commit/7a38eb715d795cd5c66cb59357d64ec197b432e6), [`3e3ae08`](https://github.com/mermaid-js/mermaid/commit/3e3ae089305e1c7b9948b9e149eba6854fe7f2d6), [`d3e2be3`](https://github.com/mermaid-js/mermaid/commit/d3e2be35be066adeb7fd502b4a24c223c3b53947), [`637680d`](https://github.com/mermaid-js/mermaid/commit/637680d4d9e39b4f8cb6f05b4cb261e8f5693ac3)]:
- mermaid@11.9.0

View File

@@ -0,0 +1,41 @@
# @mermaid-js/examples
The `@mermaid-js/examples` package contains a collection of examples used by tools like [mermaid.live](https://mermaid.live) to help users get started with new diagrams.
You can duplicate an existing diagram example file, e.g., `packages/examples/src/examples/flowchart.ts`, and modify it with details specific to your diagram.
Then, import the example in the `packages/examples/src/index.ts` file and add it to the `examples` array.
Each diagram should have at least one example, which should be marked as the default. It's a good idea to add more examples to showcase different features of the diagram.
## Usage
```bash
pnpm add @mermaid-js/examples
```
A sample usage of the package in mermaid.live, to get the default example for every diagram type:
```ts
import { diagramData } from '@mermaid-js/examples';
type DiagramDefinition = (typeof diagramData)[number];
const isValidDiagram = (diagram: DiagramDefinition): diagram is Required<DiagramDefinition> => {
return Boolean(diagram.name && diagram.examples && diagram.examples.length > 0);
};
export const getSampleDiagrams = () => {
const diagrams = diagramData
.filter((d) => isValidDiagram(d))
.map(({ examples, ...rest }) => ({
...rest,
example: examples?.filter(({ isDefault }) => isDefault)[0],
}));
const examples: Record<string, string> = {};
for (const diagram of diagrams) {
examples[diagram.name.replace(/ (Diagram|Chart|Graph)/, '')] = diagram.example.code;
}
return examples;
};
```

View File

@@ -1,6 +1,6 @@
{
"name": "@mermaid-js/examples",
"version": "0.0.1-beta.1",
"version": "1.0.0",
"description": "Mermaid examples package",
"author": "Sidharth Vinod",
"type": "module",
@@ -16,6 +16,10 @@
"files": [
"dist"
],
"repository": {
"type": "git",
"url": "https://github.com/mermaid-js/mermaid"
},
"scripts": {
"clean": "rimraf dist"
},

View File

@@ -1 +0,0 @@
../mermaid/src/docs/syntax/zenuml.md

View File

@@ -0,0 +1,384 @@
# @mermaid-js/mermaid-zenuml
MermaidJS plugin for ZenUML integration - A powerful sequence diagram rendering engine.
> A Sequence diagram is an interaction diagram that shows how processes operate with one another and in what order.
Mermaid can render sequence diagrams with [ZenUML](https://zenuml.com). Note that ZenUML uses a different
syntax than the original Sequence Diagram in mermaid.
```mermaid
zenuml
BookLibService.Borrow(id) {
User = Session.GetUser()
if(User.isActive) {
try {
BookRepository.Update(id, onLoan, User)
receipt = new Receipt(id, dueDate)
} catch (BookNotFoundException) {
ErrorService.onException(BookNotFoundException)
} finally {
Connection.close()
}
}
return receipt
}
```
## Installation
### With bundlers
```sh
npm install @mermaid-js/mermaid-zenuml
```
```ts
import mermaid from 'mermaid';
import zenuml from '@mermaid-js/mermaid-zenuml';
await mermaid.registerExternalDiagrams([zenuml]);
```
### With CDN
```html
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
import zenuml from 'https://cdn.jsdelivr.net/npm/@mermaid-js/mermaid-zenuml@0.2.0/dist/mermaid-zenuml.core.mjs';
await mermaid.registerExternalDiagrams([zenuml]);
</script>
```
> [!NOTE]
> ZenUML uses experimental lazy loading & async rendering features which could change in the future.
## Basic Usage
Once the plugin is registered, you can create ZenUML diagrams using the `zenuml` syntax:
```mermaid
zenuml
Controller.Get(id) {
Service.Get(id) {
item = Repository.Get(id)
if(item) {
return item
} else {
return null
}
}
return result
}
```
## ZenUML Syntax Reference
### Participants
The participants can be defined implicitly as in the first example on this page. The participants or actors are
rendered in order of appearance in the diagram source text. Sometimes you might want to show the participants in a
different order than how they appear in the first message. It is possible to specify the actor's order of
appearance by doing the following:
```mermaid
zenuml
title Declare participant (optional)
Bob
Alice
Alice->Bob: Hi Bob
Bob->Alice: Hi Alice
```
### Annotators
If you specifically want to use symbols instead of just rectangles with text you can do so by using the annotator syntax to declare participants as per below.
```mermaid
zenuml
title Annotators
@Actor Alice
@Database Bob
Alice->Bob: Hi Bob
Bob->Alice: Hi Alice
```
Available annotators include:
- `@Actor` - Human figure
- `@Database` - Database symbol
- `@Boundary` - Boundary symbol
- `@Control` - Control symbol
- `@Entity` - Entity symbol
- `@Queue` - Queue symbol
### Aliases
The participants can have a convenient identifier and a descriptive label.
```mermaid
zenuml
title Aliases
A as Alice
J as John
A->J: Hello John, how are you?
J->A: Great!
```
## Messages
Messages can be one of:
1. Sync message
2. Async message
3. Creation message
4. Reply message
### Sync message
You can think of a sync (blocking) method in a programming language.
```mermaid
zenuml
title Sync message
A.SyncMessage
A.SyncMessage(with, parameters) {
B.nestedSyncMessage()
}
```
### Async message
You can think of an async (non-blocking) method in a programming language. Fire an event and forget about it.
```mermaid
zenuml
title Async message
Alice->Bob: How are you?
```
### Creation message
We use `new` keyword to create an object.
```mermaid
zenuml
new A1
new A2(with, parameters)
```
### Reply message
There are three ways to express a reply message:
```mermaid
zenuml
// 1. assign a variable from a sync message.
a = A.SyncMessage()
// 1.1. optionally give the variable a type
SomeType a = A.SyncMessage()
// 2. use return keyword
A.SyncMessage() {
return result
}
// 3. use @return or @reply annotator on an async message
@return
A->B: result
```
The third way `@return` is rarely used, but it is useful when you want to return to one level up.
```mermaid
zenuml
title Reply message
Client->A.method() {
B.method() {
if(condition) {
return x1
// return early
@return
A->Client: x11
}
}
return x2
}
```
## Advanced Features
### Nesting
Sync messages and Creation messages are naturally nestable with `{}`.
```mermaid
zenuml
A.method() {
B.nested_sync_method()
B->C: nested async message
}
```
### Comments
It is possible to add comments to a sequence diagram with `// comment` syntax.
Comments will be rendered above the messages or fragments. Comments on other places
are ignored. Markdown is supported.
```mermaid
zenuml
// a comment on a participant will not be rendered
BookService
// a comment on a message.
// **Markdown** is supported.
BookService.getBook()
```
### Loops
It is possible to express loops in a ZenUML diagram. This is done by any of the
following notations:
1. while
2. for
3. forEach, foreach
4. loop
```zenuml
while(condition) {
...statements...
}
```
Example:
```mermaid
zenuml
Alice->John: Hello John, how are you?
while(true) {
John->Alice: Great!
}
```
### Alt (Alternative paths)
It is possible to express alternative paths in a sequence diagram. This is done by the notation
```zenuml
if(condition1) {
...statements...
} else if(condition2) {
...statements...
} else {
...statements...
}
```
Example:
```mermaid
zenuml
Alice->Bob: Hello Bob, how are you?
if(is_sick) {
Bob->Alice: Not so good :(
} else {
Bob->Alice: Feeling fresh like a daisy
}
```
### Opt (Optional)
It is possible to render an `opt` fragment. This is done by the notation
```zenuml
opt {
...statements...
}
```
Example:
```mermaid
zenuml
Alice->Bob: Hello Bob, how are you?
Bob->Alice: Not so good :(
opt {
Bob->Alice: Thanks for asking
}
```
### Parallel
It is possible to show actions that are happening in parallel.
This is done by the notation
```zenuml
par {
statement1
statement2
statement3
}
```
Example:
```mermaid
zenuml
par {
Alice->Bob: Hello guys!
Alice->John: Hello guys!
}
```
### Try/Catch/Finally (Break)
It is possible to indicate a stop of the sequence within the flow (usually used to model exceptions).
This is done by the notation
```
try {
...statements...
} catch {
...statements...
} finally {
...statements...
}
```
Example:
```mermaid
zenuml
try {
Consumer->API: Book something
API->BookingService: Start booking process
} catch {
API->Consumer: show failure
} finally {
API->BookingService: rollback status
}
```
## Contributing
This package is part of the [Mermaid](https://github.com/mermaid-js/mermaid) project. See the main repository for contributing guidelines.
## Contributors
- [Peng Xiao](https://github.com/MrCoder)
- [Sidharth Vinod](https://sidharth.dev)
- [Dong Cai](https://github.com/dontry)
## License
MIT
## Links
- [ZenUML Official Website](https://zenuml.com)
- [Mermaid Documentation](https://mermaid.js.org)
- [GitHub Repository](https://github.com/mermaid-js/mermaid)

View File

@@ -33,7 +33,7 @@
],
"license": "MIT",
"dependencies": {
"@zenuml/core": "^3.31.1"
"@zenuml/core": "^3.35.2"
},
"devDependencies": {
"mermaid": "workspace:^"

11
packages/mermaid-zenuml/src/zenuml.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
declare module '@zenuml/core' {
interface RenderOptions {
theme?: string;
mode?: string;
}
export default class ZenUml {
constructor(container: Element);
render(text: string, options?: RenderOptions): Promise<void>;
}
}

View File

@@ -53,7 +53,6 @@ export const draw = async function (text: string, id: string) {
const { foreignObject, container, app } = createForeignObject(id);
svgContainer.appendChild(foreignObject);
// @ts-expect-error @zenuml/core@3.0.0 exports the wrong type for ZenUml
const zenuml = new ZenUml(app);
// default is a theme name. More themes to be added and will be configurable in the future
await zenuml.render(text, { theme: 'default', mode: 'static' });

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -78,5 +78,41 @@ describe('diagram-orchestration', () => {
flowchart: 1 "pie" pie: 2 "pie"`)
).toBe('pie');
});
it('should detect proper diagram when defaultRenderer is elk for flowchart', () => {
expect(
detectType('mindmap\n root\n Photograph\n Waterfall', {
flowchart: { defaultRenderer: 'elk' },
})
).toBe('mindmap');
expect(
detectType(
`
classDiagram
class Person {
+String name
-Int id
#double age
+Text demographicProfile
}
`,
{ flowchart: { defaultRenderer: 'elk' } }
)
).toBe('class');
expect(
detectType(
`
erDiagram
p[Photograph] {
varchar(12) jobId
date dateCreated
}
`,
{
flowchart: { defaultRenderer: 'elk' },
}
)
).toBe('er');
});
});
});

View File

@@ -1,21 +1,12 @@
import { it, describe, expect } from 'vitest';
import { db } from './architectureDb.js';
import { parser } from './architectureParser.js';
const {
clear,
getDiagramTitle,
getAccTitle,
getAccDescription,
getServices,
getGroups,
getEdges,
getJunctions,
} = db;
import { ArchitectureDB } from './architectureDb.js';
describe('architecture diagrams', () => {
let db: ArchitectureDB;
beforeEach(() => {
clear();
db = new ArchitectureDB();
// @ts-expect-error since type is set to undefined we will have error
parser.parser?.yy = db;
});
describe('architecture diagram definitions', () => {
@@ -36,7 +27,7 @@ describe('architecture diagrams', () => {
it('should handle title on the first line', async () => {
const str = `architecture-beta title Simple Architecture Diagram`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getDiagramTitle()).toBe('Simple Architecture Diagram');
expect(db.getDiagramTitle()).toBe('Simple Architecture Diagram');
});
it('should handle title on another line', async () => {
@@ -44,7 +35,7 @@ describe('architecture diagrams', () => {
title Simple Architecture Diagram
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getDiagramTitle()).toBe('Simple Architecture Diagram');
expect(db.getDiagramTitle()).toBe('Simple Architecture Diagram');
});
it('should handle accessibility title and description', async () => {
@@ -53,8 +44,8 @@ describe('architecture diagrams', () => {
accDescr: Accessibility Description
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getAccTitle()).toBe('Accessibility Title');
expect(getAccDescription()).toBe('Accessibility Description');
expect(db.getAccTitle()).toBe('Accessibility Title');
expect(db.getAccDescription()).toBe('Accessibility Description');
});
it('should handle multiline accessibility description', async () => {
@@ -64,7 +55,7 @@ describe('architecture diagrams', () => {
}
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getAccDescription()).toBe('Accessibility Description');
expect(db.getAccDescription()).toBe('Accessibility Description');
});
});
});

View File

@@ -1,8 +1,9 @@
import { getConfig as commonGetConfig } from '../../config.js';
import type { ArchitectureDiagramConfig } from '../../config.type.js';
import DEFAULT_CONFIG from '../../defaultConfig.js';
import { getConfig as commonGetConfig } from '../../config.js';
import type { DiagramDB } from '../../diagram-api/types.js';
import type { D3Element } from '../../types.js';
import { ImperativeState } from '../../utils/imperativeState.js';
import { cleanAndMerge } from '../../utils.js';
import {
clear as commonClear,
getAccDescription,
@@ -14,7 +15,6 @@ import {
} from '../common/commonDb.js';
import type {
ArchitectureAlignment,
ArchitectureDB,
ArchitectureDirectionPair,
ArchitectureDirectionPairMap,
ArchitectureEdge,
@@ -33,330 +33,333 @@ import {
isArchitectureService,
shiftPositionByArchitectureDirectionPair,
} from './architectureTypes.js';
import { cleanAndMerge } from '../../utils.js';
const DEFAULT_ARCHITECTURE_CONFIG: Required<ArchitectureDiagramConfig> =
DEFAULT_CONFIG.architecture;
export class ArchitectureDB implements DiagramDB {
private nodes: Record<string, ArchitectureNode> = {};
private groups: Record<string, ArchitectureGroup> = {};
private edges: ArchitectureEdge[] = [];
private registeredIds: Record<string, 'node' | 'group'> = {};
private dataStructures?: ArchitectureState['dataStructures'];
private elements: Record<string, D3Element> = {};
const state = new ImperativeState<ArchitectureState>(() => ({
nodes: {},
groups: {},
edges: [],
registeredIds: {},
config: DEFAULT_ARCHITECTURE_CONFIG,
dataStructures: undefined,
elements: {},
}));
const clear = (): void => {
state.reset();
commonClear();
};
const addService = function ({
id,
icon,
in: parent,
title,
iconText,
}: Omit<ArchitectureService, 'edges'>) {
if (state.records.registeredIds[id] !== undefined) {
throw new Error(
`The service id [${id}] is already in use by another ${state.records.registeredIds[id]}`
);
}
if (parent !== undefined) {
if (id === parent) {
throw new Error(`The service [${id}] cannot be placed within itself`);
}
if (state.records.registeredIds[parent] === undefined) {
throw new Error(
`The service [${id}]'s parent does not exist. Please make sure the parent is created before this service`
);
}
if (state.records.registeredIds[parent] === 'node') {
throw new Error(`The service [${id}]'s parent is not a group`);
}
constructor() {
this.clear();
}
state.records.registeredIds[id] = 'node';
public clear(): void {
this.nodes = {};
this.groups = {};
this.edges = [];
this.registeredIds = {};
this.dataStructures = undefined;
this.elements = {};
commonClear();
}
state.records.nodes[id] = {
public addService({
id,
type: 'service',
icon,
in: parent,
title,
iconText,
title,
edges: [],
in: parent,
};
};
const getServices = (): ArchitectureService[] =>
Object.values(state.records.nodes).filter<ArchitectureService>(isArchitectureService);
const addJunction = function ({ id, in: parent }: Omit<ArchitectureJunction, 'edges'>) {
state.records.registeredIds[id] = 'node';
state.records.nodes[id] = {
id,
type: 'junction',
edges: [],
in: parent,
};
};
const getJunctions = (): ArchitectureJunction[] =>
Object.values(state.records.nodes).filter<ArchitectureJunction>(isArchitectureJunction);
const getNodes = (): ArchitectureNode[] => Object.values(state.records.nodes);
const getNode = (id: string): ArchitectureNode | null => state.records.nodes[id];
const addGroup = function ({ id, icon, in: parent, title }: ArchitectureGroup) {
if (state.records.registeredIds[id] !== undefined) {
throw new Error(
`The group id [${id}] is already in use by another ${state.records.registeredIds[id]}`
);
}
if (parent !== undefined) {
if (id === parent) {
throw new Error(`The group [${id}] cannot be placed within itself`);
}
if (state.records.registeredIds[parent] === undefined) {
}: Omit<ArchitectureService, 'edges'>): void {
if (this.registeredIds[id] !== undefined) {
throw new Error(
`The group [${id}]'s parent does not exist. Please make sure the parent is created before this group`
`The service id [${id}] is already in use by another ${this.registeredIds[id]}`
);
}
if (state.records.registeredIds[parent] === 'node') {
throw new Error(`The group [${id}]'s parent is not a group`);
if (parent !== undefined) {
if (id === parent) {
throw new Error(`The service [${id}] cannot be placed within itself`);
}
if (this.registeredIds[parent] === undefined) {
throw new Error(
`The service [${id}]'s parent does not exist. Please make sure the parent is created before this service`
);
}
if (this.registeredIds[parent] === 'node') {
throw new Error(`The service [${id}]'s parent is not a group`);
}
}
this.registeredIds[id] = 'node';
this.nodes[id] = {
id,
type: 'service',
icon,
iconText,
title,
edges: [],
in: parent,
};
}
state.records.registeredIds[id] = 'group';
state.records.groups[id] = {
id,
icon,
title,
in: parent,
};
};
const getGroups = (): ArchitectureGroup[] => {
return Object.values(state.records.groups);
};
const addEdge = function ({
lhsId,
rhsId,
lhsDir,
rhsDir,
lhsInto,
rhsInto,
lhsGroup,
rhsGroup,
title,
}: ArchitectureEdge<string>) {
if (!isArchitectureDirection(lhsDir)) {
throw new Error(
`Invalid direction given for left hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${lhsDir}`
);
}
if (!isArchitectureDirection(rhsDir)) {
throw new Error(
`Invalid direction given for right hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${rhsDir}`
);
public getServices(): ArchitectureService[] {
return Object.values(this.nodes).filter(isArchitectureService);
}
if (state.records.nodes[lhsId] === undefined && state.records.groups[lhsId] === undefined) {
throw new Error(
`The left-hand id [${lhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`
);
}
if (state.records.nodes[rhsId] === undefined && state.records.groups[lhsId] === undefined) {
throw new Error(
`The right-hand id [${rhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`
);
public addJunction({ id, in: parent }: Omit<ArchitectureJunction, 'edges'>): void {
this.registeredIds[id] = 'node';
this.nodes[id] = {
id,
type: 'junction',
edges: [],
in: parent,
};
}
const lhsGroupId = state.records.nodes[lhsId].in;
const rhsGroupId = state.records.nodes[rhsId].in;
if (lhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) {
throw new Error(
`The left-hand id [${lhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.`
);
}
if (rhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) {
throw new Error(
`The right-hand id [${rhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.`
);
public getJunctions(): ArchitectureJunction[] {
return Object.values(this.nodes).filter(isArchitectureJunction);
}
const edge = {
public getNodes(): ArchitectureNode[] {
return Object.values(this.nodes);
}
public getNode(id: string): ArchitectureNode | null {
return this.nodes[id] ?? null;
}
public addGroup({ id, icon, in: parent, title }: ArchitectureGroup): void {
if (this.registeredIds?.[id] !== undefined) {
throw new Error(
`The group id [${id}] is already in use by another ${this.registeredIds[id]}`
);
}
if (parent !== undefined) {
if (id === parent) {
throw new Error(`The group [${id}] cannot be placed within itself`);
}
if (this.registeredIds?.[parent] === undefined) {
throw new Error(
`The group [${id}]'s parent does not exist. Please make sure the parent is created before this group`
);
}
if (this.registeredIds?.[parent] === 'node') {
throw new Error(`The group [${id}]'s parent is not a group`);
}
}
this.registeredIds[id] = 'group';
this.groups[id] = {
id,
icon,
title,
in: parent,
};
}
public getGroups(): ArchitectureGroup[] {
return Object.values(this.groups);
}
public addEdge({
lhsId,
lhsDir,
lhsInto,
lhsGroup,
rhsId,
lhsDir,
rhsDir,
lhsInto,
rhsInto,
lhsGroup,
rhsGroup,
title,
};
state.records.edges.push(edge);
if (state.records.nodes[lhsId] && state.records.nodes[rhsId]) {
state.records.nodes[lhsId].edges.push(state.records.edges[state.records.edges.length - 1]);
state.records.nodes[rhsId].edges.push(state.records.edges[state.records.edges.length - 1]);
}
};
const getEdges = (): ArchitectureEdge[] => state.records.edges;
/**
* Returns the current diagram's adjacency list, spatial map, & group alignments.
* If they have not been created, run the algorithms to generate them.
* @returns
*/
const getDataStructures = () => {
if (state.records.dataStructures === undefined) {
// Tracks how groups are aligned with one another. Generated while creating the adj list
const groupAlignments: Record<
string,
Record<string, Exclude<ArchitectureAlignment, 'bend'>>
> = {};
// Create an adjacency list of the diagram to perform BFS on
// Outer reduce applied on all services
// Inner reduce applied on the edges for a service
const adjList = Object.entries(state.records.nodes).reduce<
Record<string, ArchitectureDirectionPairMap>
>((prevOuter, [id, service]) => {
prevOuter[id] = service.edges.reduce<ArchitectureDirectionPairMap>((prevInner, edge) => {
// track the direction groups connect to one another
const lhsGroupId = getNode(edge.lhsId)?.in;
const rhsGroupId = getNode(edge.rhsId)?.in;
if (lhsGroupId && rhsGroupId && lhsGroupId !== rhsGroupId) {
const alignment = getArchitectureDirectionAlignment(edge.lhsDir, edge.rhsDir);
if (alignment !== 'bend') {
groupAlignments[lhsGroupId] ??= {};
groupAlignments[lhsGroupId][rhsGroupId] = alignment;
groupAlignments[rhsGroupId] ??= {};
groupAlignments[rhsGroupId][lhsGroupId] = alignment;
}
}
if (edge.lhsId === id) {
// source is LHS
const pair = getArchitectureDirectionPair(edge.lhsDir, edge.rhsDir);
if (pair) {
prevInner[pair] = edge.rhsId;
}
} else {
// source is RHS
const pair = getArchitectureDirectionPair(edge.rhsDir, edge.lhsDir);
if (pair) {
prevInner[pair] = edge.lhsId;
}
}
return prevInner;
}, {});
return prevOuter;
}, {});
// Configuration for the initial pass of BFS
const firstId = Object.keys(adjList)[0];
const visited = { [firstId]: 1 };
// If a key is present in this object, it has not been visited
const notVisited = Object.keys(adjList).reduce(
(prev, id) => (id === firstId ? prev : { ...prev, [id]: 1 }),
{} as Record<string, number>
);
// Perform BFS on the adjacency list
const BFS = (startingId: string): ArchitectureSpatialMap => {
const spatialMap = { [startingId]: [0, 0] };
const queue = [startingId];
while (queue.length > 0) {
const id = queue.shift();
if (id) {
visited[id] = 1;
delete notVisited[id];
const adj = adjList[id];
const [posX, posY] = spatialMap[id];
Object.entries(adj).forEach(([dir, rhsId]) => {
if (!visited[rhsId]) {
spatialMap[rhsId] = shiftPositionByArchitectureDirectionPair(
[posX, posY],
dir as ArchitectureDirectionPair
);
queue.push(rhsId);
}
});
}
}
return spatialMap;
};
const spatialMaps = [BFS(firstId)];
// If our diagram is disconnected, keep adding additional spatial maps until all disconnected graphs have been found
while (Object.keys(notVisited).length > 0) {
spatialMaps.push(BFS(Object.keys(notVisited)[0]));
}: ArchitectureEdge): void {
if (!isArchitectureDirection(lhsDir)) {
throw new Error(
`Invalid direction given for left hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${String(lhsDir)}`
);
}
state.records.dataStructures = {
adjList,
spatialMaps,
groupAlignments,
if (!isArchitectureDirection(rhsDir)) {
throw new Error(
`Invalid direction given for right hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${String(rhsDir)}`
);
}
if (this.nodes[lhsId] === undefined && this.groups[lhsId] === undefined) {
throw new Error(
`The left-hand id [${lhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`
);
}
if (this.nodes[rhsId] === undefined && this.groups[rhsId] === undefined) {
throw new Error(
`The right-hand id [${rhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`
);
}
const lhsGroupId = this.nodes[lhsId].in;
const rhsGroupId = this.nodes[rhsId].in;
if (lhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) {
throw new Error(
`The left-hand id [${lhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.`
);
}
if (rhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) {
throw new Error(
`The right-hand id [${rhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.`
);
}
const edge = {
lhsId,
lhsDir,
lhsInto,
lhsGroup,
rhsId,
rhsDir,
rhsInto,
rhsGroup,
title,
};
this.edges.push(edge);
if (this.nodes[lhsId] && this.nodes[rhsId]) {
this.nodes[lhsId].edges.push(this.edges[this.edges.length - 1]);
this.nodes[rhsId].edges.push(this.edges[this.edges.length - 1]);
}
}
return state.records.dataStructures;
};
const setElementForId = (id: string, element: D3Element) => {
state.records.elements[id] = element;
};
const getElementById = (id: string) => state.records.elements[id];
public getEdges(): ArchitectureEdge[] {
return this.edges;
}
const getConfig = (): Required<ArchitectureDiagramConfig> => {
const config = cleanAndMerge({
...DEFAULT_ARCHITECTURE_CONFIG,
...commonGetConfig().architecture,
});
return config;
};
/**
* Returns the current diagram's adjacency list, spatial map, & group alignments.
* If they have not been created, run the algorithms to generate them.
* @returns
*/
public getDataStructures() {
if (this.dataStructures === undefined) {
// Tracks how groups are aligned with one another. Generated while creating the adj list
const groupAlignments: Record<
string,
Record<string, Exclude<ArchitectureAlignment, 'bend'>>
> = {};
export const db: ArchitectureDB = {
clear,
setDiagramTitle,
getDiagramTitle,
setAccTitle,
getAccTitle,
setAccDescription,
getAccDescription,
getConfig,
// Create an adjacency list of the diagram to perform BFS on
// Outer reduce applied on all services
// Inner reduce applied on the edges for a service
const adjList = Object.entries(this.nodes).reduce<
Record<string, ArchitectureDirectionPairMap>
>((prevOuter, [id, service]) => {
prevOuter[id] = service.edges.reduce<ArchitectureDirectionPairMap>((prevInner, edge) => {
// track the direction groups connect to one another
const lhsGroupId = this.getNode(edge.lhsId)?.in;
const rhsGroupId = this.getNode(edge.rhsId)?.in;
if (lhsGroupId && rhsGroupId && lhsGroupId !== rhsGroupId) {
const alignment = getArchitectureDirectionAlignment(edge.lhsDir, edge.rhsDir);
if (alignment !== 'bend') {
groupAlignments[lhsGroupId] ??= {};
groupAlignments[lhsGroupId][rhsGroupId] = alignment;
groupAlignments[rhsGroupId] ??= {};
groupAlignments[rhsGroupId][lhsGroupId] = alignment;
}
}
addService,
getServices,
addJunction,
getJunctions,
getNodes,
getNode,
addGroup,
getGroups,
addEdge,
getEdges,
setElementForId,
getElementById,
getDataStructures,
};
if (edge.lhsId === id) {
// source is LHS
const pair = getArchitectureDirectionPair(edge.lhsDir, edge.rhsDir);
if (pair) {
prevInner[pair] = edge.rhsId;
}
} else {
// source is RHS
const pair = getArchitectureDirectionPair(edge.rhsDir, edge.lhsDir);
if (pair) {
prevInner[pair] = edge.lhsId;
}
}
return prevInner;
}, {});
return prevOuter;
}, {});
// Configuration for the initial pass of BFS
const firstId = Object.keys(adjList)[0];
const visited = { [firstId]: 1 };
// If a key is present in this object, it has not been visited
const notVisited = Object.keys(adjList).reduce(
(prev, id) => (id === firstId ? prev : { ...prev, [id]: 1 }),
{} as Record<string, number>
);
// Perform BFS on the adjacency list
const BFS = (startingId: string): ArchitectureSpatialMap => {
const spatialMap = { [startingId]: [0, 0] };
const queue = [startingId];
while (queue.length > 0) {
const id = queue.shift();
if (id) {
visited[id] = 1;
delete notVisited[id];
const adj = adjList[id];
const [posX, posY] = spatialMap[id];
Object.entries(adj).forEach(([dir, rhsId]) => {
if (!visited[rhsId]) {
spatialMap[rhsId] = shiftPositionByArchitectureDirectionPair(
[posX, posY],
dir as ArchitectureDirectionPair
);
queue.push(rhsId);
}
});
}
}
return spatialMap;
};
const spatialMaps = [BFS(firstId)];
// If our diagram is disconnected, keep adding additional spatial maps until all disconnected graphs have been found
while (Object.keys(notVisited).length > 0) {
spatialMaps.push(BFS(Object.keys(notVisited)[0]));
}
this.dataStructures = {
adjList,
spatialMaps,
groupAlignments,
};
}
return this.dataStructures;
}
public setElementForId(id: string, element: D3Element): void {
this.elements[id] = element;
}
public getElementById(id: string): D3Element {
return this.elements[id];
}
public getConfig(): Required<ArchitectureDiagramConfig> {
return cleanAndMerge({
...DEFAULT_ARCHITECTURE_CONFIG,
...commonGetConfig().architecture,
});
}
public getConfigField<T extends keyof ArchitectureDiagramConfig>(
field: T
): Required<ArchitectureDiagramConfig>[T] {
return this.getConfig()[field];
}
public setAccTitle = setAccTitle;
public getAccTitle = getAccTitle;
public setDiagramTitle = setDiagramTitle;
public getDiagramTitle = getDiagramTitle;
public getAccDescription = getAccDescription;
public setAccDescription = setAccDescription;
}
/**
* Typed wrapper for resolving an architecture diagram's config fields. Returns the default value if undefined
* @param field - the config field to access
* @returns
*/
export function getConfigField<T extends keyof ArchitectureDiagramConfig>(
field: T
): Required<ArchitectureDiagramConfig>[T] {
return getConfig()[field];
}
// export function getConfigField<T extends keyof ArchitectureDiagramConfig>(
// field: T
// ): Required<ArchitectureDiagramConfig>[T] {
// return db.getConfig()[field];
// }

View File

@@ -1,12 +1,14 @@
import type { DiagramDefinition } from '../../diagram-api/types.js';
import { parser } from './architectureParser.js';
import { db } from './architectureDb.js';
import { ArchitectureDB } from './architectureDb.js';
import styles from './architectureStyles.js';
import { renderer } from './architectureRenderer.js';
export const diagram: DiagramDefinition = {
parser,
db,
get db() {
return new ArchitectureDB();
},
renderer,
styles,
};

View File

@@ -1,24 +1,33 @@
import type { Architecture } from '@mermaid-js/parser';
import { parse } from '@mermaid-js/parser';
import { log } from '../../logger.js';
import type { ParserDefinition } from '../../diagram-api/types.js';
import { log } from '../../logger.js';
import { populateCommonDb } from '../common/populateCommonDb.js';
import type { ArchitectureDB } from './architectureTypes.js';
import { db } from './architectureDb.js';
import { ArchitectureDB } from './architectureDb.js';
const populateDb = (ast: Architecture, db: ArchitectureDB) => {
populateCommonDb(ast, db);
ast.groups.map(db.addGroup);
ast.groups.map((group) => db.addGroup(group));
ast.services.map((service) => db.addService({ ...service, type: 'service' }));
ast.junctions.map((service) => db.addJunction({ ...service, type: 'junction' }));
// @ts-ignore TODO our parser guarantees the type is L/R/T/B and not string. How to change to union type?
ast.edges.map(db.addEdge);
ast.edges.map((edge) => db.addEdge(edge));
};
export const parser: ParserDefinition = {
parser: {
// @ts-expect-error - ArchitectureDB is not assignable to DiagramDB
yy: undefined,
},
parse: async (input: string): Promise<void> => {
const ast: Architecture = await parse('architecture', input);
log.debug(ast);
const db = parser.parser?.yy;
if (!(db instanceof ArchitectureDB)) {
throw new Error(
'parser.parser?.yy was not a ArchitectureDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.'
);
}
populateDb(ast, db);
},
};

View File

@@ -1,4 +1,3 @@
import { registerIconPacks } from '../../rendering-util/icons.js';
import type { Position } from 'cytoscape';
import cytoscape from 'cytoscape';
import type { FcoseLayoutOptions } from 'cytoscape-fcose';
@@ -7,9 +6,10 @@ import { select } from 'd3';
import type { DrawDefinition, SVG } from '../../diagram-api/types.js';
import type { Diagram } from '../../Diagram.js';
import { log } from '../../logger.js';
import { registerIconPacks } from '../../rendering-util/icons.js';
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
import { getConfigField } from './architectureDb.js';
import type { ArchitectureDB } from './architectureDb.js';
import { architectureIcons } from './architectureIcons.js';
import type {
ArchitectureAlignment,
@@ -22,7 +22,6 @@ import type {
NodeSingularData,
} from './architectureTypes.js';
import {
type ArchitectureDB,
type ArchitectureDirection,
type ArchitectureEdge,
type ArchitectureGroup,
@@ -44,7 +43,7 @@ registerIconPacks([
]);
cytoscape.use(fcose);
function addServices(services: ArchitectureService[], cy: cytoscape.Core) {
function addServices(services: ArchitectureService[], cy: cytoscape.Core, db: ArchitectureDB) {
services.forEach((service) => {
cy.add({
group: 'nodes',
@@ -54,15 +53,15 @@ function addServices(services: ArchitectureService[], cy: cytoscape.Core) {
icon: service.icon,
label: service.title,
parent: service.in,
width: getConfigField('iconSize'),
height: getConfigField('iconSize'),
width: db.getConfigField('iconSize'),
height: db.getConfigField('iconSize'),
} as NodeSingularData,
classes: 'node-service',
});
});
}
function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core) {
function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core, db: ArchitectureDB) {
junctions.forEach((junction) => {
cy.add({
group: 'nodes',
@@ -70,8 +69,8 @@ function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core) {
type: 'junction',
id: junction.id,
parent: junction.in,
width: getConfigField('iconSize'),
height: getConfigField('iconSize'),
width: db.getConfigField('iconSize'),
height: db.getConfigField('iconSize'),
} as NodeSingularData,
classes: 'node-junction',
});
@@ -257,7 +256,8 @@ function getAlignments(
}
function getRelativeConstraints(
spatialMaps: ArchitectureSpatialMap[]
spatialMaps: ArchitectureSpatialMap[],
db: ArchitectureDB
): fcose.FcoseRelativePlacementConstraint[] {
const relativeConstraints: fcose.FcoseRelativePlacementConstraint[] = [];
const posToStr = (pos: number[]) => `${pos[0]},${pos[1]}`;
@@ -296,7 +296,7 @@ function getRelativeConstraints(
[ArchitectureDirectionName[
getOppositeArchitectureDirection(dir as ArchitectureDirection)
]]: currId,
gap: 1.5 * getConfigField('iconSize'),
gap: 1.5 * db.getConfigField('iconSize'),
});
}
});
@@ -353,7 +353,7 @@ function layoutArchitecture(
style: {
'text-valign': 'bottom',
'text-halign': 'center',
'font-size': `${getConfigField('fontSize')}px`,
'font-size': `${db.getConfigField('fontSize')}px`,
},
},
{
@@ -375,7 +375,7 @@ function layoutArchitecture(
selector: '.node-group',
style: {
// @ts-ignore Incorrect library types
padding: `${getConfigField('padding')}px`,
padding: `${db.getConfigField('padding')}px`,
},
},
],
@@ -393,14 +393,14 @@ function layoutArchitecture(
renderEl.remove();
addGroups(groups, cy);
addServices(services, cy);
addJunctions(junctions, cy);
addServices(services, cy, db);
addJunctions(junctions, cy, db);
addEdges(edges, cy);
// Use the spatial map to create alignment arrays for fcose
const alignmentConstraint = getAlignments(db, spatialMaps, groupAlignments);
// Create the relative constraints for fcose by using an inverse of the spatial map and performing BFS on it
const relativePlacementConstraint = getRelativeConstraints(spatialMaps);
const relativePlacementConstraint = getRelativeConstraints(spatialMaps, db);
const layout = cy.layout({
name: 'fcose',
@@ -415,7 +415,9 @@ function layoutArchitecture(
const { parent: parentA } = nodeData(nodeA);
const { parent: parentB } = nodeData(nodeB);
const elasticity =
parentA === parentB ? 1.5 * getConfigField('iconSize') : 0.5 * getConfigField('iconSize');
parentA === parentB
? 1.5 * db.getConfigField('iconSize')
: 0.5 * db.getConfigField('iconSize');
return elasticity;
},
edgeElasticity(edge: EdgeSingular) {
@@ -535,11 +537,11 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram)
const cy = await layoutArchitecture(services, junctions, groups, edges, db, ds);
await drawEdges(edgesElem, cy);
await drawGroups(groupElem, cy);
await drawEdges(edgesElem, cy, db);
await drawGroups(groupElem, cy, db);
positionNodes(db, cy);
setupGraphViewbox(undefined, svg, getConfigField('padding'), getConfigField('useMaxWidth'));
setupGraphViewbox(undefined, svg, db.getConfigField('padding'), db.getConfigField('useMaxWidth'));
};
export const renderer = { draw };

View File

@@ -1,9 +1,9 @@
import { getIconSVG } from '../../rendering-util/icons.js';
import type cytoscape from 'cytoscape';
import { getConfig } from '../../diagram-api/diagramAPI.js';
import { createText } from '../../rendering-util/createText.js';
import { getIconSVG } from '../../rendering-util/icons.js';
import type { D3Element } from '../../types.js';
import { db, getConfigField } from './architectureDb.js';
import type { ArchitectureDB } from './architectureDb.js';
import { architectureIcons } from './architectureIcons.js';
import {
ArchitectureDirectionArrow,
@@ -16,14 +16,17 @@ import {
isArchitectureDirectionY,
isArchitecturePairXY,
nodeData,
type ArchitectureDB,
type ArchitectureJunction,
type ArchitectureService,
} from './architectureTypes.js';
export const drawEdges = async function (edgesEl: D3Element, cy: cytoscape.Core) {
const padding = getConfigField('padding');
const iconSize = getConfigField('iconSize');
export const drawEdges = async function (
edgesEl: D3Element,
cy: cytoscape.Core,
db: ArchitectureDB
) {
const padding = db.getConfigField('padding');
const iconSize = db.getConfigField('iconSize');
const halfIconSize = iconSize / 2;
const arrowSize = iconSize / 6;
const halfArrowSize = arrowSize / 2;
@@ -183,13 +186,17 @@ export const drawEdges = async function (edgesEl: D3Element, cy: cytoscape.Core)
);
};
export const drawGroups = async function (groupsEl: D3Element, cy: cytoscape.Core) {
const padding = getConfigField('padding');
export const drawGroups = async function (
groupsEl: D3Element,
cy: cytoscape.Core,
db: ArchitectureDB
) {
const padding = db.getConfigField('padding');
const groupIconSize = padding * 0.75;
const fontSize = getConfigField('fontSize');
const fontSize = db.getConfigField('fontSize');
const iconSize = getConfigField('iconSize');
const iconSize = db.getConfigField('iconSize');
const halfIconSize = iconSize / 2;
await Promise.all(
@@ -266,7 +273,7 @@ export const drawServices = async function (
): Promise<number> {
for (const service of services) {
const serviceElem = elem.append('g');
const iconSize = getConfigField('iconSize');
const iconSize = db.getConfigField('iconSize');
if (service.title) {
const textElem = serviceElem.append('g');
@@ -350,7 +357,7 @@ export const drawJunctions = function (
) {
junctions.forEach((junction) => {
const junctionElem = elem.append('g');
const iconSize = getConfigField('iconSize');
const iconSize = db.getConfigField('iconSize');
const bkgElem = junctionElem.append('g');
bkgElem

View File

@@ -92,7 +92,20 @@ export const setCssClass = function (itemIds: string, cssClassName: string) {
const populateBlockDatabase = (_blockList: Block[], parent: Block): void => {
const blockList = _blockList.flat();
const children = [];
const columnSettingBlock = blockList.find((b) => b?.type === 'column-setting');
const column = columnSettingBlock?.columns ?? -1;
for (const block of blockList) {
if (
typeof column === 'number' &&
column > 0 &&
block.type !== 'column-setting' &&
typeof block.widthInColumns === 'number' &&
block.widthInColumns > column
) {
log.warn(
`Block ${block.id} width ${block.widthInColumns} exceeds configured column width ${column}`
);
}
if (block.label) {
block.label = sanitizeText(block.label);
}

View File

@@ -270,7 +270,12 @@ function layoutBlocks(block: Block, db: BlockDB) {
if (child.children) {
layoutBlocks(child, db);
}
columnPos += child?.widthInColumns ?? 1;
let columnsFilled = child?.widthInColumns ?? 1;
if (columns > 0) {
// Make sure overflowing lines do not affect later lines
columnsFilled = Math.min(columnsFilled, columns - (columnPos % columns));
}
columnPos += columnsFilled;
log.debug('abc88 columnsPos', child, columnPos);
}
}

View File

@@ -1,6 +1,7 @@
// @ts-ignore: jison doesn't export types
import block from './block.jison';
import db from '../blockDB.js';
import { log } from '../../../logger.js';
describe('Block diagram', function () {
describe('when parsing a block diagram graph it should handle > ', function () {
@@ -402,6 +403,25 @@ columns 1
const B = blocks[0];
expect(B.styles).toContain('fill:#f9F');
});
it('should log a warning when block width exceeds column width', () => {
const str = `block-beta
columns 1
A:1
B:2
C:3
D:4
E:3
F:2
G:1`;
const logWarnSpy = vi.spyOn(log, 'warn').mockImplementation(() => undefined);
block.parse(str);
expect(logWarnSpy).toHaveBeenCalledWith('Block B width 2 exceeds configured column width 1');
logWarnSpy.mockRestore();
});
});
describe('prototype properties', function () {

View File

@@ -11,7 +11,7 @@ const detector: DiagramDetector = (txt, config = {}): boolean => {
// If diagram explicitly states flowchart-elk
/^\s*flowchart-elk/.test(txt) ||
// If a flowchart/graph diagram has their default renderer set to elk
(/^\s*flowchart|graph/.test(txt) && config?.flowchart?.defaultRenderer === 'elk')
(/^\s*(flowchart|graph)/.test(txt) && config?.flowchart?.defaultRenderer === 'elk')
) {
config.layout = 'elk';
return true;

View File

@@ -125,4 +125,60 @@ describe('flow db getData', () => {
const { edges } = flowDb.getData();
expect(edges[0].curve).toBe('basis');
});
it('should support modifying interpolate using edge id syntax', () => {
flowDb.addVertex('A', { text: 'A', type: 'text' }, undefined, [], [], '', {}, undefined);
flowDb.addVertex('B', { text: 'B', type: 'text' }, undefined, [], [], '', {}, undefined);
flowDb.addVertex('C', { text: 'C', type: 'text' }, undefined, [], [], '', {}, undefined);
flowDb.addVertex('D', { text: 'D', type: 'text' }, undefined, [], [], '', {}, undefined);
flowDb.addLink(['A'], ['B'], {});
flowDb.addLink(['A'], ['C'], { id: 'e2' });
flowDb.addLink(['B'], ['D'], { id: 'e3' });
flowDb.addLink(['C'], ['D'], {});
flowDb.updateLinkInterpolate(['default'], 'stepBefore');
flowDb.updateLinkInterpolate([0], 'basis');
flowDb.addVertex(
'e2',
{ text: 'Shouldnt be used', type: 'text' },
undefined,
[],
[],
'',
{},
' curve: monotoneX '
);
flowDb.addVertex(
'e3',
{ text: 'Shouldnt be used', type: 'text' },
undefined,
[],
[],
'',
{},
' curve: catmullRom '
);
const { edges } = flowDb.getData();
expect(edges[0].curve).toBe('basis');
expect(edges[1].curve).toBe('monotoneX');
expect(edges[2].curve).toBe('catmullRom');
expect(edges[3].curve).toBe('stepBefore');
});
});
describe('flow db direction', () => {
let flowDb: FlowDB;
beforeEach(() => {
flowDb = new FlowDB();
});
it('should set direction to TB when TD is set', () => {
flowDb.setDirection('TD');
expect(flowDb.getDirection()).toBe('TB');
});
it('should correctly set direction irrespective of leading spaces', () => {
flowDb.setDirection(' TD');
expect(flowDb.getDirection()).toBe('TB');
});
});

View File

@@ -139,6 +139,9 @@ export class FlowDB implements DiagramDB {
if (edgeDoc?.animation !== undefined) {
edge.animation = edgeDoc.animation;
}
if (edgeDoc?.curve !== undefined) {
edge.interpolate = edgeDoc.curve;
}
return;
}
@@ -403,7 +406,8 @@ You have to call mermaid.initialize.`
*
*/
public setDirection(dir: string) {
this.direction = dir;
this.direction = dir.trim();
if (/.*</.exec(this.direction)) {
this.direction = 'RL';
}

View File

@@ -37,6 +37,59 @@ describe('[Lines] when parsing', () => {
expect(edges[1].interpolate).toBe('cardinal');
});
it('should handle edge curve properties using edge ID', function () {
const res = flow.parser.parse(
'graph TD\n' +
'A e1@-->B\n' +
'A uniqueName@-->C\n' +
'e1@{curve: basis}\n' +
'uniqueName@{curve: cardinal}'
);
const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges();
expect(edges[0].interpolate).toBe('basis');
expect(edges[1].interpolate).toBe('cardinal');
});
it('should handle edge curve properties using edge ID but without overriding default', function () {
const res = flow.parser.parse(
'graph TD\n' +
'A e1@-->B\n' +
'A-->C\n' +
'linkStyle default interpolate linear\n' +
'e1@{curve: stepAfter}'
);
const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges();
expect(edges[0].interpolate).toBe('stepAfter');
expect(edges.defaultInterpolate).toBe('linear');
});
it('should handle edge curve properties using edge ID mixed with line interpolation', function () {
const res = flow.parser.parse(
'graph TD\n' +
'A e1@-->B-->D\n' +
'A-->C e4@-->D-->E\n' +
'linkStyle default interpolate linear\n' +
'linkStyle 1 interpolate basis\n' +
'e1@{curve: monotoneX}\n' +
'e4@{curve: stepBefore}'
);
const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges();
expect(edges[0].interpolate).toBe('monotoneX');
expect(edges[1].interpolate).toBe('basis');
expect(edges.defaultInterpolate).toBe('linear');
expect(edges[3].interpolate).toBe('stepBefore');
expect(edges.defaultInterpolate).toBe('linear');
});
it('should handle line interpolation multi-numbered definitions', function () {
const res = flow.parser.parse(
'graph TD\n' + 'A-->B\n' + 'A-->C\n' + 'linkStyle 0,1 interpolate basis'

View File

@@ -1,6 +1,7 @@
import { getConfig as commonGetConfig } from '../../config.js';
import type { PacketDiagramConfig } from '../../config.type.js';
import DEFAULT_CONFIG from '../../defaultConfig.js';
import type { DiagramDB } from '../../diagram-api/types.js';
import { cleanAndMerge } from '../../utils.js';
import {
clear as commonClear,
@@ -11,49 +12,42 @@ import {
setAccTitle,
setDiagramTitle,
} from '../common/commonDb.js';
import type { PacketDB, PacketData, PacketWord } from './types.js';
const defaultPacketData: PacketData = {
packet: [],
};
let data: PacketData = structuredClone(defaultPacketData);
import type { PacketWord } from './types.js';
const DEFAULT_PACKET_CONFIG: Required<PacketDiagramConfig> = DEFAULT_CONFIG.packet;
const getConfig = (): Required<PacketDiagramConfig> => {
const config = cleanAndMerge({
...DEFAULT_PACKET_CONFIG,
...commonGetConfig().packet,
});
if (config.showBits) {
config.paddingY += 10;
export class PacketDB implements DiagramDB {
private packet: PacketWord[] = [];
public getConfig() {
const config = cleanAndMerge({
...DEFAULT_PACKET_CONFIG,
...commonGetConfig().packet,
});
if (config.showBits) {
config.paddingY += 10;
}
return config;
}
return config;
};
const getPacket = (): PacketWord[] => data.packet;
const pushWord = (word: PacketWord) => {
if (word.length > 0) {
data.packet.push(word);
public getPacket() {
return this.packet;
}
};
const clear = () => {
commonClear();
data = structuredClone(defaultPacketData);
};
public pushWord(word: PacketWord) {
if (word.length > 0) {
this.packet.push(word);
}
}
export const db: PacketDB = {
pushWord,
getPacket,
getConfig,
clear,
setAccTitle,
getAccTitle,
setDiagramTitle,
getDiagramTitle,
getAccDescription,
setAccDescription,
};
public clear() {
commonClear();
this.packet = [];
}
public setAccTitle = setAccTitle;
public getAccTitle = getAccTitle;
public setDiagramTitle = setDiagramTitle;
public getDiagramTitle = getDiagramTitle;
public getAccDescription = getAccDescription;
public setAccDescription = setAccDescription;
}

View File

@@ -1,12 +1,14 @@
import type { DiagramDefinition } from '../../diagram-api/types.js';
import { db } from './db.js';
import { PacketDB } from './db.js';
import { parser } from './parser.js';
import { renderer } from './renderer.js';
import { styles } from './styles.js';
export const diagram: DiagramDefinition = {
parser,
db,
get db() {
return new PacketDB();
},
renderer,
styles,
};

View File

@@ -1,24 +1,26 @@
import { it, describe, expect } from 'vitest';
import { db } from './db.js';
import { PacketDB } from './db.js';
import { parser } from './parser.js';
const { clear, getPacket, getDiagramTitle, getAccTitle, getAccDescription } = db;
describe('packet diagrams', () => {
let db: PacketDB;
beforeEach(() => {
clear();
db = new PacketDB();
if (parser.parser) {
parser.parser.yy = db;
}
});
it('should handle a packet-beta definition', async () => {
const str = `packet-beta`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getPacket()).toMatchInlineSnapshot('[]');
expect(db.getPacket()).toMatchInlineSnapshot('[]');
});
it('should handle a packet definition', async () => {
const str = `packet`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getPacket()).toMatchInlineSnapshot('[]');
expect(db.getPacket()).toMatchInlineSnapshot('[]');
});
it('should handle diagram with data and title', async () => {
@@ -29,10 +31,10 @@ describe('packet diagrams', () => {
0-10: "test"
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getDiagramTitle()).toMatchInlineSnapshot('"Packet diagram"');
expect(getAccTitle()).toMatchInlineSnapshot('"Packet accTitle"');
expect(getAccDescription()).toMatchInlineSnapshot('"Packet accDescription"');
expect(getPacket()).toMatchInlineSnapshot(`
expect(db.getDiagramTitle()).toMatchInlineSnapshot('"Packet diagram"');
expect(db.getAccTitle()).toMatchInlineSnapshot('"Packet accTitle"');
expect(db.getAccDescription()).toMatchInlineSnapshot('"Packet accDescription"');
expect(db.getPacket()).toMatchInlineSnapshot(`
[
[
{
@@ -52,7 +54,7 @@ describe('packet diagrams', () => {
11: "single"
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getPacket()).toMatchInlineSnapshot(`
expect(db.getPacket()).toMatchInlineSnapshot(`
[
[
{
@@ -78,7 +80,7 @@ describe('packet diagrams', () => {
+16: "word"
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getPacket()).toMatchInlineSnapshot(`
expect(db.getPacket()).toMatchInlineSnapshot(`
[
[
{
@@ -104,7 +106,7 @@ describe('packet diagrams', () => {
+16: "word"
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getPacket()).toMatchInlineSnapshot(`
expect(db.getPacket()).toMatchInlineSnapshot(`
[
[
{
@@ -130,7 +132,7 @@ describe('packet diagrams', () => {
11-90: "multiple"
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getPacket()).toMatchInlineSnapshot(`
expect(db.getPacket()).toMatchInlineSnapshot(`
[
[
{
@@ -172,7 +174,7 @@ describe('packet diagrams', () => {
17-63: "multiple"
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getPacket()).toMatchInlineSnapshot(`
expect(db.getPacket()).toMatchInlineSnapshot(`
[
[
{

View File

@@ -3,12 +3,12 @@ import { parse } from '@mermaid-js/parser';
import type { ParserDefinition } from '../../diagram-api/types.js';
import { log } from '../../logger.js';
import { populateCommonDb } from '../common/populateCommonDb.js';
import { db } from './db.js';
import { PacketDB } from './db.js';
import type { PacketBlock, PacketWord } from './types.js';
const maxPacketSize = 10_000;
const populate = (ast: Packet) => {
const populate = (ast: Packet, db: PacketDB) => {
populateCommonDb(ast, db);
let lastBit = -1;
let word: PacketWord = [];
@@ -91,9 +91,17 @@ const getNextFittingBlock = (
};
export const parser: ParserDefinition = {
// @ts-expect-error - PacketDB is not assignable to DiagramDB
parser: { yy: undefined },
parse: async (input: string): Promise<void> => {
const ast: Packet = await parse('packet', input);
const db = parser.parser?.yy;
if (!(db instanceof PacketDB)) {
throw new Error(
'parser.parser?.yy was not a PacketDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.'
);
}
log.debug(ast);
populate(ast);
populate(ast, db);
},
};

View File

@@ -0,0 +1,189 @@
---
references:
- "File: /packages/mermaid/src/diagrams/flowchart/flowDiagram.ts"
- "File: /packages/mermaid/src/diagrams/flowchart/flowDb.ts"
- "File: /packages/mermaid/src/diagrams/flowchart/flowDetector.ts"
- "File: /packages/mermaid/src/diagrams/flowchart/flowDetector-v2.ts"
- "File: /packages/mermaid/src/diagrams/flowchart/flowRenderer-v3-unified.ts"
- "File: /packages/mermaid/src/diagrams/flowchart/styles.ts"
- "File: /packages/mermaid/src/diagrams/flowchart/types.ts"
- "File: /packages/mermaid/src/diagrams/flowchart/flowChartShapes.js"
- "File: /packages/mermaid/src/diagrams/flowchart/parser/flowParser.ts"
- "File: /packages/mermaid/src/diagrams/flowchart/elk/detector.ts"
generationTime: 2025-07-23T10:31:53.266Z
---
flowchart TD
%% Entry Points and Detection
Input["User Input Text"] --> Detection{Detection Phase}
Detection --> flowDetector["flowDetector.ts<br/>detector(txt, config)"]
Detection --> flowDetectorV2["flowDetector-v2.ts<br/>detector(txt, config)"]
Detection --> elkDetector["elk/detector.ts<br/>detector(txt, config)"]
flowDetector --> |"Checks /^\s*graph/"| DetectLegacy{Legacy Flowchart?}
flowDetectorV2 --> |"Checks /^\s*flowchart/"| DetectNew{New Flowchart?}
elkDetector --> |"Checks /^\s*flowchart-elk/"| DetectElk{ELK Layout?}
DetectLegacy --> |Yes| LoadDiagram
DetectNew --> |Yes| LoadDiagram
DetectElk --> |Yes| LoadDiagram
%% Loading Phase
LoadDiagram["loader() function"] --> flowDiagram["flowDiagram.ts<br/>diagram object"]
flowDiagram --> DiagramStructure{Diagram Components}
DiagramStructure --> Parser["parser: flowParser"]
DiagramStructure --> Database["db: new FlowDB()"]
DiagramStructure --> Renderer["renderer: flowRenderer-v3-unified"]
DiagramStructure --> Styles["styles: flowStyles"]
DiagramStructure --> Init["init: (cnf: MermaidConfig)"]
%% Parser Phase
Parser --> flowParser["parser/flowParser.ts<br/>newParser.parse(src)"]
flowParser --> |"Preprocesses src"| RemoveWhitespace["Remove trailing whitespace<br/>src.replace(/}\s*\n/g, '}\n')"]
RemoveWhitespace --> flowJison["parser/flow.jison<br/>flowJisonParser.parse(newSrc)"]
flowJison --> ParseGraph["Parse Graph Structure"]
ParseGraph --> ParseVertices["Parse Vertices"]
ParseGraph --> ParseEdges["Parse Edges"]
ParseGraph --> ParseSubgraphs["Parse Subgraphs"]
ParseGraph --> ParseClasses["Parse Classes"]
ParseGraph --> ParseStyles["Parse Styles"]
%% Database Phase - FlowDB Class
Database --> FlowDBClass["flowDb.ts<br/>FlowDB class"]
FlowDBClass --> DBInit["constructor()<br/>- Initialize counters<br/>- Bind methods<br/>- Setup toolTips<br/>- Call clear()"]
DBInit --> DBMethods{FlowDB Methods}
DBMethods --> addVertex["addVertex(id, textObj, type, style,<br/>classes, dir, props, metadata)"]
DBMethods --> addLink["addLink(_start[], _end[], linkData)"]
DBMethods --> addSingleLink["addSingleLink(_start, _end, type, id)"]
DBMethods --> setDirection["setDirection(dir)"]
DBMethods --> addSubGraph["addSubGraph(nodes[], id, title)"]
DBMethods --> addClass["addClass(id, style)"]
DBMethods --> setClass["setClass(ids, className)"]
DBMethods --> setTooltip["setTooltip(ids, tooltip)"]
DBMethods --> setClickEvent["setClickEvent(id, functionName, args)"]
DBMethods --> setClickFun["setClickFun(id, functionName, args)"]
%% Vertex Processing
addVertex --> VertexProcess{Vertex Processing}
VertexProcess --> CreateVertex["Create FlowVertex object<br/>- id, labelType, domId<br/>- styles[], classes[]"]
VertexProcess --> SanitizeText["sanitizeText(textObj.text)"]
VertexProcess --> ParseMetadata["Parse YAML metadata<br/>yaml.load(yamlData)"]
VertexProcess --> SetVertexProps["Set vertex properties<br/>- shape, label, icon, form<br/>- pos, img, constraint, w, h"]
%% Edge Processing
addSingleLink --> EdgeProcess{Edge Processing}
EdgeProcess --> CreateEdge["Create FlowEdge object<br/>- start, end, type, text<br/>- labelType, classes[]"]
EdgeProcess --> ProcessLinkText["Process link text<br/>- sanitizeText()<br/>- strip quotes"]
EdgeProcess --> SetEdgeProps["Set edge properties<br/>- type, stroke, length"]
EdgeProcess --> GenerateEdgeId["Generate edge ID<br/>getEdgeId(start, end, counter)"]
EdgeProcess --> ValidateEdgeLimit["Validate edge limit<br/>maxEdges check"]
%% Data Collection
DBMethods --> GetData["getData()"]
GetData --> CollectNodes["Collect nodes[] from vertices"]
GetData --> CollectEdges["Collect edges[] from edges"]
GetData --> ProcessSubGraphs["Process subgraphs<br/>- parentDB Map<br/>- subGraphDB Map"]
GetData --> AddNodeFromVertex["addNodeFromVertex()<br/>for each vertex"]
GetData --> ProcessEdgeTypes["destructEdgeType()<br/>arrowTypeStart, arrowTypeEnd"]
%% Node Creation
AddNodeFromVertex --> NodeCreation{Node Creation}
NodeCreation --> FindExistingNode["findNode(nodes, vertex.id)"]
NodeCreation --> CreateBaseNode["Create base node<br/>- id, label, parentId<br/>- cssStyles, cssClasses<br/>- shape, domId, tooltip"]
NodeCreation --> GetCompiledStyles["getCompiledStyles(classDefs)"]
NodeCreation --> GetTypeFromVertex["getTypeFromVertex(vertex)"]
%% Rendering Phase
Renderer --> flowRendererV3["flowRenderer-v3-unified.ts<br/>draw(text, id, version, diag)"]
flowRendererV3 --> RenderInit["Initialize rendering<br/>- getConfig()<br/>- handle securityLevel<br/>- getDiagramElement()"]
RenderInit --> GetLayoutData["diag.db.getData()<br/>as LayoutData"]
GetLayoutData --> SetupLayoutData["Setup layout data<br/>- type, layoutAlgorithm<br/>- direction, spacing<br/>- markers, diagramId"]
SetupLayoutData --> CallRender["render(data4Layout, svg)"]
CallRender --> SetupViewPort["setupViewPortForSVG(svg, padding)"]
SetupViewPort --> ProcessLinks["Process vertex links<br/>- create anchor elements<br/>- handle click events"]
%% Shape Rendering
CallRender --> ShapeSystem["flowChartShapes.js<br/>Shape Functions"]
ShapeSystem --> ShapeFunctions{Shape Functions}
ShapeFunctions --> question["question(parent, bbox, node)"]
ShapeFunctions --> hexagon["hexagon(parent, bbox, node)"]
ShapeFunctions --> rect_left_inv_arrow["rect_left_inv_arrow(parent, bbox, node)"]
ShapeFunctions --> lean_right["lean_right(parent, bbox, node)"]
ShapeFunctions --> lean_left["lean_left(parent, bbox, node)"]
ShapeFunctions --> insertPolygonShape["insertPolygonShape(parent, w, h, points)"]
ShapeFunctions --> intersectPolygon["intersectPolygon(node, points, point)"]
ShapeFunctions --> intersectRect["intersectRect(node, point)"]
%% Styling System
Styles --> stylesTS["styles.ts<br/>getStyles(options)"]
stylesTS --> StyleOptions["FlowChartStyleOptions<br/>- arrowheadColor, border2<br/>- clusterBkg, mainBkg<br/>- fontFamily, textColor"]
StyleOptions --> GenerateCSS["Generate CSS styles<br/>- .label, .cluster-label<br/>- .node, .edgePath<br/>- .flowchart-link, .edgeLabel"]
GenerateCSS --> GetIconStyles["getIconStyles()"]
%% Type System
Parser --> TypeSystem["types.ts<br/>Type Definitions"]
TypeSystem --> FlowVertex["FlowVertex interface"]
TypeSystem --> FlowEdge["FlowEdge interface"]
TypeSystem --> FlowClass["FlowClass interface"]
TypeSystem --> FlowSubGraph["FlowSubGraph interface"]
TypeSystem --> FlowVertexTypeParam["FlowVertexTypeParam<br/>Shape types"]
%% Utility Functions
DBMethods --> UtilityFunctions{Utility Functions}
UtilityFunctions --> lookUpDomId["lookUpDomId(id)"]
UtilityFunctions --> getClasses["getClasses()"]
UtilityFunctions --> getDirection["getDirection()"]
UtilityFunctions --> getVertices["getVertices()"]
UtilityFunctions --> getEdges["getEdges()"]
UtilityFunctions --> getSubGraphs["getSubGraphs()"]
UtilityFunctions --> clear["clear()"]
UtilityFunctions --> defaultConfig["defaultConfig()"]
%% Event Handling
ProcessLinks --> EventHandling{Event Handling}
EventHandling --> setupToolTips["setupToolTips(element)"]
EventHandling --> bindFunctions["bindFunctions(element)"]
EventHandling --> runFunc["utils.runFunc(functionName, args)"]
%% Common Database Functions
DBMethods --> CommonDB["commonDb.js functions"]
CommonDB --> setAccTitle["setAccTitle()"]
CommonDB --> getAccTitle["getAccTitle()"]
CommonDB --> setAccDescription["setAccDescription()"]
CommonDB --> getAccDescription["getAccDescription()"]
CommonDB --> setDiagramTitle["setDiagramTitle()"]
CommonDB --> getDiagramTitle["getDiagramTitle()"]
CommonDB --> commonClear["clear()"]
%% Final Output
ProcessLinks --> FinalSVG["Final SVG Output"]
%% Layout Algorithm Selection
SetupLayoutData --> LayoutAlgorithm{Layout Algorithm}
LayoutAlgorithm --> Dagre["dagre<br/>(default)"]
LayoutAlgorithm --> DagreWrapper["dagre-wrapper<br/>(v2 renderer)"]
LayoutAlgorithm --> ELK["elk<br/>(external package)"]
%% Testing Components
FlowDBClass --> TestFiles["Test Files"]
TestFiles --> flowDbSpec["flowDb.spec.ts"]
TestFiles --> flowChartShapesSpec["flowChartShapes.spec.js"]
TestFiles --> ParserTests["parser/*.spec.js files<br/>- flow-text.spec.js<br/>- flow-edges.spec.js<br/>- flow-style.spec.js<br/>- subgraph.spec.js"]
%% Configuration
Init --> ConfigSetup["Configuration Setup"]
ConfigSetup --> FlowchartConfig["cnf.flowchart config"]
ConfigSetup --> ArrowMarkers["arrowMarkerAbsolute"]
ConfigSetup --> LayoutConfig["layout config"]
ConfigSetup --> SetConfig["setConfig() calls"]

View File

@@ -0,0 +1,307 @@
---
references:
- "File: /packages/mermaid/src/mermaidAPI.ts"
- "File: /packages/mermaid/src/mermaid.ts"
generationTime: 2025-01-28T16:30:00.000Z
---
sequenceDiagram
participant User as User/Browser
participant Mermaid as mermaid.ts
participant Queue as executionQueue
participant API as mermaidAPI.ts
participant Config as configApi
participant Preprocessor as preprocessDiagram
participant DiagramAPI as diagram-api
participant Diagram as Diagram.fromText
participant Renderer as diagram.renderer
participant Styles as Styling System
participant DOM as DOM/SVG
Note over User, DOM: Mermaid Complete API Flow
%% Initialization Phase
User->>+Mermaid: mermaid.initialize(config)
Mermaid->>+API: mermaidAPI.initialize(config)
API->>API: assignWithDepth({}, userOptions)
API->>API: handle legacy fontFamily config
API->>Config: saveConfigFromInitialize(options)
alt Theme Configuration Available
API->>API: check if theme in theme object
API->>API: set themeVariables from theme
else Default Theme
API->>API: use default theme variables
end
API->>Config: setSiteConfig(options) or getSiteConfig()
API->>API: setLogLevel(config.logLevel)
API->>DiagramAPI: addDiagrams()
API-->>-Mermaid: initialization complete
Mermaid-->>-User: ready to render
%% Content Loaded Event
User->>DOM: document.load event
DOM->>+Mermaid: contentLoaded()
opt startOnLoad is true
Mermaid->>Config: getConfig()
Config-->>Mermaid: { startOnLoad: true }
Mermaid->>Mermaid: run()
end
Mermaid-->>-DOM: event handling complete
%% Main Run Function
User->>+Mermaid: mermaid.run(options)
Mermaid->>Mermaid: runThrowsErrors(options)
Mermaid->>Config: getConfig()
Config-->>Mermaid: configuration object
alt nodes provided
Mermaid->>Mermaid: use provided nodes
else querySelector provided
Mermaid->>DOM: document.querySelectorAll(querySelector)
DOM-->>Mermaid: nodesToProcess
end
Mermaid->>Mermaid: new InitIDGenerator(deterministicIds, seed)
loop For each diagram element
Mermaid->>DOM: check element.getAttribute('data-processed')
opt not processed
Mermaid->>DOM: element.setAttribute('data-processed', 'true')
Mermaid->>Mermaid: generate unique id
Mermaid->>DOM: get element.innerHTML
Mermaid->>Mermaid: entityDecode and clean text
Mermaid->>Mermaid: detectInit(txt)
Mermaid->>Queue: render(id, txt, element)
end
end
Mermaid-->>-User: processing initiated
%% Render Function (Queued)
activate Queue
Queue->>+API: mermaidAPI.render(id, text, container)
API->>DiagramAPI: addDiagrams()
API->>+Preprocessor: processAndSetConfigs(text)
Preprocessor->>Preprocessor: preprocessDiagram(text)
Preprocessor->>Config: reset()
Preprocessor->>Config: addDirective(processed.config)
Preprocessor-->>-API: { code, config }
API->>Config: getConfig()
Config-->>API: current configuration
opt text length > maxTextSize
API->>API: text = MAX_TEXTLENGTH_EXCEEDED_MSG
end
API->>API: setup id selectors and element IDs
API->>API: determine security level (sandbox/loose)
%% DOM Setup
alt svgContainingElement provided
alt isSandboxed
API->>DOM: sandboxedIframe(select(svgContainingElement), iFrameID)
API->>DOM: select iframe contentDocument body
else
API->>DOM: select(svgContainingElement)
end
else no container
API->>API: removeExistingElements(document, id, divId, iFrameId)
alt isSandboxed
API->>DOM: sandboxedIframe(select('body'), iFrameID)
else
API->>DOM: select('body')
end
end
API->>DOM: appendDivSvgG(root, id, enclosingDivID, fontFamily, XMLNS_XLINK_STD)
%% Diagram Creation
API->>+Diagram: Diagram.fromText(text, { title: processed.title })
Diagram->>DiagramAPI: detect diagram type
Diagram->>DiagramAPI: load appropriate diagram
Diagram-->>-API: diagram instance
opt parsing error occurred
API->>+Diagram: Diagram.fromText('error')
Diagram-->>-API: error diagram
API->>API: store parseEncounteredException
end
%% Style Generation
API->>DOM: get svg element and firstChild
API->>Renderer: diag.renderer.getClasses(text, diag)
Renderer-->>API: diagramClassDefs
API->>+Styles: createUserStyles(config, diagramType, diagramClassDefs, idSelector)
Styles->>Styles: createCssStyles(config, classDefs)
opt config.themeCSS defined
Styles->>Styles: append themeCSS
end
opt fontFamily configured
Styles->>Styles: add CSS variables for fonts
end
opt classDefs exist
loop For each styleClassDef
opt has styles
Styles->>Styles: cssImportantStyles for each CSS element
end
opt has textStyles
Styles->>Styles: cssImportantStyles for tspan elements
end
end
end
Styles->>Styles: getStyles(graphType, userCSSstyles, themeVariables)
Styles->>Styles: serialize(compile(svgId{allStyles}), stringify)
Styles-->>-API: compiled CSS rules
API->>DOM: create style element
API->>DOM: style.innerHTML = rules
API->>DOM: svg.insertBefore(style, firstChild)
%% Diagram Rendering
API->>+Renderer: diag.renderer.draw(text, id, version, diag)
Renderer->>Renderer: diagram-specific rendering logic
Renderer->>DOM: create SVG elements
Renderer->>DOM: apply positioning and styling
Renderer-->>-API: rendered diagram
opt rendering error
alt suppressErrorRendering
API->>API: removeTempElements()
API->>Mermaid: throw error
else
API->>Renderer: errorRenderer.draw(text, id, version)
end
end
%% Accessibility and Cleanup
API->>DOM: select svg element
API->>Diagram: diag.db.getAccTitle()
API->>Diagram: diag.db.getAccDescription()
API->>API: addA11yInfo(diagramType, svgNode, a11yTitle, a11yDescr)
API->>DOM: set xmlns for foreignobject elements
API->>DOM: get innerHTML from enclosing div
API->>+API: cleanUpSvgCode(svgCode, isSandboxed, arrowMarkerAbsolute)
opt not useArrowMarkerUrls and not sandboxed
API->>API: replace marker-end URLs with anchors
end
API->>API: decodeEntities(svgCode)
API->>API: replace <br> with <br/>
API-->>-API: cleaned SVG code
alt isSandboxed
API->>+API: putIntoIFrame(svgCode, svgEl)
API->>API: calculate iframe height
API->>API: toBase64 encode SVG content
API->>API: create iframe with sandbox attributes
API-->>-API: iframe HTML
else not loose security
API->>API: DOMPurify.sanitize(svgCode, options)
end
API->>API: attachFunctions()
API->>API: removeTempElements()
opt parseEncounteredException
API->>Mermaid: throw parseEncounteredException
end
API-->>-Queue: { diagramType, svg: svgCode, bindFunctions }
%% Return to Web Integration
activate Mermaid
Queue-->>Mermaid: render result
Mermaid->>DOM: element.innerHTML = svg
opt postRenderCallback
Mermaid->>User: postRenderCallback(id)
end
opt bindFunctions exist
Mermaid->>DOM: bindFunctions(element)
end
deactivate Mermaid
%% Parse Function Flow
User->>+Mermaid: mermaid.parse(text, parseOptions)
activate Queue
Queue->>+API: mermaidAPI.parse(text, parseOptions)
API->>DiagramAPI: addDiagrams()
API->>Preprocessor: processAndSetConfigs(text)
Preprocessor-->>API: { code, config }
API->>Diagram: getDiagramFromText(code)
Diagram-->>API: diagram instance
API-->>-Queue: { diagramType: diagram.type, config }
Queue-->>-Mermaid: parse result
Mermaid-->>-User: ParseResult or false
%% External Diagram Registration
User->>+Mermaid: registerExternalDiagrams(diagrams, options)
Mermaid->>DiagramAPI: addDiagrams()
Mermaid->>DiagramAPI: registerLazyLoadedDiagrams(...diagrams)
opt lazyLoad is false
Mermaid->>DiagramAPI: loadRegisteredDiagrams()
end
Mermaid-->>-User: registration complete
%% Error Handling
Note over Mermaid, API: Error Handling Throughout
alt Error occurs
API->>Mermaid: throw error
Mermaid->>+Mermaid: handleError(error, errors, parseError)
Mermaid->>Mermaid: log.warn(error)
alt isDetailedError
Mermaid->>User: parseError(error.str, error.hash)
else
Mermaid->>User: parseError(error)
end
opt not suppressErrors
Mermaid->>User: throw error
end
Mermaid-->>-User: error handled
end
%% Configuration Details
Note over Config: Configuration Functions
Note right of Config: Functions:<br/>- reset()<br/>- getConfig()<br/>- setConfig()<br/>- getSiteConfig()<br/>- updateSiteConfig()<br/>- saveConfigFromInitialize()
Note over Styles: CSS Generation
Note right of Styles: Features:<br/>- createCssStyles()<br/>- createUserStyles()<br/>- cssImportantStyles()<br/>- Theme integration<br/>- Class definitions
Note over API: Security Levels
Note right of API: Modes:<br/>- sandbox: iframe isolation<br/>- loose: minimal sanitization<br/>- default: DOMPurify sanitization
Note over API: Helper Functions
Note right of API: Utilities:<br/>- cleanUpSvgCode()<br/>- putIntoIFrame()<br/>- appendDivSvgG()<br/>- removeExistingElements()

View File

@@ -0,0 +1,180 @@
---
references:
- "File: /packages/mermaid/src/diagrams/mindmap/mindmap-definition.ts"
- "File: /packages/mermaid/src/diagrams/mindmap/mindmapDb.ts"
- "File: /packages/mermaid/src/diagrams/mindmap/detector.ts"
- "File: /packages/mermaid/src/diagrams/mindmap/mindmapTypes.ts"
- "File: /packages/mermaid/src/diagrams/mindmap/mindmapRenderer.ts"
- "File: /packages/mermaid/src/diagrams/mindmap/styles.ts"
- "File: /packages/mermaid/src/diagrams/mindmap/svgDraw.ts"
generationTime: 2025-01-28T16:00:00.000Z
---
sequenceDiagram
participant User as User Input Text
participant Detector as detector.ts
participant Loader as DiagramLoader
participant Definition as mindmap-definition.ts
participant Parser as parser/mindmap.jison
participant DB as MindmapDB
participant Renderer as mindmapRenderer.ts
participant Cytoscape as cytoscape.js
participant SVGDraw as svgDraw.ts
participant Styles as styles.ts
participant Output as Final SVG
Note over User, Output: Mindmap Implementation Flow
%% Detection Phase
User->>Detector: /^\s*mindmap/ text input
activate Detector
Detector->>Detector: detector(txt) validates pattern
Detector->>Loader: loader() function called
deactivate Detector
activate Loader
Loader->>Definition: import mindmap-definition.js
deactivate Loader
%% Core Structure Setup
activate Definition
Definition->>DB: get db() → new MindmapDB()
Definition->>Renderer: setup renderer
Definition->>Parser: setup parser
Definition->>Styles: setup styles
deactivate Definition
%% Database Initialization
activate DB
Note over DB: MindmapDB Constructor
DB->>DB: initialize nodes array
DB->>DB: setup nodeType constants
DB->>DB: bind methods
DB->>DB: clear() state
%% Parsing Phase
activate Parser
User->>Parser: mindmap syntax text
loop For each node in hierarchy
Parser->>DB: addNode(level, id, descr, type)
activate DB
DB->>DB: sanitizeText(id, descr)
DB->>DB: getType(startStr, endStr)
Note right of DB: Shape Detection:<br/>[ → RECT<br/>( → ROUNDED_RECT<br/>(( → CIRCLE<br/>)) → BANG<br/>{{ → HEXAGON
DB->>DB: getParent(level)
DB->>DB: create MindmapNode
DB->>DB: add to hierarchy
deactivate DB
end
opt Icon/Class Decoration
Parser->>DB: decorateNode(decoration)
DB->>DB: set icon/class properties
end
deactivate Parser
%% Data Preparation
Renderer->>DB: getData() for layout
activate DB
DB->>DB: collect all nodes
DB->>DB: build parent-child relationships
DB-->>Renderer: return node hierarchy
deactivate DB
%% Rendering Pipeline
activate Renderer
Note over Renderer: Rendering Phase
Renderer->>Cytoscape: initialize cytoscape
activate Cytoscape
loop For each node in mindmap
Renderer->>Cytoscape: addNodes(mindmap, cy, conf, level)
Cytoscape->>Cytoscape: create node data
Cytoscape->>Cytoscape: set position (x, y)
end
loop For parent-child relationships
Renderer->>Cytoscape: add edges
Cytoscape->>Cytoscape: create edge data
end
Renderer->>Cytoscape: configure cose-bilkent layout
Cytoscape->>Cytoscape: calculate optimal positions
Cytoscape-->>Renderer: return positioned graph
deactivate Cytoscape
%% SVG Generation
Renderer->>SVGDraw: drawNodes(db, svg, mindmap, section, conf)
activate SVGDraw
loop For each node recursively
SVGDraw->>SVGDraw: select shape function
alt Default Shape
SVGDraw->>SVGDraw: defaultBkg() - rounded rectangle
else Rectangle Shape
SVGDraw->>SVGDraw: rectBkg() - sharp corners
else Circle Shape
SVGDraw->>SVGDraw: circleBkg() - perfect circle
else Cloud Shape
SVGDraw->>SVGDraw: cloudBkg() - organic curves
else Bang Shape
SVGDraw->>SVGDraw: bangBkg() - explosion style
else Hexagon Shape
SVGDraw->>SVGDraw: hexagonBkg() - six sides
end
SVGDraw->>SVGDraw: create SVG elements
SVGDraw->>SVGDraw: add text labels
SVGDraw->>SVGDraw: position node
opt Node has children
SVGDraw->>SVGDraw: drawNodes() recursive call
end
end
deactivate SVGDraw
%% Edge Rendering
Renderer->>Renderer: drawEdges(edgesEl, cy)
loop For each edge
Renderer->>Renderer: extract edge bounds
Renderer->>Renderer: draw SVG path
end
%% Styling Application
Renderer->>Styles: getStyles(options)
activate Styles
Styles->>Styles: genSections(options)
loop For THEME_COLOR_LIMIT sections
Styles->>Styles: generate color scale
Styles->>Styles: create CSS rules
Note right of Styles: .section-X fills<br/>.edge-depth-X widths<br/>.node-icon-X colors
end
Styles->>Styles: apply theme integration
Styles-->>Renderer: return compiled CSS
deactivate Styles
%% Final Assembly
Renderer->>Output: selectSvgElement()
Renderer->>Output: setupGraphViewbox()
Renderer->>Output: apply styles
Renderer->>Output: add interactive elements
deactivate Renderer
activate Output
Note over Output: Final Mindmap Features
Output->>Output: responsive layout
Output->>Output: accessibility attributes
Output->>Output: hover effects
Output->>Output: click handling
Output-->>User: rendered mindmap
deactivate Output
%% Configuration Details
Note over DB, Styles: Configuration Options
Note right of DB: Padding Calculations:<br/>Base padding from config<br/>RECT: ×2 padding<br/>ROUNDED_RECT: ×2 padding<br/>HEXAGON: ×2 padding
Note right of Styles: Section Management:<br/>MAX_SECTIONS = 12<br/>Dynamic color generation<br/>Git theme integration
Note right of Renderer: Layout Parameters:<br/>Cytoscape configuration<br/>coseBilkent settings<br/>Node spacing rules

View File

@@ -79,6 +79,7 @@ To add an integration to this list, see the [Integrations - create page](./integ
LLM integrations to create mermaid diagrams using AI from text descriptions.
- [HueHive - Create mermaid diagrams with text](https://huehive.co/tools/diagrams)
- [MCP Server Mermaid](https://github.com/hustcc/mcp-mermaid) - Generate mermaid diagram and chart with AI MCP dynamically.
### CRM/ERP
@@ -98,6 +99,7 @@ Blogging frameworks and platforms
- [Mermaid](https://nextra.site/docs/guide/mermaid)
- [WordPress](https://wordpress.org)
- [MerPRess](https://wordpress.org/plugins/merpress/)
- [WP Documentation](https://wordpress.org/themes/wp-documentation/)
### CMS/ECM

View File

@@ -10,9 +10,7 @@ Applications that support Mermaid files [SHOULD](https://datatracker.ietf.org/do
### MIME Type
The recommended [MIME type](https://www.iana.org/assignments/media-types/media-types.xhtml) for Mermaid media is `text/vnd.mermaid`.
Currently pending [IANA](https://www.iana.org/) recognition.
The recommended [MIME type](https://www.iana.org/assignments/media-types/media-types.xhtml) for Mermaid media is [`text/vnd.mermaid`](https://www.iana.org/assignments/media-types/application/vnd.mermaid).
## Showcase

View File

@@ -1135,15 +1135,46 @@ It is possible to style the type of curve used for lines between items, if the d
Available curve styles include `basis`, `bumpX`, `bumpY`, `cardinal`, `catmullRom`, `linear`, `monotoneX`, `monotoneY`,
`natural`, `step`, `stepAfter`, and `stepBefore`.
For a full list of available curves, including an explanation of custom curves, refer to
the [Shapes](https://d3js.org/d3-shape/curve) documentation in the [d3-shape](https://github.com/d3/d3-shape/) project.
Line styling can be achieved in two ways:
1. Change the curve style of all the lines
2. Change the curve style of a particular line
#### Diagram level curve style
In this example, a left-to-right graph uses the `stepBefore` curve style:
```
%%{ init: { 'flowchart': { 'curve': 'stepBefore' } } }%%
---
config:
flowchart:
curve: stepBefore
---
graph LR
```
For a full list of available curves, including an explanation of custom curves, refer to
the [Shapes](https://d3js.org/d3-shape/curve) documentation in the [d3-shape](https://github.com/d3/d3-shape/) project.
#### Edge level curve style using Edge IDs (v<MERMAID_RELEASE_VERSION>+)
You can assign IDs to [edges](#attaching-an-id-to-edges). After assigning an ID you can modify the line style by modifying the edge's `curve` property using the following syntax:
```mermaid
flowchart LR
A e1@==> B
A e2@--> C
e1@{ curve: linear }
e2@{ curve: natural }
```
```info
Any edge curve style modified at the edge level overrides the diagram level style.
```
```info
If the same edge is modified multiple times the last modification will be rendered.
```
### Styling a node

View File

@@ -54,7 +54,7 @@ export async function requirementBox<T extends SVGGraphicsElement>(
if (isRequirementNode) {
const idHeight = await addText(
shapeSvg,
`${requirementNode.requirementId ? `id: ${requirementNode.requirementId}` : ''}`,
`${requirementNode.requirementId ? `ID: ${requirementNode.requirementId}` : ''}`,
accumulativeHeight,
node.labelStyle
);

View File

@@ -16,6 +16,19 @@ export interface NodeMetaData {
export interface EdgeMetaData {
animation?: 'fast' | 'slow';
animate?: boolean;
curve?:
| 'basis'
| 'bumpX'
| 'bumpY'
| 'cardinal'
| 'catmullRom'
| 'linear'
| 'monotoneX'
| 'monotoneY'
| 'natural'
| 'step'
| 'stepAfter'
| 'stepBefore';
}
import type { MermaidConfig } from './config.type.js';

View File

@@ -1,5 +1,11 @@
# @mermaid-js/parser
## 0.6.2
### Patch Changes
- [#6510](https://github.com/mermaid-js/mermaid/pull/6510) [`7a38eb7`](https://github.com/mermaid-js/mermaid/commit/7a38eb715d795cd5c66cb59357d64ec197b432e6) Thanks [@sidharthv96](https://github.com/sidharthv96)! - chore: Move packet diagram out of beta
## 0.6.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@mermaid-js/parser",
"version": "0.6.1",
"version": "0.6.2",
"description": "MermaidJS parser",
"author": "Yokozuna59",
"contributors": [

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@mermaid-js/tiny",
"version": "11.8.1",
"version": "11.9.0",
"description": "Tiny version of mermaid",
"type": "commonjs",
"main": "./dist/mermaid.tiny.js",