Merge branch 'mermaid-js:develop' into develop

This commit is contained in:
Arpit Jain
2025-05-10 13:53:52 +09:00
committed by GitHub
34 changed files with 1035 additions and 341 deletions

View File

@@ -295,6 +295,12 @@ export interface FlowchartDiagramConfig extends BaseDiagramConfig {
*
*/
wrappingWidth?: number;
/**
* If true, subgraphs without explicit direction will inherit the global graph direction
* (e.g., LR, TB, RL, BT). Defaults to false to preserve legacy layout behavior.
*
*/
inheritDir?: boolean;
}
/**
* This interface was referenced by `MermaidConfig`'s JSON-Schema

View File

@@ -71,6 +71,10 @@ const config: RequiredDeep<MermaidConfig> = {
fontWeight: this.personFontWeight,
};
},
flowchart: {
...defaultConfigJson.flowchart,
inheritDir: false, // default to legacy behavior
},
external_personFont: function () {
return {

View File

@@ -27,6 +27,7 @@ import block from '../diagrams/block/blockDetector.js';
import architecture from '../diagrams/architecture/architectureDetector.js';
import { registerLazyLoadedDiagrams } from './detectType.js';
import { registerDiagram } from './diagramAPI.js';
import '../type.d.ts';
let hasLoadedDiagrams = false;
export const addDiagrams = () => {
@@ -69,6 +70,11 @@ export const addDiagrams = () => {
return text.toLowerCase().trimStart().startsWith('---');
}
);
if (includeLargeFeatures) {
registerLazyLoadedDiagrams(flowchartElk, mindmap, architecture);
}
// Ordering of detectors is important. The first one to return true will be used.
registerLazyLoadedDiagrams(
c4,
@@ -81,10 +87,8 @@ export const addDiagrams = () => {
pie,
requirement,
sequence,
flowchartElk,
flowchartV2,
flowchart,
mindmap,
timeline,
git,
stateV2,
@@ -95,7 +99,6 @@ export const addDiagrams = () => {
packet,
xychart,
block,
architecture,
radar
);
};

View File

@@ -342,29 +342,36 @@ export const renderKatex = async (text: string, config: MermaidConfig): Promise<
return text.replace(katexRegex, 'MathML is unsupported in this environment.');
}
const { default: katex } = await import('katex');
const outputMode =
config.forceLegacyMathML || (!isMathMLSupported() && config.legacyMathML)
? 'htmlAndMathml'
: 'mathml';
return text
.split(lineBreakRegex)
.map((line) =>
hasKatex(line)
? `<div style="display: flex; align-items: center; justify-content: center; white-space: nowrap;">${line}</div>`
: `<div>${line}</div>`
)
.join('')
.replace(katexRegex, (_, c) =>
katex
.renderToString(c, {
throwOnError: true,
displayMode: true,
output: outputMode,
})
.replace(/\n/g, ' ')
.replace(/<annotation.*<\/annotation>/g, '')
);
if (includeLargeFeatures) {
const { default: katex } = await import('katex');
const outputMode =
config.forceLegacyMathML || (!isMathMLSupported() && config.legacyMathML)
? 'htmlAndMathml'
: 'mathml';
return text
.split(lineBreakRegex)
.map((line) =>
hasKatex(line)
? `<div style="display: flex; align-items: center; justify-content: center; white-space: nowrap;">${line}</div>`
: `<div>${line}</div>`
)
.join('')
.replace(katexRegex, (_, c) =>
katex
.renderToString(c, {
throwOnError: true,
displayMode: true,
output: outputMode,
})
.replace(/\n/g, ' ')
.replace(/<annotation.*<\/annotation>/g, '')
);
}
return text.replace(
katexRegex,
'Katex is not supported in @mermaid-js/tiny. Please use the full mermaid library.'
);
};
export default {

View File

@@ -651,7 +651,8 @@ You have to call mermaid.initialize.`
const prims: any = { boolean: {}, number: {}, string: {} };
const objs: any[] = [];
let dir; // = undefined; direction.trim();
let dir: string | undefined;
const nodeList = a.filter(function (item) {
const type = typeof item;
if (item.stmt && item.stmt === 'dir') {
@@ -670,7 +671,16 @@ You have to call mermaid.initialize.`
return { nodeList, dir };
};
const { nodeList, dir } = uniq(list.flat());
const result = uniq(list.flat());
const nodeList = result.nodeList;
let dir = result.dir;
const flowchartConfig = getConfig().flowchart ?? {};
dir =
dir ??
(flowchartConfig.inheritDir
? (this.getDirection() ?? (getConfig() as any).direction ?? undefined)
: undefined);
if (this.version === 'gen-1') {
for (let i = 0; i < nodeList.length; i++) {
nodeList[i] = this.lookUpDomId(nodeList[i]);
@@ -681,6 +691,7 @@ You have to call mermaid.initialize.`
title = title || '';
title = this.sanitizeText(title);
this.subCount = this.subCount + 1;
const subGraph = {
id: id,
nodes: nodeList,

View File

@@ -1,7 +1,9 @@
import type { InfoFields, InfoDB } from './infoTypes.js';
import packageJson from '../../../package.json' assert { type: 'json' };
export const DEFAULT_INFO_DB: InfoFields = { version: packageJson.version } as const;
export const DEFAULT_INFO_DB: InfoFields = {
version: packageJson.version + (includeLargeFeatures ? '' : '-tiny'),
} as const;
export const getVersion = (): string => DEFAULT_INFO_DB.version;

View File

@@ -30,6 +30,7 @@ describe('packet diagrams', () => {
[
[
{
"bits": 11,
"end": 10,
"label": "test",
"start": 0,
@@ -49,11 +50,13 @@ describe('packet diagrams', () => {
[
[
{
"bits": 11,
"end": 10,
"label": "test",
"start": 0,
},
{
"bits": 1,
"end": 11,
"label": "single",
"start": 11,
@@ -63,6 +66,58 @@ describe('packet diagrams', () => {
`);
});
it('should handle bit counts', async () => {
const str = `packet-beta
+8: "byte"
+16: "word"
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getPacket()).toMatchInlineSnapshot(`
[
[
{
"bits": 8,
"end": 7,
"label": "byte",
"start": 0,
},
{
"bits": 16,
"end": 23,
"label": "word",
"start": 8,
},
],
]
`);
});
it('should handle bit counts with bit or bits', async () => {
const str = `packet-beta
+8: "byte"
+16: "word"
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getPacket()).toMatchInlineSnapshot(`
[
[
{
"bits": 8,
"end": 7,
"label": "byte",
"start": 0,
},
{
"bits": 16,
"end": 23,
"label": "word",
"start": 8,
},
],
]
`);
});
it('should split into multiple rows', async () => {
const str = `packet-beta
0-10: "test"
@@ -73,11 +128,13 @@ describe('packet diagrams', () => {
[
[
{
"bits": 11,
"end": 10,
"label": "test",
"start": 0,
},
{
"bits": 20,
"end": 31,
"label": "multiple",
"start": 11,
@@ -85,6 +142,7 @@ describe('packet diagrams', () => {
],
[
{
"bits": 31,
"end": 63,
"label": "multiple",
"start": 32,
@@ -92,6 +150,7 @@ describe('packet diagrams', () => {
],
[
{
"bits": 26,
"end": 90,
"label": "multiple",
"start": 64,
@@ -111,11 +170,13 @@ describe('packet diagrams', () => {
[
[
{
"bits": 17,
"end": 16,
"label": "test",
"start": 0,
},
{
"bits": 14,
"end": 31,
"label": "multiple",
"start": 17,
@@ -123,6 +184,7 @@ describe('packet diagrams', () => {
],
[
{
"bits": 31,
"end": 63,
"label": "multiple",
"start": 32,
@@ -142,6 +204,16 @@ describe('packet diagrams', () => {
);
});
it('should throw error if numbers are not continuous with bit counts', async () => {
const str = `packet-beta
+16: "test"
18-20: "error"
`;
await expect(parser.parse(str)).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Packet block 18 - 20 is not contiguous. It should start from 16.]`
);
});
it('should throw error if numbers are not continuous for single packets', async () => {
const str = `packet-beta
0-16: "test"
@@ -152,6 +224,16 @@ describe('packet diagrams', () => {
);
});
it('should throw error if numbers are not continuous for single packets with bit counts', async () => {
const str = `packet-beta
+16: "test"
18: "error"
`;
await expect(parser.parse(str)).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Packet block 18 - 18 is not contiguous. It should start from 16.]`
);
});
it('should throw error if numbers are not continuous for single packets - 2', async () => {
const str = `packet-beta
0-16: "test"
@@ -172,4 +254,13 @@ describe('packet diagrams', () => {
`[Error: Packet block 25 - 20 is invalid. End must be greater than start.]`
);
});
it('should throw error if bit count is 0', async () => {
const str = `packet-beta
+0: "test"
`;
await expect(parser.parse(str)).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Packet block 0 is invalid. Cannot have a zero bit field.]`
);
});
});

View File

@@ -10,26 +10,33 @@ const maxPacketSize = 10_000;
const populate = (ast: Packet) => {
populateCommonDb(ast, db);
let lastByte = -1;
let lastBit = -1;
let word: PacketWord = [];
let row = 1;
const { bitsPerRow } = db.getConfig();
for (let { start, end, label } of ast.blocks) {
if (end && end < start) {
for (let { start, end, bits, label } of ast.blocks) {
if (start !== undefined && end !== undefined && end < start) {
throw new Error(`Packet block ${start} - ${end} is invalid. End must be greater than start.`);
}
if (start !== lastByte + 1) {
start ??= lastBit + 1;
if (start !== lastBit + 1) {
throw new Error(
`Packet block ${start} - ${end ?? start} is not contiguous. It should start from ${
lastByte + 1
lastBit + 1
}.`
);
}
lastByte = end ?? start;
log.debug(`Packet block ${start} - ${lastByte} with label ${label}`);
if (bits === 0) {
throw new Error(`Packet block ${start} is invalid. Cannot have a zero bit field.`);
}
end ??= start + (bits ?? 1) - 1;
bits ??= end - start + 1;
lastBit = end;
log.debug(`Packet block ${start} - ${lastBit} with label ${label}`);
while (word.length <= bitsPerRow + 1 && db.getPacket().length < maxPacketSize) {
const [block, nextBlock] = getNextFittingBlock({ start, end, label }, row, bitsPerRow);
const [block, nextBlock] = getNextFittingBlock({ start, end, bits, label }, row, bitsPerRow);
word.push(block);
if (block.end + 1 === row * bitsPerRow) {
db.pushWord(word);
@@ -39,7 +46,7 @@ const populate = (ast: Packet) => {
if (!nextBlock) {
break;
}
({ start, end, label } = nextBlock);
({ start, end, bits, label } = nextBlock);
}
}
db.pushWord(word);
@@ -50,8 +57,11 @@ const getNextFittingBlock = (
row: number,
bitsPerRow: number
): [Required<PacketBlock>, PacketBlock | undefined] => {
if (block.start === undefined) {
throw new Error('start should have been set during first phase');
}
if (block.end === undefined) {
block.end = block.start;
throw new Error('end should have been set during first phase');
}
if (block.start > block.end) {
@@ -62,16 +72,20 @@ const getNextFittingBlock = (
return [block as Required<PacketBlock>, undefined];
}
const rowEnd = row * bitsPerRow - 1;
const rowStart = row * bitsPerRow;
return [
{
start: block.start,
end: row * bitsPerRow - 1,
end: rowEnd,
label: block.label,
bits: rowEnd - block.start,
},
{
start: row * bitsPerRow,
start: rowStart,
end: block.end,
label: block.label,
bits: block.end - rowStart,
},
];
};

View File

@@ -92,6 +92,12 @@ Mermaid can load multiple diagrams, in the same page.
> Try it out, save this code as HTML and load it using any browser.
> (Except Internet Explorer, please don't use Internet Explorer.)
## Tiny Mermaid
We offer a smaller version of Mermaid that's approximately half the size of the full library. This tiny version doesn't support Mindmap Diagrams, Architecture Diagrams, KaTeX rendering, or lazy loading.
If you need a more lightweight version without these features, you can use [Mermaid Tiny](https://github.com/mermaid-js/mermaid/tree/develop/packages/tiny).
## Enabling Click Event and Tags in Nodes
A `securityLevel` configuration has to first be cleared. `securityLevel` sets the level of trust for the parsed diagrams and limits click functionality. This was introduced in version 8.2 as a security improvement, aimed at preventing malicious use.

View File

@@ -109,6 +109,7 @@ To Deploy Mermaid:
- [Mermaid Live Editor](https://github.com/mermaid-js/mermaid-live-editor)
- [Mermaid CLI](https://github.com/mermaid-js/mermaid-cli)
- [Mermaid Tiny](https://github.com/mermaid-js/mermaid/tree/develop/packages/tiny)
- [Mermaid Webpack Demo](https://github.com/mermaidjs/mermaid-webpack-demo)
- [Mermaid Parcel Demo](https://github.com/mermaidjs/mermaid-parcel-demo)

View File

@@ -360,6 +360,27 @@ It is possible to annotate classes with markers to provide additional metadata a
Annotations are defined within the opening `<<` and closing `>>`. There are two ways to add an annotation to a class, and either way the output will be same:
> **Tip:**
> In Mermaid class diagrams, annotations like `<<interface>>` can be attached in two ways:
>
> - **Inline with the class definition** (Recommended for consistency):
>
> ```mermaid-example
> classDiagram
> class Shape <<interface>>
> ```
>
> - **Separate line after the class definition**:
>
> ```mermaid-example
> classDiagram
> class Shape
> <<interface>> Shape
> ```
>
> Both methods are fully supported and produce identical diagrams.
> However, it is recommended to use the **inline style** for better readability and consistent formatting across diagrams.
- In a **_separate line_** after a class is defined:
```mermaid-example

View File

@@ -10,13 +10,25 @@ This diagram type is particularly useful for developers, network engineers, educ
## Syntax
```md
```
packet-beta
start: "Block name" %% Single-bit block
start-end: "Block name" %% Multi-bit blocks
... More Fields ...
```
### Bits Syntax (v<MERMAID_RELEASE_VERSION>+)
Using start and end bit counts can be difficult, especially when modifying a design. For this we add a bit count field, which starts from the end of the previous field automagically. Use `+<count>` to set the number of bits, thus:
```
packet-beta
+1: "Block name" %% Single-bit block
+8: "Block name" %% 8-bit block
9-15: "Manually set start and end, it's fine to mix and match"
... More Fields ...
```
## Examples
```mermaid-example
@@ -46,8 +58,8 @@ packet-beta
```mermaid-example
packet-beta
title UDP Packet
0-15: "Source Port"
16-31: "Destination Port"
+16: "Source Port"
+16: "Destination Port"
32-47: "Length"
48-63: "Checksum"
64-95: "Data (variable length)"

View File

@@ -5,7 +5,6 @@
// @ts-ignore TODO: Investigate D3 issue
import { select } from 'd3';
import { compile, serialize, stringify } from 'stylis';
// @ts-ignore: TODO Fix ts errors
import DOMPurify from 'dompurify';
import isEmpty from 'lodash-es/isEmpty.js';
import packageJson from '../package.json' assert { type: 'json' };

View File

@@ -89,6 +89,7 @@ export async function erBox<T extends SVGGraphicsElement>(parent: D3Selection<T>
nameBBox.height += TEXT_PADDING;
let yOffset = 0;
const yOffsets = [];
const rows = [];
let maxTypeWidth = 0;
let maxNameWidth = 0;
let maxKeysWidth = 0;
@@ -137,12 +138,12 @@ export async function erBox<T extends SVGGraphicsElement>(parent: D3Selection<T>
);
maxCommentWidth = Math.max(maxCommentWidth, commentBBox.width + PADDING);
yOffset +=
const rowHeight =
Math.max(typeBBox.height, nameBBox.height, keysBBox.height, commentBBox.height) +
TEXT_PADDING;
yOffsets.push(yOffset);
rows.push({ yOffset, rowHeight });
yOffset += rowHeight;
}
yOffsets.pop();
let totalWidthSections = 4;
if (maxKeysWidth <= PADDING) {
@@ -185,8 +186,12 @@ export async function erBox<T extends SVGGraphicsElement>(parent: D3Selection<T>
options.fillStyle = 'solid';
}
let totalShapeBBoxHeight = 0;
if (rows.length > 0) {
totalShapeBBoxHeight = rows.reduce((sum, row) => sum + (row?.rowHeight ?? 0), 0);
}
const w = Math.max(shapeBBox.width + PADDING * 2, node?.width || 0, maxWidth);
const h = Math.max(shapeBBox.height + (yOffsets[0] || yOffset) + TEXT_PADDING, node?.height || 0);
const h = Math.max((totalShapeBBoxHeight ?? 0) + nameBBox.height, node?.height || 0);
const x = -w / 2;
const y = -h / 2;
@@ -232,13 +237,10 @@ export async function erBox<T extends SVGGraphicsElement>(parent: D3Selection<T>
yOffsets.push(0);
// Draw row rects
for (const [i, yOffset] of yOffsets.entries()) {
if (i === 0 && yOffsets.length > 1) {
continue;
// Skip first row
}
const isEven = i % 2 === 0 && yOffset !== 0;
const roughRect = rc.rectangle(x, nameBBox.height + y + yOffset, w, nameBBox.height, {
for (const [i, row] of rows.entries()) {
const contentRowIndex = i + 1; // Adjusted index to skip the header (name) row
const isEven = contentRowIndex % 2 === 0 && row.yOffset !== 0;
const roughRect = rc.rectangle(x, nameBBox.height + y + row?.yOffset, w, row?.rowHeight, {
...options,
fill: isEven ? rowEven : rowOdd,
stroke: nodeBorder,
@@ -246,7 +248,7 @@ export async function erBox<T extends SVGGraphicsElement>(parent: D3Selection<T>
shapeSvg
.insert(() => roughRect, 'g.label')
.attr('style', cssStyles!.join(''))
.attr('class', `row-rect-${i % 2 === 0 ? 'even' : 'odd'}`);
.attr('class', `row-rect-${isEven ? 'even' : 'odd'}`);
}
// Draw divider lines

View File

@@ -2124,6 +2124,13 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
type: number
default: 200
inheritDir:
type: boolean
default: false
description: |
If true, subgraphs without explicit direction will inherit the global graph direction
(e.g., LR, TB, RL, BT). Defaults to false to preserve legacy layout behavior.
SankeyLinkColor:
description: |
Picks the color of the sankey diagram links, using the colors of the source and/or target of the links.

2
packages/mermaid/src/type.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
// eslint-disable-next-line no-var
declare var includeLargeFeatures: boolean;

View File

@@ -12,5 +12,10 @@ entry Packet:
;
PacketBlock:
start=INT('-' end=INT)? ':' label=STRING EOL
;
(
start=INT('-' end=INT)?
| '+' bits=INT
)
':' label=STRING
EOL
;

35
packages/tiny/README.md Normal file
View File

@@ -0,0 +1,35 @@
# Tiny Mermaid
This is a tiny version of mermaid that is optimized for the web. It is a subset of the mermaid library and is designed to be used in the browser via CDN.
## Lazy loading
The original mermaid library supports lazy loading, so it will be faster on the initial load, and only load the required diagrams.
This is not supported in the tiny mermaid library. So it's always recommended to use the full mermaid library unless you have a very specific reason to reduce the bundle size.
## Removals from mermaid
This does not support
- Mindmap Diagram
- Architecture Diagram
- Katex rendering
- Lazy loading
## Usage via NPM
This package is not meant to be installed directly from npm. It is designed to be used via CDN.
If you need to use mermaid in your project, please install the full [`mermaid` package](https://www.npmjs.com/package/mermaid) instead.
## Usage via CDN
```html
<!-- Format -->
<script src="https://cdn.jsdelivr.net/npm/@mermaid-js/tiny@<MERMAID_MAJOR_VERSION>/dist/mermaid.tiny.js"></script>
<!-- Pinning major version -->
<script src="https://cdn.jsdelivr.net/npm/@mermaid-js/tiny@11/dist/mermaid.tiny.js"></script>
<!-- Pinning specific version -->
<script src="https://cdn.jsdelivr.net/npm/@mermaid-js/tiny@11.6.0/dist/mermaid.tiny.js"></script>
```

View File

@@ -0,0 +1,25 @@
{
"name": "@mermaid-js/tiny",
"version": "11.6.0",
"description": "Tiny version of mermaid",
"type": "commonjs",
"main": "./dist/mermaid.tiny.js",
"scripts": {
"clean": "rimraf dist"
},
"repository": {
"type": "git",
"url": "https://github.com/mermaid-js/mermaid"
},
"author": "Sidharth Vinod",
"license": "MIT",
"dependencies": {},
"devDependencies": {},
"files": [
"dist/",
"README.md"
],
"publishConfig": {
"access": "public"
}
}