Merge remote-tracking branch 'origin/develop' into knsv-treemap

This commit is contained in:
Knut Sveidqvist
2025-05-12 19:18:04 +02:00
34 changed files with 1038 additions and 332 deletions

View File

@@ -0,0 +1,6 @@
---
'mermaid': minor
'@mermaid-js/parser': minor
---
feat: Add shorter `+<count>: Label` syntax in packet diagram

View File

@@ -1,5 +1,5 @@
import { build } from 'esbuild'; import { build } from 'esbuild';
import { mkdir, writeFile } from 'node:fs/promises'; import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
import { packageOptions } from '../.build/common.js'; import { packageOptions } from '../.build/common.js';
import { generateLangium } from '../.build/generateLangium.js'; import { generateLangium } from '../.build/generateLangium.js';
import type { MermaidBuildOptions } from './util.js'; import type { MermaidBuildOptions } from './util.js';
@@ -31,7 +31,15 @@ const buildPackage = async (entryName: keyof typeof packageOptions) => {
// mermaid.js // mermaid.js
{ ...iifeOptions }, { ...iifeOptions },
// mermaid.min.js // mermaid.min.js
{ ...iifeOptions, minify: true, metafile: shouldVisualize } { ...iifeOptions, minify: true, metafile: shouldVisualize },
// mermaid.tiny.min.js
{
...iifeOptions,
minify: true,
includeLargeFeatures: false,
metafile: shouldVisualize,
sourcemap: false,
}
); );
} }
if (entryName === 'mermaid-zenuml') { if (entryName === 'mermaid-zenuml') {
@@ -70,6 +78,20 @@ const handler = (e) => {
process.exit(1); process.exit(1);
}; };
const buildTinyMermaid = async () => {
await mkdir('./packages/tiny/dist', { recursive: true });
await rename(
'./packages/mermaid/dist/mermaid.tiny.min.js',
'./packages/tiny/dist/mermaid.tiny.js'
);
// Copy version from mermaid's package.json to tiny's package.json
const mermaidPkg = JSON.parse(await readFile('./packages/mermaid/package.json', 'utf8'));
const tinyPkg = JSON.parse(await readFile('./packages/tiny/package.json', 'utf8'));
tinyPkg.version = mermaidPkg.version;
await writeFile('./packages/tiny/package.json', JSON.stringify(tinyPkg, null, 2) + '\n');
};
const main = async () => { const main = async () => {
await generateLangium(); await generateLangium();
await mkdir('stats', { recursive: true }); await mkdir('stats', { recursive: true });
@@ -78,6 +100,7 @@ const main = async () => {
for (const pkg of packageNames) { for (const pkg of packageNames) {
await buildPackage(pkg).catch(handler); await buildPackage(pkg).catch(handler);
} }
await buildTinyMermaid();
}; };
void main(); void main();

View File

@@ -14,6 +14,7 @@ export interface MermaidBuildOptions extends BuildOptions {
metafile: boolean; metafile: boolean;
format: 'esm' | 'iife'; format: 'esm' | 'iife';
options: PackageOptions; options: PackageOptions;
includeLargeFeatures: boolean;
} }
export const defaultOptions: Omit<MermaidBuildOptions, 'entryName' | 'options'> = { export const defaultOptions: Omit<MermaidBuildOptions, 'entryName' | 'options'> = {
@@ -21,6 +22,7 @@ export const defaultOptions: Omit<MermaidBuildOptions, 'entryName' | 'options'>
metafile: false, metafile: false,
core: false, core: false,
format: 'esm', format: 'esm',
includeLargeFeatures: true,
} as const; } as const;
const buildOptions = (override: BuildOptions): BuildOptions => { const buildOptions = (override: BuildOptions): BuildOptions => {
@@ -39,12 +41,18 @@ const buildOptions = (override: BuildOptions): BuildOptions => {
}; };
}; };
const getFileName = (fileName: string, { core, format, minify }: MermaidBuildOptions) => { const getFileName = (
fileName: string,
{ core, format, minify, includeLargeFeatures }: MermaidBuildOptions
) => {
if (core) { if (core) {
fileName += '.core'; fileName += '.core';
} else if (format === 'esm') { } else if (format === 'esm') {
fileName += '.esm'; fileName += '.esm';
} }
if (!includeLargeFeatures) {
fileName += '.tiny';
}
if (minify) { if (minify) {
fileName += '.min'; fileName += '.min';
} }
@@ -54,25 +62,27 @@ const getFileName = (fileName: string, { core, format, minify }: MermaidBuildOpt
export const getBuildConfig = (options: MermaidBuildOptions): BuildOptions => { export const getBuildConfig = (options: MermaidBuildOptions): BuildOptions => {
const { const {
core, core,
metafile,
format, format,
minify,
options: { name, file, packageName }, options: { name, file, packageName },
globalName = 'mermaid', globalName = 'mermaid',
includeLargeFeatures,
...rest
} = options; } = options;
const external: string[] = ['require', 'fs', 'path']; const external: string[] = ['require', 'fs', 'path'];
const outFileName = getFileName(name, options); const outFileName = getFileName(name, options);
const output: BuildOptions = buildOptions({ const output: BuildOptions = buildOptions({
...rest,
absWorkingDir: resolve(__dirname, `../packages/${packageName}`), absWorkingDir: resolve(__dirname, `../packages/${packageName}`),
entryPoints: { entryPoints: {
[outFileName]: `src/${file}`, [outFileName]: `src/${file}`,
}, },
metafile,
minify,
globalName, globalName,
logLevel: 'info', logLevel: 'info',
chunkNames: `chunks/${outFileName}/[name]-[hash]`, chunkNames: `chunks/${outFileName}/[name]-[hash]`,
define: { define: {
// This needs to be stringified for esbuild
includeLargeFeatures: `${includeLargeFeatures}`,
'import.meta.vitest': 'undefined', 'import.meta.vitest': 'undefined',
}, },
}); });

View File

@@ -58,7 +58,7 @@ jobs:
echo "EOF" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT
- name: Commit and create pull request - name: Commit and create pull request
uses: peter-evans/create-pull-request@3b1f4bffdc97d7b055dd96732d7348e585ad2c4e uses: peter-evans/create-pull-request@889dce9eaba7900ce30494f5e1ac7220b27e5c81
with: with:
add-paths: | add-paths: |
cypress/timings.json cypress/timings.json

View File

@@ -94,6 +94,10 @@ export const getBuildConfig = ({ minify, core, watch, entryName }: BuildOptions)
}), }),
...visualizerOptions(packageName, core), ...visualizerOptions(packageName, core),
], ],
define: {
// Needs to be string
includeLargeFeatures: 'true',
},
}; };
if (watch && config.build) { if (watch && config.build) {

View File

@@ -934,4 +934,43 @@ graph TD
} }
); );
}); });
it('68: should honor subgraph direction when inheritDir is false', () => {
imgSnapshotTest(
`
%%{init: {"flowchart": { "inheritDir": false }}}%%
flowchart TB
direction LR
subgraph A
direction TB
a --> b
end
subgraph B
c --> d
end
`,
{
fontFamily: 'courier',
}
);
});
it('69: should inherit global direction when inheritDir is true', () => {
imgSnapshotTest(
`
%%{init: {"flowchart": { "inheritDir": true }}}%%
flowchart TB
direction LR
subgraph A
direction TB
a --> b
end
subgraph B
c --> d
end
`,
{
fontFamily: 'courier',
}
);
});
}); });

222
demos/er-multiline.html Normal file
View File

@@ -0,0 +1,222 @@
<html>
<head>
<link href="https://fonts.googleapis.com/css?family=Montserrat&display=swap" rel="stylesheet" />
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
/>
<link
href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css"
rel="stylesheet"
/>
<link
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css?family=Noto+Sans+SC&display=swap"
rel="stylesheet"
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Caveat:wght@400..700&family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Recursive:wght@300..1000&display=swap"
rel="stylesheet"
/>
<style>
.recursive-500 {
font-family: 'Recursive', serif;
font-optical-sizing: auto;
font-weight: 500;
font-style: normal;
font-variation-settings:
'slnt' 0,
'CASL' 0,
'CRSV' 0.5,
'MONO' 0;
}
body {
/* background: rgb(221, 208, 208); */
/* background: #333; */
/* font-family: 'Arial'; */
font-family: 'Recursive', serif;
font-optical-sizing: auto;
font-weight: 500;
font-style: normal;
font-variation-settings:
'slnt' 0,
'CASL' 0,
'CRSV' 0.5,
'MONO' 0;
/* color: white; */
/* font-size: 18px !important; */
}
.gridify.tiny {
background-image:
linear-gradient(transparent 11px, rgba(220, 220, 200, 0.8) 12px, transparent 12px),
linear-gradient(90deg, transparent 11px, rgba(220, 220, 200, 0.8) 12px, transparent 12px);
background-size:
100% 12px,
12px 100%;
}
.gridify.dots {
background-image: radial-gradient(
circle at center,
rgba(220, 220, 200, 0.8) 1px,
transparent 1px
);
background-size: 24px 24px;
}
h1 {
color: grey;
}
.mermaid2 {
display: none;
}
.mermaid svg {
font-size: 16px !important;
font-family: 'Recursive', serif;
font-optical-sizing: auto;
font-weight: 500;
font-style: normal;
font-variation-settings:
'slnt' 0,
'CASL' 0,
'CRSV' 0.5,
'MONO' 0;
}
pre {
width: 100%;
/*box-shadow: 4px 4px 0px 0px #0000000F;*/
}
</style>
</head>
<body class="gridify dots">
<div class="w-full h-64">
<pre id="diagram4" class="mermaid" style="background: rgb(255, 255, 255)">
erDiagram
CAR ||--o{ NAMED-DRIVER : allows
CAR ::: Pine {
string registrationNumber PK "Primary Key<br><strong>Unique registration number</strong>"
string make "Car make<br><strong>e.g., Toyota</strong>"
string model "Model of the car<br><strong>e.g., Corolla</strong>"
string[] parts "List of parts<br><strong>Stored as array</strong>"
}
PERSON ||--o{ NAMED-DRIVER : is
PERSON ::: someclass {
string driversLicense PK "The license #<br><strong>Primary Key</strong>"
string(99) firstName "Only 99 characters <br>are allowed <br> <strong>e.g., Smith</strong>"
string lastName "Last name of person<br><strong>e.g., Smith</strong>"
string phone UK "Unique phone number<br><strong>Used for contact</strong>"
int age "Age of the person<br><strong>Must be numeric</strong>"
}
NAMED-DRIVER {
string carRegistrationNumber PK, FK, UK, PK "Foreign key to CAR<br><strong>Also part of PK</strong>"
string driverLicence PK, FK "Foreign key to PERSON<br><strong>Also part of PK</strong>"
}
MANUFACTURER only one to zero or more CAR : makesx
</pre>
<hr />
<pre class="mermaid">
erDiagram
_**testẽζØ😀㌕ぼ**_ {
*__List~List~int~~sdfds__* **driversLicense** PK "***The l😀icense #***"
string last*Name*
string __phone__ UK
*string(99)~T~~~~~~* firstName "Only __99__ <br>characters are a<br>llowed dsfsdfsdfsdfs"
int _age_
}
</pre>
</div>
<script type="module">
import mermaid from './mermaid.esm.mjs';
import layouts from './mermaid-layout-elk.esm.mjs';
const staticBellIconPack = {
prefix: 'fa6-regular',
icons: {
bell: {
body: '<path fill="currentColor" d="M224 0c-17.7 0-32 14.3-32 32v19.2C119 66 64 130.6 64 208v25.4c0 45.4-15.5 89.5-43.8 124.9L5.3 377c-5.8 7.2-6.9 17.1-2.9 25.4S14.8 416 24 416h400c9.2 0 17.6-5.3 21.6-13.6s2.9-18.2-2.9-25.4l-14.9-18.6c-28.3-35.5-43.8-79.6-43.8-125V208c0-77.4-55-142-128-156.8V32c0-17.7-14.3-32-32-32m0 96c61.9 0 112 50.1 112 112v25.4c0 47.9 13.9 94.6 39.7 134.6H72.3c25.8-40 39.7-86.7 39.7-134.6V208c0-61.9 50.1-112 112-112m64 352H160c0 17 6.7 33.3 18.7 45.3S207 512 224 512s33.3-6.7 45.3-18.7S288 465 288 448"/>',
width: 448,
},
},
width: 512,
height: 512,
};
mermaid.registerIconPacks([
{
name: 'logos',
loader: () =>
fetch('https://unpkg.com/@iconify-json/logos@1/icons.json').then((res) => res.json()),
},
{
name: 'fa',
loader: () => staticBellIconPack,
},
]);
mermaid.registerLayoutLoaders(layouts);
mermaid.parseError = function (err, hash) {
console.error('Mermaid error: ', err);
};
window.callback = function () {
alert('A callback was triggered');
};
function callback() {
alert('It worked');
}
await mermaid.initialize({
startOnLoad: false,
theme: 'forest',
look: 'classic',
layout: 'dagre',
// theme: 'default',
// look: 'classic',
flowchart: { titleTopMargin: 10 },
fontFamily: 'Recursive',
sequence: {
actorFontFamily: 'courier',
noteFontFamily: 'courier',
messageFontFamily: 'courier',
},
kanban: {
htmlLabels: false,
},
fontSize: 16,
logLevel: 0,
securityLevel: 'loose',
callback,
});
// setTimeout(() => {
mermaid.init(undefined, document.querySelectorAll('.mermaid'));
// }, 1000);
mermaid.parseError = function (err, hash) {
console.error('In parse error:');
console.error(err);
};
</script>
</body>
</html>

View File

@@ -12,4 +12,4 @@
> `const` **configKeys**: `Set`<`string`> > `const` **configKeys**: `Set`<`string`>
Defined in: [packages/mermaid/src/defaultConfig.ts:285](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L285) Defined in: [packages/mermaid/src/defaultConfig.ts:289](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L289)

View File

@@ -98,6 +98,12 @@ Mermaid can load multiple diagrams, in the same page.
> Try it out, save this code as HTML and load it using any browser. > Try it out, save this code as HTML and load it using any browser.
> (Except Internet Explorer, please don't use Internet Explorer.) > (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 ## 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. 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

@@ -354,6 +354,7 @@ To Deploy Mermaid:
- [Mermaid Live Editor](https://github.com/mermaid-js/mermaid-live-editor) - [Mermaid Live Editor](https://github.com/mermaid-js/mermaid-live-editor)
- [Mermaid CLI](https://github.com/mermaid-js/mermaid-cli) - [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 Webpack Demo](https://github.com/mermaidjs/mermaid-webpack-demo)
- [Mermaid Parcel Demo](https://github.com/mermaidjs/mermaid-parcel-demo) - [Mermaid Parcel Demo](https://github.com/mermaidjs/mermaid-parcel-demo)

View File

@@ -545,6 +545,38 @@ 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: 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>>
> ```
>
> ```mermaid
> classDiagram
> class Shape <<interface>>
> ```
>
> - **Separate line after the class definition**:
>
> ```mermaid-example
> classDiagram
> class Shape
> <<interface>> Shape
> ```
>
> ```mermaid
> 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: - In a **_separate line_** after a class is defined:
```mermaid-example ```mermaid-example

View File

@@ -16,13 +16,25 @@ This diagram type is particularly useful for developers, network engineers, educ
## Syntax ## Syntax
```md ```
packet-beta packet-beta
start: "Block name" %% Single-bit block start: "Block name" %% Single-bit block
start-end: "Block name" %% Multi-bit blocks start-end: "Block name" %% Multi-bit blocks
... More Fields ... ... 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 ## Examples
```mermaid-example ```mermaid-example
@@ -76,8 +88,8 @@ packet-beta
```mermaid-example ```mermaid-example
packet-beta packet-beta
title UDP Packet title UDP Packet
0-15: "Source Port" +16: "Source Port"
16-31: "Destination Port" +16: "Destination Port"
32-47: "Length" 32-47: "Length"
48-63: "Checksum" 48-63: "Checksum"
64-95: "Data (variable length)" 64-95: "Data (variable length)"
@@ -86,8 +98,8 @@ title UDP Packet
```mermaid ```mermaid
packet-beta packet-beta
title UDP Packet title UDP Packet
0-15: "Source Port" +16: "Source Port"
16-31: "Destination Port" +16: "Destination Port"
32-47: "Length" 32-47: "Length"
48-63: "Checksum" 48-63: "Checksum"
64-95: "Data (variable length)" 64-95: "Data (variable length)"

View File

@@ -69,7 +69,7 @@
"@changesets/cli": "^2.27.12", "@changesets/cli": "^2.27.12",
"@cspell/eslint-plugin": "^8.19.3", "@cspell/eslint-plugin": "^8.19.3",
"@cypress/code-coverage": "^3.12.49", "@cypress/code-coverage": "^3.12.49",
"@eslint/js": "^9.25.1", "@eslint/js": "^9.26.0",
"@rollup/plugin-typescript": "^12.1.2", "@rollup/plugin-typescript": "^12.1.2",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
@@ -93,7 +93,7 @@
"cypress-image-snapshot": "^4.0.1", "cypress-image-snapshot": "^4.0.1",
"cypress-split": "^1.24.14", "cypress-split": "^1.24.14",
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"eslint-config-prettier": "^10.1.1", "eslint-config-prettier": "^10.1.1",
"eslint-plugin-cypress": "^4.3.0", "eslint-plugin-cypress": "^4.3.0",
"eslint-plugin-html": "^8.1.2", "eslint-plugin-html": "^8.1.2",
@@ -126,7 +126,7 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"tsx": "^4.7.3", "tsx": "^4.7.3",
"typescript": "~5.7.3", "typescript": "~5.7.3",
"typescript-eslint": "^8.31.1", "typescript-eslint": "^8.32.0",
"vite": "^6.1.1", "vite": "^6.1.1",
"vite-plugin-istanbul": "^7.0.0", "vite-plugin-istanbul": "^7.0.0",
"vitest": "^3.0.6" "vitest": "^3.0.6"

View File

@@ -295,6 +295,12 @@ export interface FlowchartDiagramConfig extends BaseDiagramConfig {
* *
*/ */
wrappingWidth?: number; 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 * This interface was referenced by `MermaidConfig`'s JSON-Schema

View File

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

View File

@@ -28,6 +28,7 @@ import architecture from '../diagrams/architecture/architectureDetector.js';
import { registerLazyLoadedDiagrams } from './detectType.js'; import { registerLazyLoadedDiagrams } from './detectType.js';
import { registerDiagram } from './diagramAPI.js'; import { registerDiagram } from './diagramAPI.js';
import { treemap } from '../diagrams/treemap/detector.js'; import { treemap } from '../diagrams/treemap/detector.js';
import '../type.d.ts';
let hasLoadedDiagrams = false; let hasLoadedDiagrams = false;
export const addDiagrams = () => { export const addDiagrams = () => {
@@ -70,6 +71,11 @@ export const addDiagrams = () => {
return text.toLowerCase().trimStart().startsWith('---'); 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. // Ordering of detectors is important. The first one to return true will be used.
registerLazyLoadedDiagrams( registerLazyLoadedDiagrams(
c4, c4,
@@ -82,10 +88,8 @@ export const addDiagrams = () => {
pie, pie,
requirement, requirement,
sequence, sequence,
flowchartElk,
flowchartV2, flowchartV2,
flowchart, flowchart,
mindmap,
timeline, timeline,
git, git,
stateV2, stateV2,
@@ -96,7 +100,6 @@ export const addDiagrams = () => {
packet, packet,
xychart, xychart,
block, block,
architecture,
radar, radar,
treemap treemap
); );

View File

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

View File

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

View File

@@ -1,7 +1,9 @@
import type { InfoFields, InfoDB } from './infoTypes.js'; import type { InfoFields, InfoDB } from './infoTypes.js';
import packageJson from '../../../package.json' assert { type: 'json' }; 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; export const getVersion = (): string => DEFAULT_INFO_DB.version;

View File

@@ -30,6 +30,7 @@ describe('packet diagrams', () => {
[ [
[ [
{ {
"bits": 11,
"end": 10, "end": 10,
"label": "test", "label": "test",
"start": 0, "start": 0,
@@ -49,11 +50,13 @@ describe('packet diagrams', () => {
[ [
[ [
{ {
"bits": 11,
"end": 10, "end": 10,
"label": "test", "label": "test",
"start": 0, "start": 0,
}, },
{ {
"bits": 1,
"end": 11, "end": 11,
"label": "single", "label": "single",
"start": 11, "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 () => { it('should split into multiple rows', async () => {
const str = `packet-beta const str = `packet-beta
0-10: "test" 0-10: "test"
@@ -73,11 +128,13 @@ describe('packet diagrams', () => {
[ [
[ [
{ {
"bits": 11,
"end": 10, "end": 10,
"label": "test", "label": "test",
"start": 0, "start": 0,
}, },
{ {
"bits": 20,
"end": 31, "end": 31,
"label": "multiple", "label": "multiple",
"start": 11, "start": 11,
@@ -85,6 +142,7 @@ describe('packet diagrams', () => {
], ],
[ [
{ {
"bits": 31,
"end": 63, "end": 63,
"label": "multiple", "label": "multiple",
"start": 32, "start": 32,
@@ -92,6 +150,7 @@ describe('packet diagrams', () => {
], ],
[ [
{ {
"bits": 26,
"end": 90, "end": 90,
"label": "multiple", "label": "multiple",
"start": 64, "start": 64,
@@ -111,11 +170,13 @@ describe('packet diagrams', () => {
[ [
[ [
{ {
"bits": 17,
"end": 16, "end": 16,
"label": "test", "label": "test",
"start": 0, "start": 0,
}, },
{ {
"bits": 14,
"end": 31, "end": 31,
"label": "multiple", "label": "multiple",
"start": 17, "start": 17,
@@ -123,6 +184,7 @@ describe('packet diagrams', () => {
], ],
[ [
{ {
"bits": 31,
"end": 63, "end": 63,
"label": "multiple", "label": "multiple",
"start": 32, "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 () => { it('should throw error if numbers are not continuous for single packets', async () => {
const str = `packet-beta const str = `packet-beta
0-16: "test" 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 () => { it('should throw error if numbers are not continuous for single packets - 2', async () => {
const str = `packet-beta const str = `packet-beta
0-16: "test" 0-16: "test"
@@ -172,4 +254,13 @@ describe('packet diagrams', () => {
`[Error: Packet block 25 - 20 is invalid. End must be greater than start.]` `[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) => { const populate = (ast: Packet) => {
populateCommonDb(ast, db); populateCommonDb(ast, db);
let lastByte = -1; let lastBit = -1;
let word: PacketWord = []; let word: PacketWord = [];
let row = 1; let row = 1;
const { bitsPerRow } = db.getConfig(); 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.`); 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( throw new Error(
`Packet block ${start} - ${end ?? start} is not contiguous. It should start from ${ `Packet block ${start} - ${end ?? start} is not contiguous. It should start from ${
lastByte + 1 lastBit + 1
}.` }.`
); );
} }
lastByte = end ?? start; if (bits === 0) {
log.debug(`Packet block ${start} - ${lastByte} with label ${label}`); 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) { 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); word.push(block);
if (block.end + 1 === row * bitsPerRow) { if (block.end + 1 === row * bitsPerRow) {
db.pushWord(word); db.pushWord(word);
@@ -39,7 +46,7 @@ const populate = (ast: Packet) => {
if (!nextBlock) { if (!nextBlock) {
break; break;
} }
({ start, end, label } = nextBlock); ({ start, end, bits, label } = nextBlock);
} }
} }
db.pushWord(word); db.pushWord(word);
@@ -50,8 +57,11 @@ const getNextFittingBlock = (
row: number, row: number,
bitsPerRow: number bitsPerRow: number
): [Required<PacketBlock>, PacketBlock | undefined] => { ): [Required<PacketBlock>, PacketBlock | undefined] => {
if (block.start === undefined) {
throw new Error('start should have been set during first phase');
}
if (block.end === undefined) { if (block.end === undefined) {
block.end = block.start; throw new Error('end should have been set during first phase');
} }
if (block.start > block.end) { if (block.start > block.end) {
@@ -62,16 +72,20 @@ const getNextFittingBlock = (
return [block as Required<PacketBlock>, undefined]; return [block as Required<PacketBlock>, undefined];
} }
const rowEnd = row * bitsPerRow - 1;
const rowStart = row * bitsPerRow;
return [ return [
{ {
start: block.start, start: block.start,
end: row * bitsPerRow - 1, end: rowEnd,
label: block.label, label: block.label,
bits: rowEnd - block.start,
}, },
{ {
start: row * bitsPerRow, start: rowStart,
end: block.end, end: block.end,
label: block.label, 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. > Try it out, save this code as HTML and load it using any browser.
> (Except Internet Explorer, please don't use Internet Explorer.) > (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 ## 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. 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 Live Editor](https://github.com/mermaid-js/mermaid-live-editor)
- [Mermaid CLI](https://github.com/mermaid-js/mermaid-cli) - [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 Webpack Demo](https://github.com/mermaidjs/mermaid-webpack-demo)
- [Mermaid Parcel Demo](https://github.com/mermaidjs/mermaid-parcel-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: 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: - In a **_separate line_** after a class is defined:
```mermaid-example ```mermaid-example

View File

@@ -10,13 +10,25 @@ This diagram type is particularly useful for developers, network engineers, educ
## Syntax ## Syntax
```md ```
packet-beta packet-beta
start: "Block name" %% Single-bit block start: "Block name" %% Single-bit block
start-end: "Block name" %% Multi-bit blocks start-end: "Block name" %% Multi-bit blocks
... More Fields ... ... 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 ## Examples
```mermaid-example ```mermaid-example
@@ -46,8 +58,8 @@ packet-beta
```mermaid-example ```mermaid-example
packet-beta packet-beta
title UDP Packet title UDP Packet
0-15: "Source Port" +16: "Source Port"
16-31: "Destination Port" +16: "Destination Port"
32-47: "Length" 32-47: "Length"
48-63: "Checksum" 48-63: "Checksum"
64-95: "Data (variable length)" 64-95: "Data (variable length)"

View File

@@ -5,7 +5,6 @@
// @ts-ignore TODO: Investigate D3 issue // @ts-ignore TODO: Investigate D3 issue
import { select } from 'd3'; import { select } from 'd3';
import { compile, serialize, stringify } from 'stylis'; import { compile, serialize, stringify } from 'stylis';
// @ts-ignore: TODO Fix ts errors
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import isEmpty from 'lodash-es/isEmpty.js'; import isEmpty from 'lodash-es/isEmpty.js';
import packageJson from '../package.json' assert { type: 'json' }; 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; nameBBox.height += TEXT_PADDING;
let yOffset = 0; let yOffset = 0;
const yOffsets = []; const yOffsets = [];
const rows = [];
let maxTypeWidth = 0; let maxTypeWidth = 0;
let maxNameWidth = 0; let maxNameWidth = 0;
let maxKeysWidth = 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); maxCommentWidth = Math.max(maxCommentWidth, commentBBox.width + PADDING);
yOffset += const rowHeight =
Math.max(typeBBox.height, nameBBox.height, keysBBox.height, commentBBox.height) + Math.max(typeBBox.height, nameBBox.height, keysBBox.height, commentBBox.height) +
TEXT_PADDING; TEXT_PADDING;
yOffsets.push(yOffset); rows.push({ yOffset, rowHeight });
yOffset += rowHeight;
} }
yOffsets.pop();
let totalWidthSections = 4; let totalWidthSections = 4;
if (maxKeysWidth <= PADDING) { if (maxKeysWidth <= PADDING) {
@@ -185,8 +186,12 @@ export async function erBox<T extends SVGGraphicsElement>(parent: D3Selection<T>
options.fillStyle = 'solid'; 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 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 x = -w / 2;
const y = -h / 2; const y = -h / 2;
@@ -232,13 +237,10 @@ export async function erBox<T extends SVGGraphicsElement>(parent: D3Selection<T>
yOffsets.push(0); yOffsets.push(0);
// Draw row rects // Draw row rects
for (const [i, yOffset] of yOffsets.entries()) { for (const [i, row] of rows.entries()) {
if (i === 0 && yOffsets.length > 1) { const contentRowIndex = i + 1; // Adjusted index to skip the header (name) row
continue; const isEven = contentRowIndex % 2 === 0 && row.yOffset !== 0;
// Skip first row const roughRect = rc.rectangle(x, nameBBox.height + y + row?.yOffset, w, row?.rowHeight, {
}
const isEven = i % 2 === 0 && yOffset !== 0;
const roughRect = rc.rectangle(x, nameBBox.height + y + yOffset, w, nameBBox.height, {
...options, ...options,
fill: isEven ? rowEven : rowOdd, fill: isEven ? rowEven : rowOdd,
stroke: nodeBorder, stroke: nodeBorder,
@@ -246,7 +248,7 @@ export async function erBox<T extends SVGGraphicsElement>(parent: D3Selection<T>
shapeSvg shapeSvg
.insert(() => roughRect, 'g.label') .insert(() => roughRect, 'g.label')
.attr('style', cssStyles!.join('')) .attr('style', cssStyles!.join(''))
.attr('class', `row-rect-${i % 2 === 0 ? 'even' : 'odd'}`); .attr('class', `row-rect-${isEven ? 'even' : 'odd'}`);
} }
// Draw divider lines // Draw divider lines

View File

@@ -2124,6 +2124,13 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
type: number type: number
default: 200 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: SankeyLinkColor:
description: | description: |
Picks the color of the sankey diagram links, using the colors of the source and/or target of the links. 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: 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"
}
}

608
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -35,6 +35,8 @@ export default defineConfig({
}, },
}, },
define: { define: {
// Needs to be string
includeLargeFeatures: 'true',
'import.meta.vitest': 'undefined', 'import.meta.vitest': 'undefined',
}, },
}); });