diff --git a/.build/jsonSchema.ts b/.build/jsonSchema.ts index d29ef7920a..dd66d984c5 100644 --- a/.build/jsonSchema.ts +++ b/.build/jsonSchema.ts @@ -24,6 +24,7 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [ 'gitGraph', 'c4', 'sankey', + 'packet', ] as const; /** diff --git a/.esbuild/build.ts b/.esbuild/build.ts index 3e829d7193..3c87f9d621 100644 --- a/.esbuild/build.ts +++ b/.esbuild/build.ts @@ -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); } diff --git a/__mocks__/c4Renderer.js b/__mocks__/c4Renderer.js deleted file mode 100644 index 576d5d8634..0000000000 --- a/__mocks__/c4Renderer.js +++ /dev/null @@ -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, -}; diff --git a/__mocks__/classRenderer-v2.js b/__mocks__/classRenderer-v2.js deleted file mode 100644 index 1ad95806fc..0000000000 --- a/__mocks__/classRenderer-v2.js +++ /dev/null @@ -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, -}; diff --git a/__mocks__/classRenderer.js b/__mocks__/classRenderer.js deleted file mode 100644 index 1c20de4b18..0000000000 --- a/__mocks__/classRenderer.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Mocked class diagram renderer - */ - -import { vi } from 'vitest'; - -export const draw = vi.fn().mockImplementation(() => { - return ''; -}); - -export default { - draw, -}; diff --git a/__mocks__/dagre-d3.ts b/__mocks__/dagre-d3.ts deleted file mode 100644 index bf6d341dc5..0000000000 --- a/__mocks__/dagre-d3.ts +++ /dev/null @@ -1 +0,0 @@ -// DO NOT delete this file. It is used by vitest to mock the dagre-d3 module. diff --git a/__mocks__/entity-decode/browser.ts b/__mocks__/entity-decode/browser.ts deleted file mode 100644 index bd82d79fd9..0000000000 --- a/__mocks__/entity-decode/browser.ts +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = function (txt: string) { - return txt; -}; diff --git a/__mocks__/erRenderer.js b/__mocks__/erRenderer.js deleted file mode 100644 index 845d641f75..0000000000 --- a/__mocks__/erRenderer.js +++ /dev/null @@ -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, -}; diff --git a/__mocks__/flowRenderer-v2.js b/__mocks__/flowRenderer-v2.js deleted file mode 100644 index 89cc86031e..0000000000 --- a/__mocks__/flowRenderer-v2.js +++ /dev/null @@ -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, -}; diff --git a/__mocks__/ganttRenderer.js b/__mocks__/ganttRenderer.js deleted file mode 100644 index 9572498321..0000000000 --- a/__mocks__/ganttRenderer.js +++ /dev/null @@ -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, -}; diff --git a/__mocks__/gitGraphRenderer.js b/__mocks__/gitGraphRenderer.js deleted file mode 100644 index 1daa82ca4c..0000000000 --- a/__mocks__/gitGraphRenderer.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Mocked git (graph) diagram renderer - */ - -import { vi } from 'vitest'; - -export const draw = vi.fn().mockImplementation(() => { - return ''; -}); - -export default { - draw, -}; diff --git a/__mocks__/journeyRenderer.js b/__mocks__/journeyRenderer.js deleted file mode 100644 index 2bc77c0b10..0000000000 --- a/__mocks__/journeyRenderer.js +++ /dev/null @@ -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, -}; diff --git a/__mocks__/pieRenderer.ts b/__mocks__/pieRenderer.ts deleted file mode 100644 index 439800f8c5..0000000000 --- a/__mocks__/pieRenderer.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Mocked pie (picChart) diagram renderer - */ -import { vi } from 'vitest'; - -const draw = vi.fn().mockImplementation(() => ''); - -export const renderer = { draw }; diff --git a/__mocks__/requirementRenderer.js b/__mocks__/requirementRenderer.js deleted file mode 100644 index 48d8997ac1..0000000000 --- a/__mocks__/requirementRenderer.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Mocked requirement diagram renderer - */ - -import { vi } from 'vitest'; - -export const draw = vi.fn().mockImplementation(() => { - return ''; -}); - -export default { - draw, -}; diff --git a/__mocks__/sankeyRenderer.js b/__mocks__/sankeyRenderer.js deleted file mode 100644 index 76324c93f1..0000000000 --- a/__mocks__/sankeyRenderer.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Mocked Sankey diagram renderer - */ - -import { vi } from 'vitest'; - -export const draw = vi.fn().mockImplementation(() => { - return ''; -}); - -export default { - draw, -}; diff --git a/__mocks__/sequenceRenderer.js b/__mocks__/sequenceRenderer.js deleted file mode 100644 index 11080c6bbf..0000000000 --- a/__mocks__/sequenceRenderer.js +++ /dev/null @@ -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, -}; diff --git a/__mocks__/stateRenderer-v2.js b/__mocks__/stateRenderer-v2.js deleted file mode 100644 index a2d103b50e..0000000000 --- a/__mocks__/stateRenderer-v2.js +++ /dev/null @@ -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, -}; diff --git a/cypress/integration/rendering/packet.spec.ts b/cypress/integration/rendering/packet.spec.ts new file mode 100644 index 0000000000..61555ea530 --- /dev/null +++ b/cypress/integration/rendering/packet.spec.ts @@ -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" + ` + ); + }); +}); diff --git a/demos/dev/example.html b/demos/dev/example.html index adb6da331d..4ece7efa8b 100644 --- a/demos/dev/example.html +++ b/demos/dev/example.html @@ -3,10 +3,24 @@ Mermaid development page + -
info
-
 graph TB
       a --> b
@@ -15,22 +29,35 @@
       c --> d
     
-
+
+ Type code to view diagram: +
+ +
+
+
info
diff --git a/demos/index.html b/demos/index.html index c634aad2d4..11fa144365 100644 --- a/demos/index.html +++ b/demos/index.html @@ -81,6 +81,9 @@

ZenUML

  • Sankey

  • +
  • +

    Packet

    +
  • diff --git a/demos/packet.html b/demos/packet.html new file mode 100644 index 0000000000..f332dcf8cf --- /dev/null +++ b/demos/packet.html @@ -0,0 +1,141 @@ + + + + + + Mermaid Quick Test Page + + + + + +

    Packet diagram demo

    + +
    +
    +      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"
    +    
    + +
    +      ---
    +      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"
    +    
    + +
    +      ---
    +      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"
    +    
    + +
    +      ---
    +      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"
    +    
    +
    + + + + + diff --git a/docs/config/setup/modules/defaultConfig.md b/docs/config/setup/modules/defaultConfig.md index 7a9b891c43..d3495bc0c3 100644 --- a/docs/config/setup/modules/defaultConfig.md +++ b/docs/config/setup/modules/defaultConfig.md @@ -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) --- diff --git a/docs/syntax/packet.md b/docs/syntax/packet.md new file mode 100644 index 0000000000..5cd0b5638a --- /dev/null +++ b/docs/syntax/packet.md @@ -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\+) + +## 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. + + diff --git a/packages/mermaid/src/config.type.ts b/packages/mermaid/src/config.type.ts index fc5f2ec5dc..e9c2fecbf2 100644 --- a/packages/mermaid/src/config.type.ts +++ b/packages/mermaid/src/config.type.ts @@ -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". diff --git a/packages/mermaid/src/defaultConfig.ts b/packages/mermaid/src/defaultConfig.ts index fb9db0c6a9..76a8152b7a 100644 --- a/packages/mermaid/src/defaultConfig.ts +++ b/packages/mermaid/src/defaultConfig.ts @@ -257,6 +257,9 @@ const config: RequiredDeep = { // TODO: can we make this default to `true` instead? useMaxWidth: false, }, + packet: { + ...defaultConfigJson.packet, + }, }; const keyify = (obj: any, prefix = ''): string[] => diff --git a/packages/mermaid/src/diagram-api/diagram-orchestration.ts b/packages/mermaid/src/diagram-api/diagram-orchestration.ts index d6b9c00d8f..f6a89459bf 100644 --- a/packages/mermaid/src/diagram-api/diagram-orchestration.ts +++ b/packages/mermaid/src/diagram-api/diagram-orchestration.ts @@ -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 ); }; diff --git a/packages/mermaid/src/diagram-api/types.ts b/packages/mermaid/src/diagram-api/types.ts index d4f34de702..88957b5fb8 100644 --- a/packages/mermaid/src/diagram-api/types.ts +++ b/packages/mermaid/src/diagram-api/types.ts @@ -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 = { + getConfig: () => Required; +} & 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. diff --git a/packages/mermaid/src/diagrams/packet/db.ts b/packages/mermaid/src/diagrams/packet/db.ts new file mode 100644 index 0000000000..d7b5504723 --- /dev/null +++ b/packages/mermaid/src/diagrams/packet/db.ts @@ -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 = DEFAULT_CONFIG.packet; + +const getConfig = (): Required => { + 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, +}; diff --git a/packages/mermaid/src/diagrams/packet/detector.ts b/packages/mermaid/src/diagrams/packet/detector.ts new file mode 100644 index 0000000000..5aca92e6cf --- /dev/null +++ b/packages/mermaid/src/diagrams/packet/detector.ts @@ -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, +}; diff --git a/packages/mermaid/src/diagrams/packet/diagram.ts b/packages/mermaid/src/diagrams/packet/diagram.ts new file mode 100644 index 0000000000..a73a77c052 --- /dev/null +++ b/packages/mermaid/src/diagrams/packet/diagram.ts @@ -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, +}; diff --git a/packages/mermaid/src/diagrams/packet/packet.spec.ts b/packages/mermaid/src/diagrams/packet/packet.spec.ts new file mode 100644 index 0000000000..87432f4891 --- /dev/null +++ b/packages/mermaid/src/diagrams/packet/packet.spec.ts @@ -0,0 +1,192 @@ +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', () => { + const str = `packet-beta`; + expect(() => { + parser.parse(str); + }).not.toThrow(); + expect(getPacket()).toMatchInlineSnapshot('[]'); + }); + + it('should handle diagram with data and title', () => { + const str = `packet-beta + title Packet diagram + accTitle: Packet accTitle + accDescr: Packet accDescription + 0-10: "test" + `; + expect(() => { + parser.parse(str); + }).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', () => { + const str = `packet-beta + 0-10: "test" + 11: "single" + `; + expect(() => { + parser.parse(str); + }).not.toThrow(); + expect(getPacket()).toMatchInlineSnapshot(` + [ + [ + { + "end": 10, + "label": "test", + "start": 0, + }, + { + "end": 11, + "label": "single", + "start": 11, + }, + ], + ] + `); + }); + + it('should split into multiple rows', () => { + const str = `packet-beta + 0-10: "test" + 11-90: "multiple" + `; + expect(() => { + parser.parse(str); + }).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', () => { + const str = `packet-beta + 0-16: "test" + 17-63: "multiple" + `; + expect(() => { + parser.parse(str); + }).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', () => { + const str = `packet-beta + 0-16: "test" + 18-20: "error" + `; + expect(() => { + parser.parse(str); + }).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', () => { + const str = `packet-beta + 0-16: "test" + 18: "error" + `; + expect(() => { + parser.parse(str); + }).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', () => { + const str = `packet-beta + 0-16: "test" + 17: "good" + 19: "error" + `; + expect(() => { + parser.parse(str); + }).toThrowErrorMatchingInlineSnapshot( + '"Packet block 19 - 19 is not contiguous. It should start from 18."' + ); + }); + + it('should throw error if end is less than start', () => { + const str = `packet-beta + 0-16: "test" + 25-20: "error" + `; + expect(() => { + parser.parse(str); + }).toThrowErrorMatchingInlineSnapshot( + '"Packet block 25 - 20 is invalid. End must be greater than start."' + ); + }); +}); diff --git a/packages/mermaid/src/diagrams/packet/parser.ts b/packages/mermaid/src/diagrams/packet/parser.ts new file mode 100644 index 0000000000..d7cc1f06fb --- /dev/null +++ b/packages/mermaid/src/diagrams/packet/parser.ts @@ -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 | 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, 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: (input: string): void => { + const ast: Packet = parse('packet', input); + log.debug(ast); + populate(ast); + }, +}; diff --git a/packages/mermaid/src/diagrams/packet/renderer.ts b/packages/mermaid/src/diagrams/packet/renderer.ts new file mode 100644 index 0000000000..84feb8c439 --- /dev/null +++ b/packages/mermaid/src/diagrams/packet/renderer.ts @@ -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 +) => { + 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 }; diff --git a/packages/mermaid/src/diagrams/packet/styles.ts b/packages/mermaid/src/diagrams/packet/styles.ts new file mode 100644 index 0000000000..ff940d0e63 --- /dev/null +++ b/packages/mermaid/src/diagrams/packet/styles.ts @@ -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; diff --git a/packages/mermaid/src/diagrams/packet/types.ts b/packages/mermaid/src/diagrams/packet/types.ts new file mode 100644 index 0000000000..ea3c5d0dda --- /dev/null +++ b/packages/mermaid/src/diagrams/packet/types.ts @@ -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>; +export type PacketWord = Required[]; + +export interface PacketDB extends DiagramDBBase { + 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[]; +} diff --git a/packages/mermaid/src/docs/.vitepress/config.ts b/packages/mermaid/src/docs/.vitepress/config.ts index dfea8843f0..c11af59044 100644 --- a/packages/mermaid/src/docs/.vitepress/config.ts +++ b/packages/mermaid/src/docs/.vitepress/config.ts @@ -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' }, ], }, diff --git a/packages/mermaid/src/docs/syntax/packet.md b/packages/mermaid/src/docs/syntax/packet.md new file mode 100644 index 0000000000..b509cf130f --- /dev/null +++ b/packages/mermaid/src/docs/syntax/packet.md @@ -0,0 +1,101 @@ +# Packet Diagram (v+) + +## 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. + + diff --git a/packages/mermaid/src/mermaidAPI.spec.ts b/packages/mermaid/src/mermaidAPI.spec.ts index ff7cc4bfd2..901e19ea1b 100644 --- a/packages/mermaid/src/mermaidAPI.spec.ts +++ b/packages/mermaid/src/mermaidAPI.spec.ts @@ -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' }, diff --git a/packages/mermaid/src/schemas/config.schema.yaml b/packages/mermaid/src/schemas/config.schema.yaml index fd88b3b5e4..bf8b90b832 100644 --- a/packages/mermaid/src/schemas/config.schema.yaml +++ b/packages/mermaid/src/schemas/config.schema.yaml @@ -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: | diff --git a/packages/mermaid/src/styles.spec.ts b/packages/mermaid/src/styles.spec.ts index d22b0f2ee3..a139ff526e 100644 --- a/packages/mermaid/src/styles.spec.ts +++ b/packages/mermaid/src/styles.spec.ts @@ -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'); diff --git a/packages/mermaid/src/themes/theme-dark.js b/packages/mermaid/src/themes/theme-dark.js index c566251090..dc7a600098 100644 --- a/packages/mermaid/src/themes/theme-dark.js +++ b/packages/mermaid/src/themes/theme-dark.js @@ -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; diff --git a/packages/mermaid/src/themes/theme-forest.js b/packages/mermaid/src/themes/theme-forest.js index 0270f51ff9..eda905c661 100644 --- a/packages/mermaid/src/themes/theme-forest.js +++ b/packages/mermaid/src/themes/theme-forest.js @@ -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, diff --git a/packages/mermaid/src/types.ts b/packages/mermaid/src/types.ts index 13da885033..487e089dce 100644 --- a/packages/mermaid/src/types.ts +++ b/packages/mermaid/src/types.ts @@ -32,3 +32,5 @@ export interface EdgeData { labelStyle: string; curve: any; } + +export type ArrayElement = A extends readonly (infer T)[] ? T : never; diff --git a/packages/parser/langium-config.json b/packages/parser/langium-config.json index 4ffaaf372f..6daa551c17 100644 --- a/packages/parser/langium-config.json +++ b/packages/parser/langium-config.json @@ -5,6 +5,11 @@ "id": "info", "grammar": "src/language/info/info.langium", "fileExtensions": [".mmd", ".mermaid"] + }, + { + "id": "packet", + "grammar": "src/language/packet/packet.langium", + "fileExtensions": [".mmd", ".mermaid"] } ], "mode": "production", diff --git a/packages/parser/src/index.ts b/packages/parser/src/index.ts index 9dded54fa2..c72b4fcb6e 100644 --- a/packages/parser/src/index.ts +++ b/packages/parser/src/index.ts @@ -1,3 +1,12 @@ -export type { Info } from './language/index.js'; +import type { AstNode } from 'langium'; + +export type { Info, Packet } from './language/index.js'; +export { MermaidParseError, parse } from './parse.js'; export type { DiagramAST } from './parse.js'; -export { parse, MermaidParseError } from './parse.js'; + +/** + * Exclude/omit all `AstNode` attributes recursively. + */ +export type RecursiveAstOmit = T extends object + ? { [P in keyof T as Exclude]: RecursiveAstOmit } + : T; diff --git a/packages/parser/src/language/index.ts b/packages/parser/src/language/index.ts index b6685a07f6..a1c3dfd8d3 100644 --- a/packages/parser/src/language/index.ts +++ b/packages/parser/src/language/index.ts @@ -4,3 +4,4 @@ export * from './generated/module.js'; export * from './common/index.js'; export * from './info/index.js'; +export * from './packet/index.js'; diff --git a/packages/parser/src/language/packet/index.ts b/packages/parser/src/language/packet/index.ts new file mode 100644 index 0000000000..fd3c604b08 --- /dev/null +++ b/packages/parser/src/language/packet/index.ts @@ -0,0 +1 @@ +export * from './module.js'; diff --git a/packages/parser/src/language/packet/module.ts b/packages/parser/src/language/packet/module.ts new file mode 100644 index 0000000000..183ec0ff83 --- /dev/null +++ b/packages/parser/src/language/packet/module.ts @@ -0,0 +1,71 @@ +import type { + DefaultSharedModuleContext, + LangiumServices, + LangiumSharedServices, + Module, + PartialLangiumServices, +} from 'langium'; +import { EmptyFileSystem, createDefaultModule, createDefaultSharedModule, inject } from 'langium'; +import { CommonLexer } from '../common/lexer.js'; +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: { + Lexer: CommonLexer; + 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 = { + parser: { + Lexer: (services: PacketServices) => new CommonLexer(services), + 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 }; +} diff --git a/packages/parser/src/language/packet/packet.langium b/packages/parser/src/language/packet/packet.langium new file mode 100644 index 0000000000..dce1c1ef86 --- /dev/null +++ b/packages/parser/src/language/packet/packet.langium @@ -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 NEWLINE+ +; + +terminal INT returns number: /0|[1-9][0-9]*/; +terminal STRING: /"[^"]*"|'[^']*'/; diff --git a/packages/parser/src/language/packet/tokenBuilder.ts b/packages/parser/src/language/packet/tokenBuilder.ts new file mode 100644 index 0000000000..3317f85496 --- /dev/null +++ b/packages/parser/src/language/packet/tokenBuilder.ts @@ -0,0 +1,7 @@ +import { MermaidTokenBuilder } from '../common/index.js'; + +export class PacketTokenBuilder extends MermaidTokenBuilder { + public constructor() { + super(['packet-beta']); + } +} diff --git a/packages/parser/src/parse.ts b/packages/parser/src/parse.ts index 90358bbf16..eba118e417 100644 --- a/packages/parser/src/parse.ts +++ b/packages/parser/src/parse.ts @@ -1,8 +1,8 @@ import type { LangiumParser, ParseResult } from 'langium'; -import type { Info } from './index.js'; -import { createInfoServices } from './language/index.js'; +import type { Info, Packet } from './index.js'; +import { createInfoServices, createPacketServices } from './language/index.js'; -export type DiagramAST = Info; +export type DiagramAST = Info | Packet; const parsers: Record = {}; @@ -13,8 +13,13 @@ const initializers = { const parser = createInfoServices().Info.parser.LangiumParser; parsers['info'] = parser; }, + packet: () => { + const parser = createPacketServices().Packet.parser.LangiumParser; + parsers['packet'] = parser; + }, } as const; export function parse(diagramType: 'info', text: string): Info; +export function parse(diagramType: 'packet', text: string): Packet; export function parse( diagramType: keyof typeof initializers, text: string diff --git a/scripts/editor.bash b/scripts/editor.bash index fd9b415825..421fdec747 100755 --- a/scripts/editor.bash +++ b/scripts/editor.bash @@ -13,8 +13,8 @@ mv package.tmp.json package.json popd pnpm run -r clean +pnpm build:esbuild pnpm build:types -pnpm build:mermaid # Clone the Mermaid Live Editor repository rm -rf mermaid-live-editor