Merge branch 'add-pie-langium-parser' of github.com:Yokozuna59/mermaid into pr/Yokozuna59/4751

* 'add-pie-langium-parser' of github.com:Yokozuna59/mermaid: (48 commits)
  make pie parser async
  Changes to gantt.html 1. Added a Gantt diagram that demonstrates to users that hashtages and semicolons can be added to titles, sections, and task data. Changes to gantt.spec.js 1. Added unit tests to ensure that semicolons and hashtags didn't break the functionality of the gantt diagram when used in titles, sections or task data. Changes to /parser/gantt.spec.js 1. Added rendering tests to ensure that semicolons and hashtags in titles, sections, and task data didn't break the rendering of Gantt diagrams.
  perf: prevent adding multiple DOMPurify hooks
  Update docs
  chore: Update tests
  Fix types
  refactor: Make parser.parse async
  refactor: Support async parsers Add `Diagram.fromText`
  Lint
  Remove echo
  RefTest
  Echo event
  Update cypress
  Fix applitools
  Fix applitools
  add sequenceDiagram link e2e test
  fix sequence diagram popup
  Changes to gantt.jison 1. Consistent spacing on line 30
  Changes to gantt.jison 1. Removed typo
  Changes to gnatt.jison 1. Removed the hash and semicolon symbols from the title regex to allow for their use. 2. Removed the hash and semicolon symbols from the section regex to allow for their use. 3. Removed the hash and semicolon symbols for the taskTxt regex to allow for their use. I did not remove the colon because the parser fails to recognize when the actual taskData begins if that distinctions isn't kept. 4. Removed the regex \#[^\n]* which skipped comments to fix some bugs with hash symbols in the taskTxt. I tested this changed by putting it back and using the comment  to see if it was recognized as a comment, but I would receive a syntax error and the diagram would not be rendered. So, I think we can safely remove that line, BUT it would be best practice if someone else tested this change to ensure that this will not break anyone's Gantt diagrams.
  ...
This commit is contained in:
Sidharth Vinod 2024-02-11 19:36:26 +05:30
commit b30d609d19
No known key found for this signature in database
GPG Key ID: FB5CCD378D3907CD
74 changed files with 2007 additions and 862 deletions

View File

@ -24,6 +24,7 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [
'gitGraph',
'c4',
'sankey',
'packet',
] as const;
/**

View File

@ -1,8 +1,8 @@
import { build } from 'esbuild';
import { mkdir, writeFile } from 'node:fs/promises';
import { MermaidBuildOptions, defaultOptions, getBuildConfig } from './util.js';
import { packageOptions } from '../.build/common.js';
import { generateLangium } from '../.build/generateLangium.js';
import { MermaidBuildOptions, defaultOptions, getBuildConfig } from './util.js';
const shouldVisualize = process.argv.includes('--visualize');
@ -56,7 +56,7 @@ const main = async () => {
await generateLangium();
await mkdir('stats').catch(() => {});
const packageNames = Object.keys(packageOptions) as (keyof typeof packageOptions)[];
// it should build `parser` before `mermaid` because it's a dependecy
// it should build `parser` before `mermaid` because it's a dependency
for (const pkg of packageNames) {
await buildPackage(pkg).catch(handler);
}

View File

@ -18,7 +18,7 @@ permissions:
env:
# For PRs and MergeQueues, the target commit is used, and for push events, github.event.previous is used.
targetHash: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || github.event.before }}
targetHash: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || (github.event.before == '0000000000000000000000000000000000000000' && 'develop' || github.event.before) }}
jobs:
cache:
@ -30,7 +30,6 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 18.x
- name: Cache snapshots
id: cache-snapshot
uses: actions/cache@v4

View File

@ -1,21 +0,0 @@
/**
* Mocked C4Context diagram renderer
*/
import { vi } from 'vitest';
export const drawPersonOrSystemArray = vi.fn();
export const drawBoundary = vi.fn();
export const setConf = vi.fn();
export const draw = vi.fn().mockImplementation(() => {
return '';
});
export default {
drawPersonOrSystemArray,
drawBoundary,
setConf,
draw,
};

View File

@ -1,16 +0,0 @@
/**
* Mocked class diagram v2 renderer
*/
import { vi } from 'vitest';
export const setConf = vi.fn();
export const draw = vi.fn().mockImplementation(() => {
return '';
});
export default {
setConf,
draw,
};

View File

@ -1,13 +0,0 @@
/**
* Mocked class diagram renderer
*/
import { vi } from 'vitest';
export const draw = vi.fn().mockImplementation(() => {
return '';
});
export default {
draw,
};

View File

@ -1 +0,0 @@
// DO NOT delete this file. It is used by vitest to mock the dagre-d3 module.

View File

@ -1,3 +0,0 @@
module.exports = function (txt: string) {
return txt;
};

View File

@ -1,16 +0,0 @@
/**
* Mocked er diagram renderer
*/
import { vi } from 'vitest';
export const setConf = vi.fn();
export const draw = vi.fn().mockImplementation(() => {
return '';
});
export default {
setConf,
draw,
};

View File

@ -1,24 +0,0 @@
/**
* Mocked flow (flowchart) diagram v2 renderer
*/
import { vi } from 'vitest';
export const setConf = vi.fn();
export const addVertices = vi.fn();
export const addEdges = vi.fn();
export const getClasses = vi.fn().mockImplementation(() => {
return {};
});
export const draw = vi.fn().mockImplementation(() => {
return '';
});
export default {
setConf,
addVertices,
addEdges,
getClasses,
draw,
};

View File

@ -1,16 +0,0 @@
/**
* Mocked gantt diagram renderer
*/
import { vi } from 'vitest';
export const setConf = vi.fn();
export const draw = vi.fn().mockImplementation(() => {
return '';
});
export default {
setConf,
draw,
};

View File

@ -1,13 +0,0 @@
/**
* Mocked git (graph) diagram renderer
*/
import { vi } from 'vitest';
export const draw = vi.fn().mockImplementation(() => {
return '';
});
export default {
draw,
};

View File

@ -1,15 +0,0 @@
/**
* Mocked pie (picChart) diagram renderer
*/
import { vi } from 'vitest';
export const setConf = vi.fn();
export const draw = vi.fn().mockImplementation(() => {
return '';
});
export default {
setConf,
draw,
};

View File

@ -1,8 +0,0 @@
/**
* Mocked pie (picChart) diagram renderer
*/
import { vi } from 'vitest';
const draw = vi.fn().mockImplementation(() => '');
export const renderer = { draw };

View File

@ -1,13 +0,0 @@
/**
* Mocked requirement diagram renderer
*/
import { vi } from 'vitest';
export const draw = vi.fn().mockImplementation(() => {
return '';
});
export default {
draw,
};

View File

@ -1,13 +0,0 @@
/**
* Mocked Sankey diagram renderer
*/
import { vi } from 'vitest';
export const draw = vi.fn().mockImplementation(() => {
return '';
});
export default {
draw,
};

View File

@ -1,23 +0,0 @@
/**
* Mocked sequence diagram renderer
*/
import { vi } from 'vitest';
export const bounds = vi.fn();
export const drawActors = vi.fn();
export const drawActorsPopup = vi.fn();
export const setConf = vi.fn();
export const draw = vi.fn().mockImplementation(() => {
return '';
});
export default {
bounds,
drawActors,
drawActorsPopup,
setConf,
draw,
};

View File

@ -1,22 +0,0 @@
/**
* Mocked state diagram v2 renderer
*/
import { vi } from 'vitest';
export const setConf = vi.fn();
export const getClasses = vi.fn().mockImplementation(() => {
return {};
});
export const stateDomId = vi.fn().mockImplementation(() => {
return 'mocked-stateDiagram-stateDomId';
});
export const draw = vi.fn().mockImplementation(() => {
return '';
});
export default {
setConf,
getClasses,
draw,
};

View File

@ -1,19 +0,0 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { defineConfig } = require('cypress');
module.exports = defineConfig({
testConcurrency: 1,
browser: [
// Add browsers with different viewports
// { width: 800, height: 600, name: 'chrome' },
// { width: 700, height: 500, name: 'firefox' },
// { width: 1600, height: 1200, name: 'ie11' },
// { width: 1024, height: 768, name: 'edgechromium' },
// { width: 800, height: 600, name: 'safari' },
// // Add mobile emulation devices in Portrait mode
// { deviceName: 'iPhone X', screenOrientation: 'portrait' },
// { deviceName: 'Pixel 2', screenOrientation: 'portrait' },
],
// set batch name to the configuration
// batchName: `Mermaid ${process.env.APPLI_BRANCH ?? "'no APPLI_BRANCH set'"}`,
});

View File

@ -1,32 +0,0 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { defineConfig } = require('cypress');
const { addMatchImageSnapshotPlugin } = require('cypress-image-snapshot/plugin');
const coverage = require('@cypress/code-coverage/task');
module.exports = defineConfig({
projectId: 'n2sma2',
viewportWidth: 1440,
viewportHeight: 1024,
e2e: {
specPattern: 'cypress/integration/**/*.{js,ts}',
setupNodeEvents(on, config) {
coverage(on, config);
on('before:browser:launch', (browser = {}, launchOptions) => {
if (browser.name === 'chrome' && browser.isHeadless) {
launchOptions.args.push('--window-size=1440,1024', '--force-device-scale-factor=1');
}
return launchOptions;
});
addMatchImageSnapshotPlugin(on, config);
// copy any needed variables from process.env to config.env
config.env.useAppli = process.env.USE_APPLI ? true : false;
// do not forget to return the changed config object!
return config;
},
},
video: false,
});
require('@applitools/eyes-cypress')(module);

30
cypress.config.ts Normal file
View File

@ -0,0 +1,30 @@
import { defineConfig } from 'cypress';
import { addMatchImageSnapshotPlugin } from 'cypress-image-snapshot/plugin';
import coverage from '@cypress/code-coverage/task';
import eyesPlugin from '@applitools/eyes-cypress';
export default eyesPlugin(
defineConfig({
projectId: 'n2sma2',
viewportWidth: 1440,
viewportHeight: 1024,
e2e: {
specPattern: 'cypress/integration/**/*.{js,ts}',
setupNodeEvents(on, config) {
coverage(on, config);
on('before:browser:launch', (browser, launchOptions) => {
if (browser.name === 'chrome' && browser.isHeadless) {
launchOptions.args.push('--window-size=1440,1024', '--force-device-scale-factor=1');
}
return launchOptions;
});
addMatchImageSnapshotPlugin(on, config);
// copy any needed variables from process.env to config.env
config.env.useAppli = process.env.USE_APPLI ? true : false;
// do not forget to return the changed config object!
return config;
},
},
video: false,
})
);

View File

@ -583,4 +583,106 @@ describe('Gantt diagram', () => {
{}
);
});
it("should render when there's a semicolon in the title", () => {
imgSnapshotTest(
`
gantt
title ;Gantt With a Semicolon in the Title
dateFormat YYYY-MM-DD
section Section
A task :a1, 2014-01-01, 30d
Another task :after a1 , 20d
section Another
Task in sec :2014-01-12 , 12d
another task : 24d
`,
{}
);
});
it("should render when there's a semicolon in a section is true", () => {
imgSnapshotTest(
`
gantt
title Gantt Digram
dateFormat YYYY-MM-DD
section ;Section With a Semicolon
A task :a1, 2014-01-01, 30d
Another task :after a1 , 20d
section Another
Task in sec :2014-01-12 , 12d
another task : 24d
`,
{}
);
});
it("should render when there's a semicolon in the task data", () => {
imgSnapshotTest(
`
gantt
title Gantt Digram
dateFormat YYYY-MM-DD
section Section
;A task with a semiclon :a1, 2014-01-01, 30d
Another task :after a1 , 20d
section Another
Task in sec :2014-01-12 , 12d
another task : 24d
`,
{}
);
});
it("should render when there's a hashtag in the title", () => {
imgSnapshotTest(
`
gantt
title #Gantt With a Hashtag in the Title
dateFormat YYYY-MM-DD
section Section
A task :a1, 2014-01-01, 30d
Another task :after a1 , 20d
section Another
Task in sec :2014-01-12 , 12d
another task : 24d
`,
{}
);
});
it("should render when there's a hashtag in a section is true", () => {
imgSnapshotTest(
`
gantt
title Gantt Digram
dateFormat YYYY-MM-DD
section #Section With a Hashtag
A task :a1, 2014-01-01, 30d
Another task :after a1 , 20d
section Another
Task in sec :2014-01-12 , 12d
another task : 24d
`,
{}
);
});
it("should render when there's a hashtag in the task data", () => {
imgSnapshotTest(
`
gantt
title Gantt Digram
dateFormat YYYY-MM-DD
section Section
#A task with a hashtag :a1, 2014-01-01, 30d
Another task :after a1 , 20d
section Another
Task in sec :2014-01-12 , 12d
another task : 24d
`,
{}
);
});
});

View File

@ -0,0 +1,67 @@
import { imgSnapshotTest } from '../../helpers/util';
describe('packet structure', () => {
it('should render a simple packet diagram', () => {
imgSnapshotTest(
`packet-beta
title Hello world
0-10: "hello"
`
);
});
it('should render a complex packet diagram', () => {
imgSnapshotTest(
`packet-beta
0-15: "Source Port"
16-31: "Destination Port"
32-63: "Sequence Number"
64-95: "Acknowledgment Number"
96-99: "Data Offset"
100-105: "Reserved"
106: "URG"
107: "ACK"
108: "PSH"
109: "RST"
110: "SYN"
111: "FIN"
112-127: "Window"
128-143: "Checksum"
144-159: "Urgent Pointer"
160-191: "(Options and Padding)"
192-223: "data"
`
);
});
it('should render a complex packet diagram with showBits false', () => {
imgSnapshotTest(
`
---
title: "Packet Diagram"
config:
packet:
showBits: false
---
packet-beta
0-15: "Source Port"
16-31: "Destination Port"
32-63: "Sequence Number"
64-95: "Acknowledgment Number"
96-99: "Data Offset"
100-105: "Reserved"
106: "URG"
107: "ACK"
108: "PSH"
109: "RST"
110: "SYN"
111: "FIN"
112-127: "Window"
128-143: "Checksum"
144-159: "Urgent Pointer"
160-191: "(Options and Padding)"
192-223: "data"
`
);
});
});

View File

@ -792,6 +792,34 @@ context('Sequence diagram', () => {
});
});
context('links', () => {
it('should support actor links', () => {
renderGraph(
`
sequenceDiagram
link Alice: Dashboard @ https://dashboard.contoso.com/alice
link Alice: Wiki @ https://wiki.contoso.com/alice
link John: Dashboard @ https://dashboard.contoso.com/john
link John: Wiki @ https://wiki.contoso.com/john
Alice->>John: Hello John<br/>
John-->>Alice: Great<br/><br/>day!
`,
{ securityLevel: 'loose' }
);
cy.get('#actor0_popup').should((popupMenu) => {
const style = popupMenu.attr('style');
expect(style).to.undefined;
});
cy.get('#root-0').click();
cy.get('#actor0_popup').should((popupMenu) => {
const style = popupMenu.attr('style');
expect(style).to.match(/^display: block;$/);
});
cy.get('#root-0').click();
cy.get('#actor0_popup').should((popupMenu) => {
const style = popupMenu.attr('style');
expect(style).to.match(/^display: none;$/);
});
});
it('should support actor links and properties EXPERIMENTAL: USE WITH CAUTION', () => {
//Be aware that the syntax for "properties" is likely to be changed.
imgSnapshotTest(

View File

@ -3,10 +3,24 @@
<html>
<head>
<title>Mermaid development page</title>
<style>
.container {
display: flex;
flex-direction: row;
}
#code {
max-width: 30vw;
width: 30vw;
}
#dynamicDiagram {
padding-left: 2em;
flex: 1;
}
</style>
</head>
<body>
<pre class="mermaid">info</pre>
<pre id="diagram" class="mermaid">
graph TB
a --> b
@ -15,22 +29,35 @@ graph TB
c --> d
</pre>
<div id="dynamicDiagram"></div>
<hr />
Type code to view diagram:
<div class="container">
<textarea name="code" id="code" cols="30" rows="10"></textarea>
<div id="dynamicDiagram"></div>
</div>
<pre class="mermaid">info</pre>
<script type="module">
import mermaid from '/mermaid.esm.mjs';
mermaid.parseError = function (err, hash) {
console.error('Mermaid error: ', err);
};
async function render(str) {
const { svg } = await mermaid.render('dynamic', str);
document.getElementById('dynamicDiagram').innerHTML = svg;
}
const storeKey = window.location.pathname;
const code = localStorage.getItem(storeKey);
if (code) {
document.getElementById('code').value = code;
await render(code);
}
mermaid.initialize({
startOnLoad: true,
logLevel: 0,
});
const value = `graph TD\nHello --> World`;
const el = document.getElementById('dynamicDiagram');
const { svg } = await mermaid.render('dd', value);
console.log(svg);
el.innerHTML = svg;
document.getElementById('code').addEventListener('input', async (e) => {
const value = e.target.value;
localStorage.setItem(storeKey, value);
await render(value);
});
</script>
<script src="/dev/reload.js"></script>

View File

@ -30,6 +30,21 @@
</pre>
<hr />
<pre class="mermaid">
gantt
title #; Gantt Diagrams Allow Semicolons and Hashtags #;!
accTitle: A simple sample gantt diagram
accDescr: 2 sections with 2 tasks each, from 2014
dateFormat YYYY-MM-DD
section #;Section
#;A task :a1, 2014-01-01, 30d
#;Another task :after a1 , 20d
section #;Another
Task in sec :2014-01-12 , 12d
another task : 24d
</pre>
<hr />
<pre class="mermaid">
gantt
title Airworks roadmap

View File

@ -81,6 +81,9 @@
<li>
<h2><a href="./sankey.html">Sankey</a></h2>
</li>
<li>
<h2><a href="./packet.html">Packet</a></h2>
</li>
</ul>
</body>
</html>

141
demos/packet.html Normal file
View File

@ -0,0 +1,141 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Mermaid Quick Test Page</title>
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=" />
<style>
div.mermaid {
font-family: 'Courier New', Courier, monospace !important;
}
</style>
</head>
<body>
<h1>Packet diagram demo</h1>
<div class="diagrams">
<pre class="mermaid">
packet-beta
0-15: "Source Port"
16-31: "Destination Port"
32-63: "Sequence Number"
64-95: "Acknowledgment Number"
96-99: "Data Offset"
100-105: "Reserved"
106: "URG"
107: "ACK"
108: "PSH"
109: "RST"
110: "SYN"
111: "FIN"
112-127: "Window"
128-143: "Checksum"
144-159: "Urgent Pointer"
160-191: "(Options and Padding)"
192-223: "data"
</pre
>
<pre class="mermaid">
---
config:
packet:
showBits: false
---
packet-beta
0-15: "Source Port"
16-31: "Destination Port"
32-63: "Sequence Number"
64-95: "Acknowledgment Number"
96-99: "Data Offset"
100-105: "Reserved"
106: "URG"
107: "ACK"
108: "PSH"
109: "RST"
110: "SYN"
111: "FIN"
112-127: "Window"
128-143: "Checksum"
144-159: "Urgent Pointer"
160-191: "(Options and Padding)"
192-223: "data"
</pre
>
<pre class="mermaid">
---
config:
theme: forest
---
packet-beta
title Forest theme
0-15: "Source Port"
16-31: "Destination Port"
32-63: "Sequence Number"
64-95: "Acknowledgment Number"
96-99: "Data Offset"
100-105: "Reserved"
106: "URG"
107: "ACK"
108: "PSH"
109: "RST"
110: "SYN"
111: "FIN"
112-127: "Window"
128-143: "Checksum"
144-159: "Urgent Pointer"
160-191: "(Options and Padding)"
192-223: "data"
</pre
>
<pre class="mermaid" style="background-color: black">
---
config:
theme: dark
---
packet-beta
title Dark theme
0-15: "Source Port"
16-31: "Destination Port"
32-63: "Sequence Number"
64-95: "Acknowledgment Number"
96-99: "Data Offset"
100-105: "Reserved"
106: "URG"
107: "ACK"
108: "PSH"
109: "RST"
110: "SYN"
111: "FIN"
112-127: "Window"
128-143: "Checksum"
144-159: "Urgent Pointer"
160-191: "(Options and Padding)"
192-223: "data"
</pre
>
</div>
<script type="module">
import mermaid from '/mermaid.esm.mjs';
mermaid.initialize({
logLevel: 3,
securityLevel: 'loose',
});
</script>
<style>
.diagrams {
display: flex;
flex-wrap: wrap;
}
pre {
width: 45vw;
padding: 2em;
}
</style>
</body>
</html>

View File

@ -23,6 +23,10 @@
participant Alice
participant Bob
participant John as John<br />Second Line
link Alice: Dashboard @ https://dashboard.contoso.com/alice
link Alice: Wiki @ https://wiki.contoso.com/alice
link John: Dashboard @ https://dashboard.contoso.com/john
link John: Wiki @ https://wiki.contoso.com/john
autonumber 10 10
rect rgb(200, 220, 100)
rect rgb(200, 255, 200)
@ -62,6 +66,26 @@
</pre>
<hr />
<pre class="mermaid">
---
title: With forced menus
config:
sequence:
forceMenus: true
---
sequenceDiagram
participant Alice
participant John
link Alice: Dashboard @ https://dashboard.contoso.com/alice
link Alice: Wiki @ https://wiki.contoso.com/alice
link John: Dashboard @ https://dashboard.contoso.com/john
link John: Wiki @ https://wiki.contoso.com/john
Alice->>John: Hello John, how are you?
John-->>Alice: Great!
Alice-)John: See you later!
</pre
>
<hr />
<pre class="mermaid">
sequenceDiagram
accTitle: Sequence diagram title is here
accDescr: Hello friends

View File

@ -14,7 +14,7 @@
#### Defined in
[defaultConfig.ts:272](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L272)
[defaultConfig.ts:275](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L275)
---

141
docs/syntax/packet.md Normal file
View File

@ -0,0 +1,141 @@
> **Warning**
>
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
>
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/syntax/packet.md](../../packages/mermaid/src/docs/syntax/packet.md).
# Packet Diagram (v\<MERMAID_RELEASE_VERSION>+)
## Introduction
A packet diagram is a visual representation used to illustrate the structure and contents of a network packet. Network packets are the fundamental units of data transferred over a network.
## Usage
This diagram type is particularly useful for network engineers, educators, and students who require a clear and concise way to represent the structure of network packets.
## Syntax
```md
packet-beta
start: "Block name" %% Single-bit block
start-end: "Block name" %% Multi-bit blocks
... More Fields ...
```
## Examples
```mermaid-example
---
title: "TCP Packet"
---
packet-beta
0-15: "Source Port"
16-31: "Destination Port"
32-63: "Sequence Number"
64-95: "Acknowledgment Number"
96-99: "Data Offset"
100-105: "Reserved"
106: "URG"
107: "ACK"
108: "PSH"
109: "RST"
110: "SYN"
111: "FIN"
112-127: "Window"
128-143: "Checksum"
144-159: "Urgent Pointer"
160-191: "(Options and Padding)"
192-255: "Data (variable length)"
```
```mermaid
---
title: "TCP Packet"
---
packet-beta
0-15: "Source Port"
16-31: "Destination Port"
32-63: "Sequence Number"
64-95: "Acknowledgment Number"
96-99: "Data Offset"
100-105: "Reserved"
106: "URG"
107: "ACK"
108: "PSH"
109: "RST"
110: "SYN"
111: "FIN"
112-127: "Window"
128-143: "Checksum"
144-159: "Urgent Pointer"
160-191: "(Options and Padding)"
192-255: "Data (variable length)"
```
```mermaid-example
packet-beta
title UDP Packet
0-15: "Source Port"
16-31: "Destination Port"
32-47: "Length"
48-63: "Checksum"
64-95: "Data (variable length)"
```
```mermaid
packet-beta
title UDP Packet
0-15: "Source Port"
16-31: "Destination Port"
32-47: "Length"
48-63: "Checksum"
64-95: "Data (variable length)"
```
## Details of Syntax
- **Ranges**: Each line after the title represents a different field in the packet. The range (e.g., `0-15`) indicates the bit positions in the packet.
- **Field Description**: A brief description of what the field represents, enclosed in quotes.
## Configuration
Please refer to the [configuration](/config/schema-docs/config-defs-packet-diagram-config.html) guide for details.
<!--
Theme variables are not currently working due to a mermaid bug. The passed values are not being propogated into styles function.
## Theme Variables
| Property | Description | Default Value |
| ---------------- | -------------------------- | ------------- |
| byteFontSize | Font size of the bytes | '10px' |
| startByteColor | Color of the starting byte | 'black' |
| endByteColor | Color of the ending byte | 'black' |
| labelColor | Color of the labels | 'black' |
| labelFontSize | Font size of the labels | '12px' |
| titleColor | Color of the title | 'black' |
| titleFontSize | Font size of the title | '14px' |
| blockStrokeColor | Color of the block stroke | 'black' |
| blockStrokeWidth | Width of the block stroke | '1' |
| blockFillColor | Fill color of the block | '#efefef' |
## Example on config and theme
```mermaid-example
---
config:
packet:
showBits: true
themeVariables:
packet:
startByteColor: red
---
packet-beta
0-15: "Source Port"
16-31: "Destination Port"
32-63: "Sequence Number"
```
-->

View File

@ -61,11 +61,11 @@
]
},
"devDependencies": {
"@applitools/eyes-cypress": "^3.33.1",
"@applitools/eyes-cypress": "^3.40.6",
"@commitlint/cli": "^17.6.1",
"@commitlint/config-conventional": "^17.6.1",
"@cspell/eslint-plugin": "^6.31.1",
"@cypress/code-coverage": "^3.10.7",
"@cypress/code-coverage": "^3.12.18",
"@rollup/plugin-typescript": "^11.1.1",
"@types/cors": "^2.8.13",
"@types/eslint": "^8.37.0",
@ -86,7 +86,7 @@
"chokidar": "^3.5.3",
"concurrently": "^8.0.1",
"cors": "^2.8.5",
"cypress": "^12.10.0",
"cypress": "^12.17.4",
"cypress-image-snapshot": "^4.0.1",
"esbuild": "^0.19.0",
"eslint": "^8.47.0",

View File

@ -1,10 +1,8 @@
import * as configApi from './config.js';
import { log } from './logger.js';
import { getDiagram, registerDiagram } from './diagram-api/diagramAPI.js';
import { detectType, getDiagramLoader } from './diagram-api/detectType.js';
import { UnknownDiagramError } from './errors.js';
import { encodeEntities } from './utils.js';
import type { DetailedError } from './utils.js';
import type { DiagramDefinition, DiagramMetadata } from './diagram-api/types.js';
@ -15,51 +13,45 @@ export type ParseErrorFunction = (err: string | DetailedError | unknown, hash?:
* @privateRemarks This is exported as part of the public mermaidAPI.
*/
export class Diagram {
type = 'graph';
parser: DiagramDefinition['parser'];
renderer: DiagramDefinition['renderer'];
db: DiagramDefinition['db'];
private init?: DiagramDefinition['init'];
private detectError?: UnknownDiagramError;
constructor(public text: string, public metadata: Pick<DiagramMetadata, 'title'> = {}) {
this.text = encodeEntities(text);
this.text += '\n';
const cnf = configApi.getConfig();
try {
this.type = detectType(text, cnf);
} catch (e) {
this.type = 'error';
this.detectError = e as UnknownDiagramError;
}
const diagram = getDiagram(this.type);
log.debug('Type ' + this.type);
// Setup diagram
this.db = diagram.db;
this.renderer = diagram.renderer;
this.parser = diagram.parser;
if (this.parser.parser) {
// The parser.parser.yy is only present in JISON parsers. So, we'll only set if required.
this.parser.parser.yy = this.db;
}
this.init = diagram.init;
this.parse();
}
parse() {
if (this.detectError) {
throw this.detectError;
}
this.db.clear?.();
public static async fromText(text: string, metadata: Pick<DiagramMetadata, 'title'> = {}) {
const config = configApi.getConfig();
this.init?.(config);
// This block was added for legacy compatibility. Use frontmatter instead of adding more special cases.
if (this.metadata.title) {
this.db.setDiagramTitle?.(this.metadata.title);
const type = detectType(text, config);
text = encodeEntities(text) + '\n';
try {
getDiagram(type);
} catch (e) {
const loader = getDiagramLoader(type);
if (!loader) {
throw new UnknownDiagramError(`Diagram ${type} not found.`);
}
// Diagram not available, loading it.
// new diagram will try getDiagram again and if fails then it is a valid throw
const { id, diagram } = await loader();
registerDiagram(id, diagram);
}
this.parser.parse(this.text);
const { db, parser, renderer, init } = getDiagram(type);
if (parser.parser) {
// The parser.parser.yy is only present in JISON parsers. So, we'll only set if required.
parser.parser.yy = db;
}
db.clear?.();
init?.(config);
// This block was added for legacy compatibility. Use frontmatter instead of adding more special cases.
if (metadata.title) {
db.setDiagramTitle?.(metadata.title);
}
await parser.parse(text);
return new Diagram(type, text, db, parser, renderer);
}
private constructor(
public type: string,
public text: string,
public db: DiagramDefinition['db'],
public parser: DiagramDefinition['parser'],
public renderer: DiagramDefinition['renderer']
) {}
async render(id: string, version: string) {
await this.renderer.draw(this.text, id, version, this);
}
@ -72,34 +64,3 @@ export class Diagram {
return this.type;
}
}
/**
* Parse the text asynchronously and generate a Diagram object asynchronously.
* **Warning:** This function may be changed in the future.
* @alpha
* @param text - The mermaid diagram definition.
* @param metadata - Diagram metadata, defined in YAML.
* @returns A the Promise of a Diagram object.
* @throws {@link UnknownDiagramError} if the diagram type can not be found.
* @privateRemarks This is exported as part of the public mermaidAPI.
*/
export const getDiagramFromText = async (
text: string,
metadata: Pick<DiagramMetadata, 'title'> = {}
): Promise<Diagram> => {
const type = detectType(text, configApi.getConfig());
try {
// Trying to find the diagram
getDiagram(type);
} catch (error) {
const loader = getDiagramLoader(type);
if (!loader) {
throw new UnknownDiagramError(`Diagram ${type} not found.`);
}
// Diagram not available, loading it.
// new diagram will try getDiagram again and if fails then it is a valid throw
const { id, diagram } = await loader();
registerDiagram(id, diagram);
}
return new Diagram(text, metadata);
};

View File

@ -146,10 +146,43 @@ export interface MermaidConfig {
gitGraph?: GitGraphDiagramConfig;
c4?: C4DiagramConfig;
sankey?: SankeyDiagramConfig;
packet?: PacketDiagramConfig;
dompurifyConfig?: DOMPurifyConfiguration;
wrap?: boolean;
fontSize?: number;
}
/**
* The object containing configurations specific for packet diagrams.
*
* This interface was referenced by `MermaidConfig`'s JSON-Schema
* via the `definition` "PacketDiagramConfig".
*/
export interface PacketDiagramConfig extends BaseDiagramConfig {
/**
* The height of each row in the packet diagram.
*/
rowHeight?: number;
/**
* The width of each bit in the packet diagram.
*/
bitWidth?: number;
/**
* The number of bits to display per row.
*/
bitsPerRow?: number;
/**
* Toggle to display or hide bit numbers.
*/
showBits?: boolean;
/**
* The horizontal padding between the blocks in a row.
*/
paddingX?: number;
/**
* The vertical padding between the rows.
*/
paddingY?: number;
}
/**
* This interface was referenced by `MermaidConfig`'s JSON-Schema
* via the `definition` "BaseDiagramConfig".

View File

@ -257,6 +257,9 @@ const config: RequiredDeep<MermaidConfig> = {
// TODO: can we make this default to `true` instead?
useMaxWidth: false,
},
packet: {
...defaultConfigJson.packet,
},
};
const keyify = (obj: any, prefix = ''): string[] =>

View File

@ -20,6 +20,7 @@ import flowchartElk from '../diagrams/flowchart/elk/detector.js';
import timeline from '../diagrams/timeline/detector.js';
import mindmap from '../diagrams/mindmap/detector.js';
import sankey from '../diagrams/sankey/sankeyDetector.js';
import { packet } from '../diagrams/packet/detector.js';
import { registerLazyLoadedDiagrams } from './detectType.js';
import { registerDiagram } from './diagramAPI.js';
@ -86,6 +87,7 @@ export const addDiagrams = () => {
journey,
quadrantChart,
sankey,
packet,
xychart
);
};

View File

@ -2,12 +2,12 @@ import { detectType } from './detectType.js';
import { getDiagram, registerDiagram } from './diagramAPI.js';
import { addDiagrams } from './diagram-orchestration.js';
import type { DiagramDetector } from './types.js';
import { getDiagramFromText } from '../Diagram.js';
import { Diagram } from '../Diagram.js';
import { it, describe, expect, beforeAll } from 'vitest';
addDiagrams();
beforeAll(async () => {
await getDiagramFromText('sequenceDiagram');
await Diagram.fromText('sequenceDiagram');
});
describe('DiagramAPI', () => {

View File

@ -1,7 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type * as d3 from 'd3';
import type { SetRequired } from 'type-fest';
import type { Diagram } from '../Diagram.js';
import type { BaseDiagramConfig, MermaidConfig } from '../config.type.js';
import type * as d3 from 'd3';
export interface DiagramMetadata {
title?: string;
@ -32,13 +33,29 @@ export interface DiagramDB {
getDiagramTitle?: () => string;
setAccTitle?: (title: string) => void;
getAccTitle?: () => string;
setAccDescription?: (describetion: string) => void;
setAccDescription?: (description: string) => void;
getAccDescription?: () => string;
setDisplayMode?: (title: string) => void;
bindFunctions?: (element: Element) => void;
}
/**
* DiagramDB with fields that is required for all new diagrams.
*/
export type DiagramDBBase<T extends BaseDiagramConfig> = {
getConfig: () => Required<T>;
} & SetRequired<
DiagramDB,
| 'clear'
| 'getAccTitle'
| 'getDiagramTitle'
| 'getAccDescription'
| 'setAccDescription'
| 'setAccTitle'
| 'setDiagramTitle'
>;
// This is what is returned from getClasses(...) methods.
// It is slightly renamed to ..StyleClassDef instead of just ClassDef because "class" is a greatly ambiguous and overloaded word.
// It makes it clear we're working with a style class definition, even though defining the type is currently difficult.
@ -104,7 +121,7 @@ export type DrawDefinition = (
) => void | Promise<void>;
export interface ParserDefinition {
parse: (text: string) => void;
parse: (text: string) => void | Promise<void>;
parser?: { yy: DiagramDB };
}

View File

@ -1,5 +1,5 @@
import { describe, test, expect } from 'vitest';
import { Diagram, getDiagramFromText } from './Diagram.js';
import { Diagram } from './Diagram.js';
import { addDetector } from './diagram-api/detectType.js';
import { addDiagrams } from './diagram-api/diagram-orchestration.js';
import type { DiagramLoader } from './diagram-api/types.js';
@ -30,10 +30,10 @@ const getDummyDiagram = (id: string, title?: string): Awaited<ReturnType<Diagram
describe('diagram detection', () => {
test('should detect inbuilt diagrams', async () => {
const graph = (await getDiagramFromText('graph TD; A-->B')) as Diagram;
const graph = (await Diagram.fromText('graph TD; A-->B')) as Diagram;
expect(graph).toBeInstanceOf(Diagram);
expect(graph.type).toBe('flowchart-v2');
const sequence = (await getDiagramFromText(
const sequence = (await Diagram.fromText(
'sequenceDiagram; Alice->>+John: Hello John, how are you?'
)) as Diagram;
expect(sequence).toBeInstanceOf(Diagram);
@ -46,7 +46,7 @@ describe('diagram detection', () => {
(str) => str.startsWith('loki'),
() => Promise.resolve(getDummyDiagram('loki'))
);
const diagram = await getDiagramFromText('loki TD; A-->B');
const diagram = await Diagram.fromText('loki TD; A-->B');
expect(diagram).toBeInstanceOf(Diagram);
expect(diagram.type).toBe('loki');
});
@ -58,19 +58,19 @@ describe('diagram detection', () => {
(str) => str.startsWith('flowchart-elk'),
() => Promise.resolve(getDummyDiagram('flowchart-elk', title))
);
const diagram = await getDiagramFromText('flowchart-elk TD; A-->B');
const diagram = await Diagram.fromText('flowchart-elk TD; A-->B');
expect(diagram).toBeInstanceOf(Diagram);
expect(diagram.db.getDiagramTitle?.()).toBe(title);
});
test('should throw the right error for incorrect diagram', async () => {
await expect(getDiagramFromText('graph TD; A-->')).rejects.toThrowErrorMatchingInlineSnapshot(`
await expect(Diagram.fromText('graph TD; A-->')).rejects.toThrowErrorMatchingInlineSnapshot(`
"Parse error on line 2:
graph TD; A-->
--------------^
Expecting 'AMP', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'EOF'"
`);
await expect(getDiagramFromText('sequenceDiagram; A-->B')).rejects
await expect(Diagram.fromText('sequenceDiagram; A-->B')).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Parse error on line 1:
...quenceDiagram; A-->B
@ -80,13 +80,13 @@ Expecting 'TXT', got 'NEWLINE'"
});
test('should throw the right error for unregistered diagrams', async () => {
await expect(getDiagramFromText('thor TD; A-->B')).rejects.toThrowErrorMatchingInlineSnapshot(
await expect(Diagram.fromText('thor TD; A-->B')).rejects.toThrowErrorMatchingInlineSnapshot(
'"No diagram type detected matching given configuration for text: thor TD; A-->B"'
);
});
test('should consider entity codes when present in diagram defination', async () => {
const diagram = await getDiagramFromText(`sequenceDiagram
const diagram = await Diagram.fromText(`sequenceDiagram
A->>B: I #9829; you!
B->>A: I #9829; you #infin; times more!`);
// @ts-ignore: we need to add types for sequenceDb which will be done in separate PR

View File

@ -18,13 +18,18 @@ export const getRows = (s?: string): string[] => {
return str.split('#br#');
};
/**
* Removes script tags from a text
*
* @param txt - The text to sanitize
* @returns The safer text
*/
export const removeScript = (txt: string): string => {
const setupDompurifyHooksIfNotSetup = (() => {
let setup = false;
return () => {
if (!setup) {
setupDompurifyHooks();
setup = true;
}
};
})();
function setupDompurifyHooks() {
const TEMPORARY_ATTRIBUTE = 'data-temp-href-target';
DOMPurify.addHook('beforeSanitizeAttributes', (node: Element) => {
@ -33,8 +38,6 @@ export const removeScript = (txt: string): string => {
}
});
const sanitizedText = DOMPurify.sanitize(txt);
DOMPurify.addHook('afterSanitizeAttributes', (node: Element) => {
if (node.tagName === 'A' && node.hasAttribute(TEMPORARY_ATTRIBUTE)) {
node.setAttribute('target', node.getAttribute(TEMPORARY_ATTRIBUTE) || '');
@ -44,6 +47,18 @@ export const removeScript = (txt: string): string => {
}
}
});
}
/**
* Removes script tags from a text
*
* @param txt - The text to sanitize
* @returns The safer text
*/
export const removeScript = (txt: string): string => {
setupDompurifyHooksIfNotSetup();
const sanitizedText = DOMPurify.sanitize(txt);
return sanitizedText;
};

View File

@ -27,11 +27,10 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
\%\%(?!\{)*[^\n]* /* skip comments */
[^\}]\%\%*[^\n]* /* skip comments */
\%\%*[^\n]*[\n]* /* do nothing */
\%\%*[^\n]*[\n]* /* do nothing */
[\n]+ return 'NL';
\s+ /* skip whitespace */
\#[^\n]* /* skip comments */
\%%[^\n]* /* skip comments */
/*
@ -86,10 +85,10 @@ weekday\s+friday return 'weekday_friday'
weekday\s+saturday return 'weekday_saturday'
weekday\s+sunday return 'weekday_sunday'
\d\d\d\d"-"\d\d"-"\d\d return 'date';
"title"\s[^#\n;]+ return 'title';
"title"\s[^\n]+ return 'title';
"accDescription"\s[^#\n;]+ return 'accDescription'
"section"\s[^#:\n;]+ return 'section';
[^#:\n;]+ return 'taskTxt';
"section"\s[^\n]+ return 'section';
[^:\n]+ return 'taskTxt';
":"[^#\n;]+ return 'taskData';
":" return ':';
<<EOF>> return 'EOF';

View File

@ -28,8 +28,12 @@ describe('when parsing a gantt diagram it', function () {
});
it('should handle a title definition', function () {
const str = 'gantt\ndateFormat yyyy-mm-dd\ntitle Adding gantt diagram functionality to mermaid';
const semi = 'gantt\ndateFormat yyyy-mm-dd\ntitle ;Gantt diagram titles support semicolons';
const hash = 'gantt\ndateFormat yyyy-mm-dd\ntitle #Gantt diagram titles support hashtags';
expect(parserFnConstructor(str)).not.toThrow();
expect(parserFnConstructor(semi)).not.toThrow();
expect(parserFnConstructor(hash)).not.toThrow();
});
it('should handle an excludes definition', function () {
const str =
@ -53,7 +57,23 @@ describe('when parsing a gantt diagram it', function () {
'excludes weekdays 2019-02-01\n' +
'section Documentation';
const semi =
'gantt\n' +
'dateFormat yyyy-mm-dd\n' +
'title Adding gantt diagram functionality to mermaid\n' +
'excludes weekdays 2019-02-01\n' +
'section ;Documentation';
const hash =
'gantt\n' +
'dateFormat yyyy-mm-dd\n' +
'title Adding gantt diagram functionality to mermaid\n' +
'excludes weekdays 2019-02-01\n' +
'section #Documentation';
expect(parserFnConstructor(str)).not.toThrow();
expect(parserFnConstructor(semi)).not.toThrow();
expect(parserFnConstructor(hash)).not.toThrow();
});
it('should handle multiline section titles with different line breaks', function () {
const str =
@ -73,7 +93,23 @@ describe('when parsing a gantt diagram it', function () {
'section Documentation\n' +
'Design jison grammar:des1, 2014-01-01, 2014-01-04';
const semi =
'gantt\n' +
'dateFormat YYYY-MM-DD\n' +
'title Adding gantt diagram functionality to mermaid\n' +
'section Documentation\n' +
';Design jison grammar:des1, 2014-01-01, 2014-01-04';
const hash =
'gantt\n' +
'dateFormat YYYY-MM-DD\n' +
'title Adding gantt diagram functionality to mermaid\n' +
'section Documentation\n' +
'#Design jison grammar:des1, 2014-01-01, 2014-01-04';
expect(parserFnConstructor(str)).not.toThrow();
expect(parserFnConstructor(semi)).not.toThrow();
expect(parserFnConstructor(hash)).not.toThrow();
const tasks = parser.yy.getTasks();

View File

@ -1,31 +1,27 @@
import { parser } from './infoParser.js';
describe('info', () => {
it('should handle an info definition', () => {
it('should handle an info definition', async () => {
const str = `info`;
expect(() => {
parser.parse(str);
}).not.toThrow();
await expect(parser.parse(str)).resolves.not.toThrow();
});
it('should handle an info definition with showInfo', () => {
it('should handle an info definition with showInfo', async () => {
const str = `info showInfo`;
expect(() => {
parser.parse(str);
}).not.toThrow();
await expect(parser.parse(str)).resolves.not.toThrow();
});
it('should throw because of unsupported info grammar', () => {
it('should throw because of unsupported info grammar', async () => {
const str = `info unsupported`;
expect(() => {
parser.parse(str);
}).toThrow('Parsing failed: unexpected character: ->u<- at offset: 5, skipped 11 characters.');
await expect(parser.parse(str)).rejects.toThrow(
'Parsing failed: unexpected character: ->u<- at offset: 5, skipped 11 characters.'
);
});
it('should throw because of unsupported info grammar', () => {
it('should throw because of unsupported info grammar', async () => {
const str = `info unsupported`;
expect(() => {
parser.parse(str);
}).toThrow('Parsing failed: unexpected character: ->u<- at offset: 5, skipped 11 characters.');
await expect(parser.parse(str)).rejects.toThrow(
'Parsing failed: unexpected character: ->u<- at offset: 5, skipped 11 characters.'
);
});
});

View File

@ -1,12 +1,11 @@
import type { Info } 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';
export const parser: ParserDefinition = {
parse: (input: string): void => {
const ast: Info = parse('info', input);
parse: async (input: string): Promise<void> => {
const ast: Info = await parse('info', input);
log.debug(ast);
},
};

View File

@ -0,0 +1,59 @@
import { getConfig as commonGetConfig } from '../../config.js';
import type { PacketDiagramConfig } from '../../config.type.js';
import DEFAULT_CONFIG from '../../defaultConfig.js';
import { cleanAndMerge } from '../../utils.js';
import {
clear as commonClear,
getAccDescription,
getAccTitle,
getDiagramTitle,
setAccDescription,
setAccTitle,
setDiagramTitle,
} from '../common/commonDb.js';
import type { PacketDB, PacketData, PacketWord } from './types.js';
const defaultPacketData: PacketData = {
packet: [],
};
let data: PacketData = structuredClone(defaultPacketData);
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;
}
return config;
};
const getPacket = (): PacketWord[] => data.packet;
const pushWord = (word: PacketWord) => {
if (word.length > 0) {
data.packet.push(word);
}
};
const clear = () => {
commonClear();
data = structuredClone(defaultPacketData);
};
export const db: PacketDB = {
pushWord,
getPacket,
getConfig,
clear,
setAccTitle,
getAccTitle,
setDiagramTitle,
getDiagramTitle,
getAccDescription,
setAccDescription,
};

View File

@ -0,0 +1,22 @@
import type {
DiagramDetector,
DiagramLoader,
ExternalDiagramDefinition,
} from '../../diagram-api/types.js';
const id = 'packet';
const detector: DiagramDetector = (txt) => {
return /^\s*packet-beta/.test(txt);
};
const loader: DiagramLoader = async () => {
const { diagram } = await import('./diagram.js');
return { id, diagram };
};
export const packet: ExternalDiagramDefinition = {
id,
detector,
loader,
};

View File

@ -0,0 +1,12 @@
import type { DiagramDefinition } from '../../diagram-api/types.js';
import { db } from './db.js';
import { parser } from './parser.js';
import { renderer } from './renderer.js';
import { styles } from './styles.js';
export const diagram: DiagramDefinition = {
parser,
db,
renderer,
styles,
};

View File

@ -0,0 +1,175 @@
import { it, describe, expect } from 'vitest';
import { db } from './db.js';
import { parser } from './parser.js';
const { clear, getPacket, getDiagramTitle, getAccTitle, getAccDescription } = db;
describe('packet diagrams', () => {
beforeEach(() => {
clear();
});
it('should handle a packet-beta definition', async () => {
const str = `packet-beta`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getPacket()).toMatchInlineSnapshot('[]');
});
it('should handle diagram with data and title', async () => {
const str = `packet-beta
title Packet diagram
accTitle: Packet accTitle
accDescr: Packet accDescription
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(`
[
[
{
"end": 10,
"label": "test",
"start": 0,
},
],
]
`);
});
it('should handle single bits', async () => {
const str = `packet-beta
0-10: "test"
11: "single"
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getPacket()).toMatchInlineSnapshot(`
[
[
{
"end": 10,
"label": "test",
"start": 0,
},
{
"end": 11,
"label": "single",
"start": 11,
},
],
]
`);
});
it('should split into multiple rows', async () => {
const str = `packet-beta
0-10: "test"
11-90: "multiple"
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getPacket()).toMatchInlineSnapshot(`
[
[
{
"end": 10,
"label": "test",
"start": 0,
},
{
"end": 31,
"label": "multiple",
"start": 11,
},
],
[
{
"end": 63,
"label": "multiple",
"start": 32,
},
],
[
{
"end": 90,
"label": "multiple",
"start": 64,
},
],
]
`);
});
it('should split into multiple rows when cut at exact length', async () => {
const str = `packet-beta
0-16: "test"
17-63: "multiple"
`;
await expect(parser.parse(str)).resolves.not.toThrow();
expect(getPacket()).toMatchInlineSnapshot(`
[
[
{
"end": 16,
"label": "test",
"start": 0,
},
{
"end": 31,
"label": "multiple",
"start": 17,
},
],
[
{
"end": 63,
"label": "multiple",
"start": 32,
},
],
]
`);
});
it('should throw error if numbers are not continuous', async () => {
const str = `packet-beta
0-16: "test"
18-20: "error"
`;
await expect(parser.parse(str)).rejects.toThrowErrorMatchingInlineSnapshot(
'"Packet block 18 - 20 is not contiguous. It should start from 17."'
);
});
it('should throw error if numbers are not continuous for single packets', async () => {
const str = `packet-beta
0-16: "test"
18: "error"
`;
await expect(parser.parse(str)).rejects.toThrowErrorMatchingInlineSnapshot(
'"Packet block 18 - 18 is not contiguous. It should start from 17."'
);
});
it('should throw error if numbers are not continuous for single packets - 2', async () => {
const str = `packet-beta
0-16: "test"
17: "good"
19: "error"
`;
await expect(parser.parse(str)).rejects.toThrowErrorMatchingInlineSnapshot(
'"Packet block 19 - 19 is not contiguous. It should start from 18."'
);
});
it('should throw error if end is less than start', async () => {
const str = `packet-beta
0-16: "test"
25-20: "error"
`;
await expect(parser.parse(str)).rejects.toThrowErrorMatchingInlineSnapshot(
'"Packet block 25 - 20 is invalid. End must be greater than start."'
);
});
});

View File

@ -0,0 +1,85 @@
import type { Packet } from '@mermaid-js/parser';
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 type { PacketBlock, PacketWord } from './types.js';
const maxPacketSize = 10_000;
const populate = (ast: Packet) => {
populateCommonDb(ast, db);
let lastByte = -1;
let word: PacketWord = [];
let row = 1;
const { bitsPerRow } = db.getConfig();
for (let { start, end, label } of ast.blocks) {
if (end && end < start) {
throw new Error(`Packet block ${start} - ${end} is invalid. End must be greater than start.`);
}
if (start !== lastByte + 1) {
throw new Error(
`Packet block ${start} - ${end ?? start} is not contiguous. It should start from ${
lastByte + 1
}.`
);
}
lastByte = end ?? start;
log.debug(`Packet block ${start} - ${lastByte} with label ${label}`);
while (word.length <= bitsPerRow + 1 && db.getPacket().length < maxPacketSize) {
const [block, nextBlock] = getNextFittingBlock({ start, end, label }, row, bitsPerRow);
word.push(block);
if (block.end + 1 === row * bitsPerRow) {
db.pushWord(word);
word = [];
row++;
}
if (!nextBlock) {
break;
}
({ start, end, label } = nextBlock);
}
}
db.pushWord(word);
};
const getNextFittingBlock = (
block: PacketBlock,
row: number,
bitsPerRow: number
): [Required<PacketBlock>, PacketBlock | undefined] => {
if (block.end === undefined) {
block.end = block.start;
}
if (block.start > block.end) {
throw new Error(`Block start ${block.start} is greater than block end ${block.end}.`);
}
if (block.end + 1 <= row * bitsPerRow) {
return [block as Required<PacketBlock>, undefined];
}
return [
{
start: block.start,
end: row * bitsPerRow - 1,
label: block.label,
},
{
start: row * bitsPerRow,
end: block.end,
label: block.label,
},
];
};
export const parser: ParserDefinition = {
parse: async (input: string): Promise<void> => {
const ast: Packet = await parse('packet', input);
log.debug(ast);
populate(ast);
},
};

View File

@ -0,0 +1,95 @@
import type { Diagram } from '../../Diagram.js';
import type { PacketDiagramConfig } from '../../config.type.js';
import type { DiagramRenderer, DrawDefinition, Group, SVG } from '../../diagram-api/types.js';
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
import { configureSvgSize } from '../../setupGraphViewbox.js';
import type { PacketDB, PacketWord } from './types.js';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => {
const db = diagram.db as PacketDB;
const config = db.getConfig();
const { rowHeight, paddingY, bitWidth, bitsPerRow } = config;
const words = db.getPacket();
const title = db.getDiagramTitle();
const totalRowHeight = rowHeight + paddingY;
const svgHeight = totalRowHeight * (words.length + 1) - (title ? 0 : rowHeight);
const svgWidth = bitWidth * bitsPerRow + 2;
const svg: SVG = selectSvgElement(id);
svg.attr('viewbox', `0 0 ${svgWidth} ${svgHeight}`);
configureSvgSize(svg, svgHeight, svgWidth, config.useMaxWidth);
for (const [word, packet] of words.entries()) {
drawWord(svg, packet, word, config);
}
svg
.append('text')
.text(title)
.attr('x', svgWidth / 2)
.attr('y', svgHeight - totalRowHeight / 2)
.attr('dominant-baseline', 'middle')
.attr('text-anchor', 'middle')
.attr('class', 'packetTitle');
};
const drawWord = (
svg: SVG,
word: PacketWord,
rowNumber: number,
{ rowHeight, paddingX, paddingY, bitWidth, bitsPerRow, showBits }: Required<PacketDiagramConfig>
) => {
const group: Group = svg.append('g');
const wordY = rowNumber * (rowHeight + paddingY) + paddingY;
for (const block of word) {
const blockX = (block.start % bitsPerRow) * bitWidth + 1;
const width = (block.end - block.start + 1) * bitWidth - paddingX;
// Block rectangle
group
.append('rect')
.attr('x', blockX)
.attr('y', wordY)
.attr('width', width)
.attr('height', rowHeight)
.attr('class', 'packetBlock');
// Block label
group
.append('text')
.attr('x', blockX + width / 2)
.attr('y', wordY + rowHeight / 2)
.attr('class', 'packetLabel')
.attr('dominant-baseline', 'middle')
.attr('text-anchor', 'middle')
.text(block.label);
if (!showBits) {
continue;
}
// Start byte count
const isSingleBlock = block.end === block.start;
const bitNumberY = wordY - 2;
group
.append('text')
.attr('x', blockX + (isSingleBlock ? width / 2 : 0))
.attr('y', bitNumberY)
.attr('class', 'packetByte start')
.attr('dominant-baseline', 'auto')
.attr('text-anchor', isSingleBlock ? 'middle' : 'start')
.text(block.start);
// Draw end byte count if it is not the same as start byte count
if (!isSingleBlock) {
group
.append('text')
.attr('x', blockX + width)
.attr('y', bitNumberY)
.attr('class', 'packetByte end')
.attr('dominant-baseline', 'auto')
.attr('text-anchor', 'end')
.text(block.end);
}
}
};
export const renderer: DiagramRenderer = { draw };

View File

@ -0,0 +1,47 @@
import type { DiagramStylesProvider } from '../../diagram-api/types.js';
import { cleanAndMerge } from '../../utils.js';
import type { PacketStyleOptions } from './types.js';
const defaultPacketStyleOptions: PacketStyleOptions = {
byteFontSize: '10px',
startByteColor: 'black',
endByteColor: 'black',
labelColor: 'black',
labelFontSize: '12px',
titleColor: 'black',
titleFontSize: '14px',
blockStrokeColor: 'black',
blockStrokeWidth: '1',
blockFillColor: '#efefef',
};
export const styles: DiagramStylesProvider = ({ packet }: { packet?: PacketStyleOptions } = {}) => {
const options = cleanAndMerge(defaultPacketStyleOptions, packet);
return `
.packetByte {
font-size: ${options.byteFontSize};
}
.packetByte.start {
fill: ${options.startByteColor};
}
.packetByte.end {
fill: ${options.endByteColor};
}
.packetLabel {
fill: ${options.labelColor};
font-size: ${options.labelFontSize};
}
.packetTitle {
fill: ${options.titleColor};
font-size: ${options.titleFontSize};
}
.packetBlock {
stroke: ${options.blockStrokeColor};
stroke-width: ${options.blockStrokeWidth};
fill: ${options.blockFillColor};
}
`;
};
export default styles;

View File

@ -0,0 +1,29 @@
import type { Packet, RecursiveAstOmit } from '@mermaid-js/parser';
import type { PacketDiagramConfig } from '../../config.type.js';
import type { DiagramDBBase } from '../../diagram-api/types.js';
import type { ArrayElement } from '../../types.js';
export type PacketBlock = RecursiveAstOmit<ArrayElement<Packet['blocks']>>;
export type PacketWord = Required<PacketBlock>[];
export interface PacketDB extends DiagramDBBase<PacketDiagramConfig> {
pushWord: (word: PacketWord) => void;
getPacket: () => PacketWord[];
}
export interface PacketStyleOptions {
byteFontSize?: string;
startByteColor?: string;
endByteColor?: string;
labelColor?: string;
labelFontSize?: string;
blockStrokeColor?: string;
blockStrokeWidth?: string;
blockFillColor?: string;
titleColor?: string;
titleFontSize?: string;
}
export interface PacketData {
packet: PacketWord[];
}

View File

@ -10,8 +10,8 @@ describe('pie', () => {
beforeEach(() => db.clear());
describe('parse', () => {
it('should handle very simple pie', () => {
parser.parse(`pie
it('should handle very simple pie', async () => {
await parser.parse(`pie
"ash": 100
`);
@ -19,8 +19,8 @@ describe('pie', () => {
expect(sections['ash']).toBe(100);
});
it('should handle simple pie', () => {
parser.parse(`pie
it('should handle simple pie', async () => {
await parser.parse(`pie
"ash" : 60
"bat" : 40
`);
@ -30,8 +30,8 @@ describe('pie', () => {
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with showData', () => {
parser.parse(`pie showData
it('should handle simple pie with showData', async () => {
await parser.parse(`pie showData
"ash" : 60
"bat" : 40
`);
@ -43,8 +43,8 @@ describe('pie', () => {
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with comments', () => {
parser.parse(`pie
it('should handle simple pie with comments', async () => {
await parser.parse(`pie
%% comments
"ash" : 60
"bat" : 40
@ -55,8 +55,8 @@ describe('pie', () => {
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with a title', () => {
parser.parse(`pie title a 60/40 pie
it('should handle simple pie with a title', async () => {
await parser.parse(`pie title a 60/40 pie
"ash" : 60
"bat" : 40
`);
@ -68,8 +68,8 @@ describe('pie', () => {
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with an acc title (accTitle)', () => {
parser.parse(`pie title a neat chart
it('should handle simple pie with an acc title (accTitle)', async () => {
await parser.parse(`pie title a neat chart
accTitle: a neat acc title
"ash" : 60
"bat" : 40
@ -84,8 +84,8 @@ describe('pie', () => {
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with an acc description (accDescr)', () => {
parser.parse(`pie title a neat chart
it('should handle simple pie with an acc description (accDescr)', async () => {
await parser.parse(`pie title a neat chart
accDescr: a neat description
"ash" : 60
"bat" : 40
@ -100,8 +100,8 @@ describe('pie', () => {
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with a multiline acc description (accDescr)', () => {
parser.parse(`pie title a neat chart
it('should handle simple pie with a multiline acc description (accDescr)', async () => {
await parser.parse(`pie title a neat chart
accDescr {
a neat description
on multiple lines
@ -119,8 +119,8 @@ describe('pie', () => {
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with positive decimal', () => {
parser.parse(`pie
it('should handle simple pie with positive decimal', async () => {
await parser.parse(`pie
"ash" : 60.67
"bat" : 40
`);
@ -131,12 +131,12 @@ describe('pie', () => {
});
it('should handle simple pie with negative decimal', () => {
expect(() => {
parser.parse(`pie
expect(async () => {
await parser.parse(`pie
"ash" : -60.67
"bat" : 40.12
`);
}).toThrowError();
}).rejects.toThrowError();
});
});

View File

@ -13,8 +13,8 @@ const populateDb = (ast: Pie, db: PieDB) => {
};
export const parser: ParserDefinition = {
parse: (input: string): void => {
const ast: Pie = parse('pie', input);
parse: async (input: string): Promise<void> => {
const ast: Pie = await parse('pie', input);
log.debug(ast);
populateDb(ast, db);
},

View File

@ -1,12 +1,12 @@
import { vi } from 'vitest';
import { setSiteConfig } from '../../diagram-api/diagramAPI.js';
import mermaidAPI from '../../mermaidAPI.js';
import { Diagram, getDiagramFromText } from '../../Diagram.js';
import { Diagram } from '../../Diagram.js';
import { addDiagrams } from '../../diagram-api/diagram-orchestration.js';
beforeAll(async () => {
// Is required to load the sequence diagram
await getDiagramFromText('sequenceDiagram');
await Diagram.fromText('sequenceDiagram');
});
/**
@ -95,8 +95,8 @@ function addConf(conf, key, value) {
let diagram;
describe('more than one sequence diagram', () => {
it('should not have duplicated messages', () => {
const diagram1 = new Diagram(`
it('should not have duplicated messages', async () => {
const diagram1 = await Diagram.fromText(`
sequenceDiagram
Alice->Bob:Hello Bob, how are you?
Bob-->Alice: I am good thanks!`);
@ -120,7 +120,7 @@ describe('more than one sequence diagram', () => {
},
]
`);
const diagram2 = new Diagram(`
const diagram2 = await Diagram.fromText(`
sequenceDiagram
Alice->Bob:Hello Bob, how are you?
Bob-->Alice: I am good thanks!`);
@ -147,7 +147,7 @@ describe('more than one sequence diagram', () => {
`);
// Add John actor
const diagram3 = new Diagram(`
const diagram3 = await Diagram.fromText(`
sequenceDiagram
Alice->John:Hello John, how are you?
John-->Alice: I am good thanks!`);
@ -176,8 +176,8 @@ describe('more than one sequence diagram', () => {
});
describe('when parsing a sequenceDiagram', function () {
beforeEach(function () {
diagram = new Diagram(`
beforeEach(async function () {
diagram = await Diagram.fromText(`
sequenceDiagram
Alice->Bob:Hello Bob, how are you?
Note right of Bob: Bob thinks
@ -1613,7 +1613,7 @@ describe('when rendering a sequenceDiagram APA', function () {
setSiteConfig({ logLevel: 5, sequence: conf });
});
let conf;
beforeEach(function () {
beforeEach(async function () {
mermaidAPI.reset();
// });
@ -1632,7 +1632,7 @@ describe('when rendering a sequenceDiagram APA', function () {
mirrorActors: false,
};
setSiteConfig({ logLevel: 5, sequence: conf });
diagram = new Diagram(`
diagram = await Diagram.fromText(`
sequenceDiagram
Alice->Bob:Hello Bob, how are you?
Note right of Bob: Bob thinks

View File

@ -10,22 +10,6 @@ export const drawRect = function (elem, rectData) {
return svgDrawCommon.drawRect(elem, rectData);
};
const addPopupInteraction = (id, actorCnt) => {
addFunction(() => {
const arr = document.querySelectorAll(id);
// This will be the case when running in sandboxed mode
if (arr.length === 0) {
return;
}
arr[0].addEventListener('mouseover', function () {
popupMenuUpFunc('actor' + actorCnt + '_popup');
});
arr[0].addEventListener('mouseout', function () {
popupMenuDownFunc('actor' + actorCnt + '_popup');
});
});
};
export const drawPopup = function (elem, actor, minMenuWidth, textAttrs, forceMenus) {
if (actor.links === undefined || actor.links === null || Object.keys(actor.links).length === 0) {
return { height: 0, width: 0 };
@ -44,7 +28,6 @@ export const drawPopup = function (elem, actor, minMenuWidth, textAttrs, forceMe
g.attr('id', 'actor' + actorCnt + '_popup');
g.attr('class', 'actorPopupMenu');
g.attr('display', displayValue);
addPopupInteraction('#actor' + actorCnt + '_popup', actorCnt);
var actorClass = '';
if (rectData.class !== undefined) {
actorClass = ' ' + rectData.class;
@ -90,36 +73,14 @@ export const drawPopup = function (elem, actor, minMenuWidth, textAttrs, forceMe
return { height: rectData.height + linkY, width: menuWidth };
};
export const popupMenu = function (popid) {
const popupMenuToggle = function (popid) {
return (
"var pu = document.getElementById('" +
popid +
"'); if (pu != null) { pu.style.display = 'block'; }"
"'); if (pu != null) { pu.style.display = pu.style.display == 'block' ? 'none' : 'block'; }"
);
};
export const popdownMenu = function (popid) {
return (
"var pu = document.getElementById('" +
popid +
"'); if (pu != null) { pu.style.display = 'none'; }"
);
};
const popupMenuUpFunc = function (popupId) {
var pu = document.getElementById(popupId);
if (pu != null) {
pu.style.display = 'block';
}
};
const popupMenuDownFunc = function (popupId) {
var pu = document.getElementById(popupId);
if (pu != null) {
pu.style.display = 'none';
}
};
export const drawText = function (elem, textData) {
let prevTextHeight = 0;
let textHeight = 0;
@ -329,6 +290,9 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) {
if (!isFooter) {
actorCnt++;
if (Object.keys(actor.links || {}).length && !conf.forceMenus) {
g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer');
}
g.append('line')
.attr('id', 'actor' + actorCnt)
.attr('x1', center)
@ -345,7 +309,6 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) {
if (actor.links != null) {
g.attr('id', 'root-' + actorCnt);
addPopupInteraction('#root-' + actorCnt, actorCnt);
}
}
@ -1053,8 +1016,6 @@ export default {
insertClockIcon,
getTextObj,
getNoteRect,
popupMenu,
popdownMenu,
fixLifeLineHeights,
sanitizeUrl,
};

View File

@ -1,6 +1,6 @@
import { defineConfig, MarkdownOptions } from 'vitepress';
import { version } from '../../../package.json';
import MermaidExample from './mermaid-markdown-all.js';
import { defineConfig, MarkdownOptions } from 'vitepress';
const allMarkdownTransformers: MarkdownOptions = {
// the shiki theme to highlight code blocks
@ -146,13 +146,14 @@ function sidebarSyntax() {
{ text: 'Pie Chart', link: '/syntax/pie' },
{ text: 'Quadrant Chart', link: '/syntax/quadrantChart' },
{ text: 'Requirement Diagram', link: '/syntax/requirementDiagram' },
{ text: 'Gitgraph (Git) Diagram 🔥', link: '/syntax/gitgraph' },
{ text: 'Gitgraph (Git) Diagram', link: '/syntax/gitgraph' },
{ text: 'C4 Diagram 🦺⚠️', link: '/syntax/c4' },
{ text: 'Mindmaps 🔥', link: '/syntax/mindmap' },
{ text: 'Timeline 🔥', link: '/syntax/timeline' },
{ text: 'Zenuml 🔥', link: '/syntax/zenuml' },
{ text: 'Mindmaps', link: '/syntax/mindmap' },
{ text: 'Timeline', link: '/syntax/timeline' },
{ text: 'Zenuml', link: '/syntax/zenuml' },
{ text: 'Sankey 🔥', link: '/syntax/sankey' },
{ text: 'XYChart 🔥', link: '/syntax/xyChart' },
{ text: 'Packet 🔥', link: '/syntax/packet' },
{ text: 'Other Examples', link: '/syntax/examples' },
],
},

View File

@ -0,0 +1,101 @@
# Packet Diagram (v<MERMAID_RELEASE_VERSION>+)
## Introduction
A packet diagram is a visual representation used to illustrate the structure and contents of a network packet. Network packets are the fundamental units of data transferred over a network.
## Usage
This diagram type is particularly useful for network engineers, educators, and students who require a clear and concise way to represent the structure of network packets.
## Syntax
```md
packet-beta
start: "Block name" %% Single-bit block
start-end: "Block name" %% Multi-bit blocks
... More Fields ...
```
## Examples
```mermaid-example
---
title: "TCP Packet"
---
packet-beta
0-15: "Source Port"
16-31: "Destination Port"
32-63: "Sequence Number"
64-95: "Acknowledgment Number"
96-99: "Data Offset"
100-105: "Reserved"
106: "URG"
107: "ACK"
108: "PSH"
109: "RST"
110: "SYN"
111: "FIN"
112-127: "Window"
128-143: "Checksum"
144-159: "Urgent Pointer"
160-191: "(Options and Padding)"
192-255: "Data (variable length)"
```
```mermaid-example
packet-beta
title UDP Packet
0-15: "Source Port"
16-31: "Destination Port"
32-47: "Length"
48-63: "Checksum"
64-95: "Data (variable length)"
```
## Details of Syntax
- **Ranges**: Each line after the title represents a different field in the packet. The range (e.g., `0-15`) indicates the bit positions in the packet.
- **Field Description**: A brief description of what the field represents, enclosed in quotes.
## Configuration
Please refer to the [configuration](/config/schema-docs/config-defs-packet-diagram-config.html) guide for details.
<!--
Theme variables are not currently working due to a mermaid bug. The passed values are not being propogated into styles function.
## Theme Variables
| Property | Description | Default Value |
| ---------------- | -------------------------- | ------------- |
| byteFontSize | Font size of the bytes | '10px' |
| startByteColor | Color of the starting byte | 'black' |
| endByteColor | Color of the ending byte | 'black' |
| labelColor | Color of the labels | 'black' |
| labelFontSize | Font size of the labels | '12px' |
| titleColor | Color of the title | 'black' |
| titleFontSize | Font size of the title | '14px' |
| blockStrokeColor | Color of the block stroke | 'black' |
| blockStrokeWidth | Width of the block stroke | '1' |
| blockFillColor | Fill color of the block | '#efefef' |
## Example on config and theme
```mermaid-example
---
config:
packet:
showBits: true
themeVariables:
packet:
startByteColor: red
---
packet-beta
0-15: "Source Port"
16-31: "Destination Port"
32-63: "Sequence Number"
```
-->

View File

@ -1,4 +1,3 @@
'use strict';
import { vi, it, expect, describe, beforeEach } from 'vitest';
// -------------------------------------
@ -27,26 +26,26 @@ vi.mock('./diagrams/git/gitGraphRenderer.js');
vi.mock('./diagrams/gantt/ganttRenderer.js');
vi.mock('./diagrams/user-journey/journeyRenderer.js');
vi.mock('./diagrams/pie/pieRenderer.js');
vi.mock('./diagrams/packet/renderer.js');
vi.mock('./diagrams/xychart/xychartRenderer.js');
vi.mock('./diagrams/requirement/requirementRenderer.js');
vi.mock('./diagrams/sequence/sequenceRenderer.js');
vi.mock('./diagrams/state/stateRenderer-v2.js');
// -------------------------------------
import mermaid from './mermaid.js';
import assignWithDepth from './assignWithDepth.js';
import type { MermaidConfig } from './config.type.js';
import mermaidAPI, { removeExistingElements } from './mermaidAPI.js';
import {
createCssStyles,
createUserStyles,
import mermaid from './mermaid.js';
import mermaidAPI, {
appendDivSvgG,
cleanUpSvgCode,
createCssStyles,
createUserStyles,
putIntoIFrame,
removeExistingElements,
} from './mermaidAPI.js';
import assignWithDepth from './assignWithDepth.js';
// --------------
// Mocks
// To mock a module, first define a mock for it, then (if used explicitly in the tests) import it. Be sure the path points to exactly the same file as is imported in mermaidAPI (the module being tested)
@ -56,6 +55,7 @@ vi.mock('./styles.js', () => {
default: vi.fn().mockReturnValue(' .userStyle { font-weight:bold; }'),
};
});
import getStyles from './styles.js';
vi.mock('stylis', () => {
@ -65,6 +65,7 @@ vi.mock('stylis', () => {
serialize: vi.fn().mockReturnValue('stylis serialized css'),
};
});
import { compile, serialize } from 'stylis';
import { decodeEntities, encodeEntities } from './utils.js';
import { Diagram } from './Diagram.js';
@ -722,6 +723,8 @@ describe('mermaidAPI', () => {
{ textDiagramType: 'gantt', expectedType: 'gantt' },
{ textDiagramType: 'journey', expectedType: 'journey' },
{ textDiagramType: 'pie', expectedType: 'pie' },
{ textDiagramType: 'packet-beta', expectedType: 'packet' },
{ textDiagramType: 'xychart-beta', expectedType: 'xychart' },
{ textDiagramType: 'requirementDiagram', expectedType: 'requirement' },
{ textDiagramType: 'sequenceDiagram', expectedType: 'sequence' },
{ textDiagramType: 'stateDiagram-v2', expectedType: 'stateDiagram' },

View File

@ -17,7 +17,7 @@ import { compile, serialize, stringify } from 'stylis';
import { version } from '../package.json';
import * as configApi from './config.js';
import { addDiagrams } from './diagram-api/diagram-orchestration.js';
import { Diagram, getDiagramFromText as getDiagramFromTextInternal } from './Diagram.js';
import { Diagram } from './Diagram.js';
import errorRenderer from './diagrams/error/errorRenderer.js';
import { attachFunctions } from './interactionDb.js';
import { log, setLogLevel } from './logger.js';
@ -422,9 +422,9 @@ const render = async function (
let parseEncounteredException;
try {
diag = await getDiagramFromText(text, { title: processed.title });
diag = await Diagram.fromText(text, { title: processed.title });
} catch (error) {
diag = new Diagram('error');
diag = await Diagram.fromText('error');
parseEncounteredException = error;
}
@ -536,7 +536,7 @@ function initialize(options: MermaidConfig = {}) {
const getDiagramFromText = (text: string, metadata: Pick<DiagramMetadata, 'title'> = {}) => {
const { code } = preprocessDiagram(text);
return getDiagramFromTextInternal(code, metadata);
return Diagram.fromText(code, metadata);
};
/**

View File

@ -50,6 +50,7 @@ required:
- gitGraph
- c4
- sankey
- packet
properties:
theme:
description: |
@ -211,6 +212,8 @@ properties:
$ref: '#/$defs/C4DiagramConfig'
sankey:
$ref: '#/$defs/SankeyDiagramConfig'
packet:
$ref: '#/$defs/PacketDiagramConfig'
dompurifyConfig:
title: DOM Purify Configuration
description: Configuration options to pass to the `dompurify` library.
@ -2009,6 +2012,43 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
type: string
default: ''
PacketDiagramConfig:
title: Packet Diagram Config
allOf: [{ $ref: '#/$defs/BaseDiagramConfig' }]
description: The object containing configurations specific for packet diagrams.
type: object
unevaluatedProperties: false
properties:
rowHeight:
description: The height of each row in the packet diagram.
type: number
minimum: 1
default: 32
bitWidth:
description: The width of each bit in the packet diagram.
type: number
minimum: 1
default: 32
bitsPerRow:
description: The number of bits to display per row.
type: number
minimum: 1
default: 32
showBits:
description: Toggle to display or hide bit numbers.
type: boolean
default: true
paddingX:
description: The horizontal padding between the blocks in a row.
type: number
minimum: 0
default: 5
paddingY:
description: The vertical padding between the rows.
type: number
minimum: 0
default: 5
FontCalculator:
title: Font Calculator
description: |

View File

@ -27,6 +27,7 @@ import state from './diagrams/state/styles.js';
import journey from './diagrams/user-journey/styles.js';
import timeline from './diagrams/timeline/styles.js';
import mindmap from './diagrams/mindmap/styles.js';
import packet from './diagrams/packet/styles.js';
import themes from './themes/index.js';
async function checkValidStylisCSSStyleSheet(stylisString: string) {
@ -94,6 +95,7 @@ describe('styles', () => {
sequence,
state,
timeline,
packet,
})) {
test(`should return a valid style for diagram ${diagramId} and theme ${themeId}`, async () => {
const { default: getStyles, addStylesForDiagram } = await import('./styles.js');

View File

@ -1,4 +1,4 @@
import { invert, lighten, darken, rgba, adjust, isDark } from 'khroma';
import { adjust, darken, invert, isDark, lighten, rgba } from 'khroma';
import { mkBorder } from './theme-helpers.js';
class Theme {
@ -268,6 +268,15 @@ class Theme {
'#3498db,#2ecc71,#e74c3c,#f1c40f,#bdc3c7,#ffffff,#34495e,#9b59b6,#1abc9c,#e67e22',
};
this.packet = {
startByteColor: this.primaryTextColor,
endByteColor: this.primaryTextColor,
labelColor: this.primaryTextColor,
titleColor: this.primaryTextColor,
blockStrokeColor: this.primaryTextColor,
blockFillColor: this.background,
};
/* class */
this.classText = this.primaryTextColor;

View File

@ -1,9 +1,9 @@
import { darken, lighten, adjust, invert, isDark } from 'khroma';
import { mkBorder } from './theme-helpers.js';
import { adjust, darken, invert, isDark, lighten } from 'khroma';
import {
oldAttributeBackgroundColorEven,
oldAttributeBackgroundColorOdd,
} from './erDiagram-oldHardcodedValues.js';
import { mkBorder } from './theme-helpers.js';
class Theme {
constructor() {
@ -240,6 +240,15 @@ class Theme {
this.quadrantExternalBorderStrokeFill || this.primaryBorderColor;
this.quadrantTitleFill = this.quadrantTitleFill || this.primaryTextColor;
this.packet = {
startByteColor: this.primaryTextColor,
endByteColor: this.primaryTextColor,
labelColor: this.primaryTextColor,
titleColor: this.primaryTextColor,
blockStrokeColor: this.primaryTextColor,
blockFillColor: this.mainBkg,
};
/* xychart */
this.xyChart = {
backgroundColor: this.xyChart?.backgroundColor || this.background,

View File

@ -32,3 +32,5 @@ export interface EdgeData {
labelStyle: string;
curve: any;
}
export type ArrayElement<A> = A extends readonly (infer T)[] ? T : never;

View File

@ -6,6 +6,11 @@
"grammar": "src/language/info/info.langium",
"fileExtensions": [".mmd", ".mermaid"]
},
{
"id": "packet",
"grammar": "src/language/packet/packet.langium",
"fileExtensions": [".mmd", ".mermaid"]
},
{
"id": "pie",
"grammar": "src/language/pie/pie.langium",

View File

@ -1,2 +1,11 @@
import type { AstNode } from 'langium';
export * from './language/index.js';
export * from './parse.js';
/**
* Exclude/omit all `AstNode` attributes recursively.
*/
export type RecursiveAstOmit<T> = T extends object
? { [P in keyof T as Exclude<P, keyof AstNode>]: RecursiveAstOmit<T[P]> }
: T;

View File

@ -1,19 +1,25 @@
export {
Info,
MermaidAstType,
Packet,
PacketBlock,
Pie,
PieSection,
isCommon,
isInfo,
isPacket,
isPacketBlock,
isPie,
isPieSection,
} from './generated/ast.js';
export {
InfoGeneratedModule,
MermaidGeneratedSharedModule,
PacketGeneratedModule,
PieGeneratedModule,
} from './generated/module.js';
export * from './common/index.js';
export * from './info/index.js';
export * from './packet/index.js';
export * from './pie/index.js';

View File

@ -0,0 +1 @@
export * from './module.js';

View File

@ -0,0 +1,68 @@
import type {
DefaultSharedModuleContext,
LangiumServices,
LangiumSharedServices,
Module,
PartialLangiumServices,
} from 'langium';
import { EmptyFileSystem, createDefaultModule, createDefaultSharedModule, inject } from 'langium';
import { CommonValueConverter } from '../common/valueConverter.js';
import { MermaidGeneratedSharedModule, PacketGeneratedModule } from '../generated/module.js';
import { PacketTokenBuilder } from './tokenBuilder.js';
/**
* Declaration of `Packet` services.
*/
type PacketAddedServices = {
parser: {
TokenBuilder: PacketTokenBuilder;
ValueConverter: CommonValueConverter;
};
};
/**
* Union of Langium default services and `Packet` services.
*/
export type PacketServices = LangiumServices & PacketAddedServices;
/**
* Dependency injection module that overrides Langium default services and
* contributes the declared `Packet` services.
*/
export const PacketModule: Module<PacketServices, PartialLangiumServices & PacketAddedServices> = {
parser: {
TokenBuilder: () => new PacketTokenBuilder(),
ValueConverter: () => new CommonValueConverter(),
},
};
/**
* Create the full set of services required by Langium.
*
* First inject the shared services by merging two modules:
* - Langium default shared services
* - Services generated by langium-cli
*
* Then inject the language-specific services by merging three modules:
* - Langium default language-specific services
* - Services generated by langium-cli
* - Services specified in this file
* @param context - Optional module context with the LSP connection
* @returns An object wrapping the shared services and the language-specific services
*/
export function createPacketServices(context: DefaultSharedModuleContext = EmptyFileSystem): {
shared: LangiumSharedServices;
Packet: PacketServices;
} {
const shared: LangiumSharedServices = inject(
createDefaultSharedModule(context),
MermaidGeneratedSharedModule
);
const Packet: PacketServices = inject(
createDefaultModule({ shared }),
PacketGeneratedModule,
PacketModule
);
shared.ServiceRegistry.register(Packet);
return { shared, Packet };
}

View File

@ -0,0 +1,19 @@
grammar Packet
import "../common/common";
entry Packet:
NEWLINE*
"packet-beta"
(
NEWLINE* TitleAndAccessibilities blocks+=PacketBlock*
| NEWLINE+ blocks+=PacketBlock+
| NEWLINE*
)
;
PacketBlock:
start=INT('-' end=INT)? ':' label=STRING EOL
;
terminal INT returns number: /0|[1-9][0-9]*/;
terminal STRING: /"[^"]*"|'[^']*'/;

View File

@ -0,0 +1,7 @@
import { AbstractMermaidTokenBuilder } from '../common/index.js';
export class PacketTokenBuilder extends AbstractMermaidTokenBuilder {
public constructor() {
super(['packet-beta']);
}
}

View File

@ -1,34 +1,41 @@
import type { LangiumParser, ParseResult } from 'langium';
import type { Info, Pie } from './index.js';
import { createInfoServices, createPieServices } from './language/index.js';
import type { Info, Packet, Pie } from './index.js';
export type DiagramAST = Info | Pie;
export type DiagramAST = Info | Packet | Pie;
const parsers: Record<string, LangiumParser> = {};
const initializers = {
info: () => {
// Will have to make parse async to use this. Can try later...
// const { createInfoServices } = await import('./language/info/index.js');
parsers['info'] = createInfoServices().Info.parser.LangiumParser;
info: async () => {
const { createInfoServices } = await import('./language/info/index.js');
const parser = createInfoServices().Info.parser.LangiumParser;
parsers['info'] = parser;
},
pie: () => {
parsers['pie'] = createPieServices().Pie.parser.LangiumParser;
packet: async () => {
const { createPacketServices } = await import('./language/packet/index.js');
const parser = createPacketServices().Packet.parser.LangiumParser;
parsers['packet'] = parser;
},
pie: async () => {
const { createPieServices } = await import('./language/pie/index.js');
const parser = createPieServices().Pie.parser.LangiumParser;
parsers['pie'] = parser;
},
} as const;
export function parse(diagramType: 'info', text: string): Info;
export function parse(diagramType: 'pie', text: string): Pie;
export function parse<T extends DiagramAST>(
export async function parse(diagramType: 'info', text: string): Promise<Info>;
export async function parse(diagramType: 'packet', text: string): Promise<Packet>;
export async function parse(diagramType: 'pie', text: string): Promise<Pie>;
export async function parse<T extends DiagramAST>(
diagramType: keyof typeof initializers,
text: string
): T {
): Promise<T> {
const initializer = initializers[diagramType];
if (!initializer) {
throw new Error(`Unknown diagram type: ${diagramType}`);
}
if (!parsers[diagramType]) {
initializer();
await initializer();
}
const parser: LangiumParser = parsers[diagramType];
const result: ParseResult<T> = parser.parse<T>(text);

682
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff