From 70cd2852d3ef3a3a70c65984f99be085b0c641ef Mon Sep 17 00:00:00 2001
From: Angelo Ceccato <angeloceccato@IT-mac-cean-669341865-old.local>
Date: Sat, 2 Mar 2024 18:29:25 +0100
Subject: [PATCH] feat(1624): first draft, prototype of context map grammar and
 diagram

---
 .vite/jsonSchemaPlugin.ts                     |   1 +
 demos/contextMap.html                         |  74 +++
 demos/index.html                              |   3 +
 docs/config/setup/modules/defaultConfig.md    |   2 +-
 .../scripts/create-types-from-json-schema.mts |   1 +
 packages/mermaid/src/config.type.ts           |  36 ++
 packages/mermaid/src/defaultConfig.ts         |   4 +
 .../diagram-api/diagram-orchestration.spec.ts |   1 +
 .../src/diagram-api/diagram-orchestration.ts  |   2 +
 .../context-map/contextMap-definition.ts      |  11 +
 .../diagrams/context-map/contextMap.spec.ts   | 116 +++++
 .../src/diagrams/context-map/contextMap.ts    |  85 ++++
 .../src/diagrams/context-map/contextMapDb.js  |  47 ++
 .../context-map/contextMapRenderer.js         |  57 +++
 .../src/diagrams/context-map/detector.ts      |  23 +
 .../src/diagrams/context-map/drawSvg.spec.ts  | 263 +++++++++++
 .../src/diagrams/context-map/drawSvg.ts       | 426 ++++++++++++++++++
 .../context-map/parser/contextMap.jison       |  74 +++
 .../context-map/parser/contextMap.spec.js     | 287 ++++++++++++
 .../src/diagrams/mindmap/parser/mindmap.jison |   1 +
 .../mermaid/src/schemas/config.schema.yaml    |  66 +++
 21 files changed, 1579 insertions(+), 1 deletion(-)
 create mode 100644 demos/contextMap.html
 create mode 100644 packages/mermaid/src/diagrams/context-map/contextMap-definition.ts
 create mode 100644 packages/mermaid/src/diagrams/context-map/contextMap.spec.ts
 create mode 100644 packages/mermaid/src/diagrams/context-map/contextMap.ts
 create mode 100644 packages/mermaid/src/diagrams/context-map/contextMapDb.js
 create mode 100644 packages/mermaid/src/diagrams/context-map/contextMapRenderer.js
 create mode 100644 packages/mermaid/src/diagrams/context-map/detector.ts
 create mode 100644 packages/mermaid/src/diagrams/context-map/drawSvg.spec.ts
 create mode 100644 packages/mermaid/src/diagrams/context-map/drawSvg.ts
 create mode 100644 packages/mermaid/src/diagrams/context-map/parser/contextMap.jison
 create mode 100644 packages/mermaid/src/diagrams/context-map/parser/contextMap.spec.js

diff --git a/.vite/jsonSchemaPlugin.ts b/.vite/jsonSchemaPlugin.ts
index dd9af8cc55..0456c485dd 100644
--- a/.vite/jsonSchemaPlugin.ts
+++ b/.vite/jsonSchemaPlugin.ts
@@ -21,6 +21,7 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [
   'xyChart',
   'requirement',
   'mindmap',
+  'contextMap',
   'timeline',
   'gitGraph',
   'c4',
diff --git a/demos/contextMap.html b/demos/contextMap.html
new file mode 100644
index 0000000000..2d9bea413b
--- /dev/null
+++ b/demos/contextMap.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <title>Context Map Language Quick Test Page</title>
+    <link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=" />
+    <style>
+      div.mermaid {
+        /* font-family: 'trebuchet ms', verdana, arial; */
+        font-family: 'Courier New', Courier, monospace !important;
+      }
+    </style>
+  </head>
+
+  <body>
+    <h1>Context Map demo</h1>
+    <pre class="mermaid">
+    ContextMap DDDSampleMap {
+      contains CargoBookingContext
+      contains VoyagePlanningContext
+      contains LocationContext
+
+      CargoBookingContext [SK]<->[SK] VoyagePlanningContext
+      CargoBookingContext [D]<-[U,OHS,PL] LocationContext
+      VoyagePlanningContext [D]<-[U,OHS,PL] LocationContext
+    }
+   </pre
+    >
+
+    <pre class="mermaid">
+  ContextMap InsuranceContextMap {
+    contains CustomerManagementContext 
+    contains CustomerSelfServiceContext 
+    contains PrintingContext
+    contains PolicyManagementContext 
+    contains RiskManagementContext 
+    contains DebtCollection
+
+    CustomerSelfServiceContext [D,C]<-[U,S] CustomerManagementContext 
+    CustomerManagementContext [D,ACL]<-[U,OHS,PL] PrintingContext 
+    PrintingContext [U,OHS,PL]->[D,ACL] PolicyManagementContext 
+    RiskManagementContext [P]<->[P] PolicyManagementContext 
+    PolicyManagementContext [D,CF]<-[U,OHS,PL] CustomerManagementContext 
+    DebtCollection [D,ACL]<-[U,OHS,PL] PrintingContext 
+    PolicyManagementContext [SK]<->[SK] DebtCollection 
+  }
+
+
+   </pre
+    >
+
+    <script type="module">
+      import mermaid from './mermaid.esm.mjs';
+
+      mermaid.parseError = function (err, hash) {
+        // console.error('Mermaid error: ', err);
+      };
+      mermaid.initialize({
+        theme: 'base',
+        startOnLoad: true,
+        logLevel: 0,
+        useMaxWidth: false,
+      });
+      function callback() {
+        alert('It worked');
+      }
+      mermaid.parseError = function (err, hash) {
+        console.error('In parse error:');
+        console.error(err);
+      };
+    </script>
+  </body>
+</html>
diff --git a/demos/index.html b/demos/index.html
index efe054b4d5..01cc05f192 100644
--- a/demos/index.html
+++ b/demos/index.html
@@ -54,6 +54,9 @@ <h2><a href="./journey.html">Journey</a></h2>
       <li>
         <h2><a href="./mindmap.html">Mindmap</a></h2>
       </li>
+      <li>
+        <h2><a href="./contextMap.html">Context Map</a></h2>
+      </li>
       <li>
         <h2><a href="./pie.html">Pie</a></h2>
       </li>
diff --git a/docs/config/setup/modules/defaultConfig.md b/docs/config/setup/modules/defaultConfig.md
index 7a9b891c43..b329cfd0bd 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:276](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L276)
 
 ---
 
diff --git a/packages/mermaid/scripts/create-types-from-json-schema.mts b/packages/mermaid/scripts/create-types-from-json-schema.mts
index b028fe818d..0acb4c471c 100644
--- a/packages/mermaid/scripts/create-types-from-json-schema.mts
+++ b/packages/mermaid/scripts/create-types-from-json-schema.mts
@@ -50,6 +50,7 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [
   'xyChart',
   'requirement',
   'mindmap',
+  'contextMap',
   'timeline',
   'gitGraph',
   'c4',
diff --git a/packages/mermaid/src/config.type.ts b/packages/mermaid/src/config.type.ts
index 0ba3178680..28b645e7ae 100644
--- a/packages/mermaid/src/config.type.ts
+++ b/packages/mermaid/src/config.type.ts
@@ -166,6 +166,7 @@ export interface MermaidConfig {
   xyChart?: XYChartConfig;
   requirement?: RequirementDiagramConfig;
   mindmap?: MindmapDiagramConfig;
+  contextMap?: MiniContextMapLanguageDiagramConfig;
   gitGraph?: GitGraphDiagramConfig;
   c4?: C4DiagramConfig;
   sankey?: SankeyDiagramConfig;
@@ -639,6 +640,41 @@ export interface MindmapDiagramConfig extends BaseDiagramConfig {
   padding?: number;
   maxNodeWidth?: number;
 }
+/**
+ * The object containing configurations specific for mini context map language diagrams
+ *
+ * This interface was referenced by `MermaidConfig`'s JSON-Schema
+ * via the `definition` "MiniContextMapLanguageDiagramConfig".
+ */
+export interface MiniContextMapLanguageDiagramConfig extends BaseDiagramConfig {
+  width?: number;
+  height?: number;
+  nodeMargin?: ContextMapNodeMargin;
+  nodePadding?: ContextMapNodePadding;
+  font?: ContextMapFont;
+}
+/**
+ * margins of nodes
+ */
+export interface ContextMapNodeMargin {
+  horizontal?: number;
+  vertical?: number;
+}
+/**
+ * padding of nodes
+ */
+export interface ContextMapNodePadding {
+  horizontal?: number;
+  vertical?: number;
+}
+/**
+ * Font of all Context Map texts
+ */
+export interface ContextMapFont {
+  fontFamily?: string;
+  fontSize?: number;
+  fontWeight?: number;
+}
 /**
  * This interface was referenced by `MermaidConfig`'s JSON-Schema
  * via the `definition` "PieDiagramConfig".
diff --git a/packages/mermaid/src/defaultConfig.ts b/packages/mermaid/src/defaultConfig.ts
index fb9db0c6a9..46265d6231 100644
--- a/packages/mermaid/src/defaultConfig.ts
+++ b/packages/mermaid/src/defaultConfig.ts
@@ -257,6 +257,10 @@ const config: RequiredDeep<MermaidConfig> = {
     // TODO: can we make this default to `true` instead?
     useMaxWidth: false,
   },
+  contextMap: {
+    ...defaultConfigJson.contextMap,
+    useMaxWidth: false,
+  },
 };
 
 const keyify = (obj: any, prefix = ''): string[] =>
diff --git a/packages/mermaid/src/diagram-api/diagram-orchestration.spec.ts b/packages/mermaid/src/diagram-api/diagram-orchestration.spec.ts
index d6c6b30463..b0cda94848 100644
--- a/packages/mermaid/src/diagram-api/diagram-orchestration.spec.ts
+++ b/packages/mermaid/src/diagram-api/diagram-orchestration.spec.ts
@@ -31,6 +31,7 @@ describe('diagram-orchestration', () => {
       { text: 'info', expected: 'info' },
       { text: 'sequenceDiagram', expected: 'sequence' },
       { text: 'mindmap', expected: 'mindmap' },
+      { text: 'ContextMap', expected: 'contextMap' },
       { text: 'timeline', expected: 'timeline' },
       { text: 'gitGraph', expected: 'gitGraph' },
       { text: 'stateDiagram', expected: 'state' },
diff --git a/packages/mermaid/src/diagram-api/diagram-orchestration.ts b/packages/mermaid/src/diagram-api/diagram-orchestration.ts
index eb123c4a20..feeb95142a 100644
--- a/packages/mermaid/src/diagram-api/diagram-orchestration.ts
+++ b/packages/mermaid/src/diagram-api/diagram-orchestration.ts
@@ -19,6 +19,7 @@ import errorDiagram from '../diagrams/error/errorDiagram.js';
 import flowchartElk from '../diagrams/flowchart/elk/detector.js';
 import timeline from '../diagrams/timeline/detector.js';
 import mindmap from '../diagrams/mindmap/detector.js';
+import contextMap from '../diagrams/context-map/detector.js';
 import sankey from '../diagrams/sankey/sankeyDetector.js';
 import block from '../diagrams/block/blockDetector.js';
 import { registerLazyLoadedDiagrams } from './detectType.js';
@@ -81,6 +82,7 @@ export const addDiagrams = () => {
     flowchartV2,
     flowchart,
     mindmap,
+    contextMap,
     timeline,
     git,
     stateV2,
diff --git a/packages/mermaid/src/diagrams/context-map/contextMap-definition.ts b/packages/mermaid/src/diagrams/context-map/contextMap-definition.ts
new file mode 100644
index 0000000000..f6cd4b6185
--- /dev/null
+++ b/packages/mermaid/src/diagrams/context-map/contextMap-definition.ts
@@ -0,0 +1,11 @@
+// @ts-ignore: JISON doesn't support types
+import contextMapParser from './parser/contextMap.jison';
+import * as contextMapDb from './contextMapDb.js';
+import contextMapRenderer from './contextMapRenderer.js';
+
+export const diagram = {
+  db: contextMapDb,
+  renderer: contextMapRenderer,
+  parser: contextMapParser,
+  styles: undefined,
+};
diff --git a/packages/mermaid/src/diagrams/context-map/contextMap.spec.ts b/packages/mermaid/src/diagrams/context-map/contextMap.spec.ts
new file mode 100644
index 0000000000..2d5a0b51a1
--- /dev/null
+++ b/packages/mermaid/src/diagrams/context-map/contextMap.spec.ts
@@ -0,0 +1,116 @@
+import { describe, test, expect } from 'vitest';
+import type { Link, RawLink } from './contextMap.js';
+import { mapEdgeLabels } from './contextMap.js';
+
+describe('graph construction', () => {
+  test.each([
+    {
+      rawLink: {
+        source: { id: 'CargoBookingContext', type: ['SK'] },
+        target: {
+          id: 'VoyagePlanningContextVoyagePlanningContextVoyagePlanningContext',
+          type: ['SK'],
+        },
+        arrow: ['left', 'right'],
+      },
+      link: {
+        source: { id: 'CargoBookingContext', boxText: undefined, bodyText: undefined },
+        target: {
+          id: 'VoyagePlanningContextVoyagePlanningContextVoyagePlanningContext',
+          boxText: undefined,
+          bodyText: undefined,
+        },
+        middleText: 'Shared Kernel',
+      },
+    },
+    {
+      rawLink: {
+        source: { id: 'CustomerSelfServiceContext', type: ['D', 'C'] },
+        target: { id: 'CustomerManagementContext', type: ['U', 'S'] },
+        arrow: ['right'],
+      },
+      link: {
+        source: { id: 'CustomerSelfServiceContext', boxText: 'D', bodyText: undefined },
+        target: { id: 'CustomerManagementContext', boxText: 'U', bodyText: undefined },
+        middleText: 'Customer/Supplier',
+      },
+    },
+    {
+      rawLink: {
+        source: { id: 'CustomerManagementContext', type: ['D', 'ACL'] },
+        target: { id: 'PrintingContext', type: ['U', 'OHS', 'PL'] },
+        arrow: ['right'],
+      },
+      link: {
+        source: { id: 'CustomerManagementContext', boxText: 'D', bodyText: 'ACL' },
+        target: { id: 'PrintingContext', boxText: 'U', bodyText: 'OHS, PL' },
+        middleText: undefined,
+      },
+    },
+    {
+      rawLink: {
+        source: { id: 'PrintingContext', type: ['U', 'OHS', 'PL'] },
+        target: { id: 'PolicyManagementContext', type: ['D', 'ACL'] },
+        arrow: ['right'],
+      },
+      link: {
+        source: { id: 'PrintingContext', boxText: 'U', bodyText: 'OHS, PL' },
+        target: { id: 'PolicyManagementContext', boxText: 'D', bodyText: 'ACL' },
+        middleText: undefined,
+      },
+    },
+    {
+      rawLink: {
+        source: { id: 'RiskManagementContext', type: ['P'] },
+        target: { id: 'PolicyManagementContext', type: ['P'] },
+        arrow: ['left', 'right'],
+      },
+      link: {
+        source: { id: 'RiskManagementContext', boxText: undefined, bodyText: undefined },
+        target: { id: 'PolicyManagementContext', boxText: undefined, bodyText: undefined },
+        middleText: 'Partnership',
+      },
+    },
+    {
+      rawLink: {
+        source: { id: 'PolicyManagementContext', type: ['D', 'CF'] },
+        target: { id: 'CustomerManagementContext', type: ['U', 'OHS', 'PL'] },
+        arrow: ['right'],
+      },
+      link: {
+        source: { id: 'PolicyManagementContext', boxText: 'D', bodyText: 'CF' },
+        target: { id: 'CustomerManagementContext', boxText: 'U', bodyText: 'OHS, PL' },
+        middleText: undefined,
+      },
+    },
+    {
+      rawLink: {
+        source: { id: 'DebtCollection', type: ['D', 'ACL'] },
+        target: { id: 'PrintingContext', type: ['U', 'OHS', 'PL'] },
+        arrow: ['right'],
+      },
+      link: {
+        source: { id: 'DebtCollection', boxText: 'D', bodyText: 'ACL' },
+        target: { id: 'PrintingContext', boxText: 'U', bodyText: 'OHS, PL' },
+        middleText: undefined,
+      },
+    },
+    {
+      rawLink: {
+        source: { id: 'CustomersBackofficeTeam', type: ['U', 'S'] },
+        target: { id: 'CustomersFrontofficeTeam', type: ['D', 'C'] },
+        arrow: ['right'],
+      },
+      link: {
+        source: { id: 'CustomersBackofficeTeam', boxText: 'U', bodyText: undefined },
+        target: { id: 'CustomersFrontofficeTeam', boxText: 'D', bodyText: undefined },
+        middleText: 'Customer/Supplier',
+      },
+    },
+  ] as { rawLink: RawLink; link: Link }[])(
+    'map labels for source: $rawLink.source.type, and target: $rawLink.target.type',
+    ({ rawLink, link }: { rawLink: RawLink; link: Link }) => {
+      expect(mapEdgeLabels(rawLink)).toStrictEqual(link);
+    }
+  );
+});
diff --git a/packages/mermaid/src/diagrams/context-map/contextMap.ts b/packages/mermaid/src/diagrams/context-map/contextMap.ts
new file mode 100644
index 0000000000..3ab3198c53
--- /dev/null
+++ b/packages/mermaid/src/diagrams/context-map/contextMap.ts
@@ -0,0 +1,85 @@
+export const boxLabels = ['D', 'U'] as const;
+export const bodyLabels = ['CF', 'ACL', 'OHS', 'PL', 'SK', 'C', 'S', 'P'] as const;
+export const middleLabels = ['Shared Kernel', 'Partnership', 'Customer/Supplier'] as const;
+export const middleLabelsRelations: Partial<Record<BodyLabel, MiddleLabel>> = {
+  SK: 'Shared Kernel',
+  P: 'Partnership',
+  C: 'Customer/Supplier',
+  S: 'Customer/Supplier',
+};
+export type BoxLabel = (typeof boxLabels)[number];
+export type BodyLabel = (typeof bodyLabels)[number];
+export type MiddleLabel = (typeof middleLabels)[number];
+
+type Arrow = 'left' | 'right';
+type RawLabel = BoxLabel | BodyLabel;
+export type RawLink = {
+  source: { id: string; type: RawLabel[] };
+  target: { id: string; type: RawLabel[] };
+  arrow: Arrow[];
+};
+export type Link = {
+  source: { id: string; boxText?: string; bodyText?: string };
+  target: { id: string; boxText?: string; bodyText?: string };
+  middleText?: string;
+};
+
+export function mapEdgeLabels(rawLink: RawLink): Link {
+  let middleText: MiddleLabel | undefined = undefined;
+  let boxTarget: BoxLabel | undefined = undefined;
+  let boxSource: BoxLabel | undefined = undefined;
+  let bodyTarget: BodyLabel | undefined = undefined;
+  let bodySource: BodyLabel | undefined = undefined;
+  for (const bodyLabel of bodyLabels) {
+    if (
+      rawLink.source.type.includes(bodyLabel) &&
+      rawLink.target.type.includes(bodyLabel) &&
+      !middleText
+    ) {
+      middleText = middleLabelsRelations[bodyLabel];
+    }
+  }
+  if (
+    ((rawLink.source.type.includes('C') && rawLink.target.type.includes('S')) ||
+      (rawLink.source.type.includes('S') && rawLink.target.type.includes('C'))) &&
+    !middleText
+  ) {
+    middleText = 'Customer/Supplier';
+  }
+  for (const boxLabel of boxLabels) {
+    if (rawLink.source.type.includes(boxLabel)) {
+      boxSource = boxLabel;
+    }
+    if (rawLink.target.type.includes(boxLabel)) {
+      boxTarget = boxLabel;
+    }
+  }
+
+  for (const bodyLabel of bodyLabels) {
+    if (Object.keys(middleLabelsRelations).includes(bodyLabel)) {
+      break;
+    }
+
+    if (rawLink.source.type.includes(bodyLabel)) {
+      if (!bodySource) {
+        bodySource = bodyLabel;
+      } else {
+        bodySource += ', ' + bodyLabel;
+      }
+    }
+
+    if (rawLink.target.type.includes(bodyLabel)) {
+      if (!bodyTarget) {
+        bodyTarget = bodyLabel;
+      } else {
+        bodyTarget += ', ' + bodyLabel;
+      }
+    }
+  }
+
+  return {
+    source: { id: rawLink.source.id, boxText: boxSource, bodyText: bodySource },
+    target: { id: rawLink.target.id, boxText: boxTarget, bodyText: bodyTarget },
+    middleText: middleText,
+  };
+}
diff --git a/packages/mermaid/src/diagrams/context-map/contextMapDb.js b/packages/mermaid/src/diagrams/context-map/contextMapDb.js
new file mode 100644
index 0000000000..546fc07b21
--- /dev/null
+++ b/packages/mermaid/src/diagrams/context-map/contextMapDb.js
@@ -0,0 +1,47 @@
+let contextMap = undefined;
+let nodes = [];
+let edges = [];
+
+/**
+ *
+ * @param name
+ */
+export function setContextMapName(name) {
+  contextMap = name;
+}
+/**
+ *
+ * @param name
+ */
+export function addNode(name) {
+  nodes.push({ id: name });
+}
+/**
+ *
+ * @param obj
+ */
+export function addEdge(obj) {
+  edges.push(obj);
+}
+/**
+ *
+ */
+export function getGraph() {
+  return { contextMap, nodes, edges };
+}
+/**
+ *
+ */
+export function clear() {
+  nodes = [];
+  edges = [];
+  contextMap = undefined;
+}
+
+export default {
+  setContextMapName,
+  addNode,
+  addEdge,
+  getGraph,
+  clear,
+};
diff --git a/packages/mermaid/src/diagrams/context-map/contextMapRenderer.js b/packages/mermaid/src/diagrams/context-map/contextMapRenderer.js
new file mode 100644
index 0000000000..8ab911fcc1
--- /dev/null
+++ b/packages/mermaid/src/diagrams/context-map/contextMapRenderer.js
@@ -0,0 +1,57 @@
+import * as d3 from 'd3';
+import { log } from '../../logger.js';
+import { getConfig } from '../../diagram-api/diagramAPI.js';
+import * as db from './contextMapDb.js';
+import { configureSvgSize } from '../../setupGraphViewbox.js';
+import { buildGraph, Configuration } from './drawSvg.js';
+import { mapEdgeLabels } from './contextMap.js';
+import { calculateTextHeight, calculateTextWidth } from '../../utils.js';
+
+export const draw = async (text, id, version, diagObj) => {
+  const conf = getConfig().contextMap;
+
+  log.debug('things', conf.font);
+  log.debug('Rendering cml\n' + text, diagObj.parser);
+
+  const securityLevel = getConfig().securityLevel;
+  // Handle root and Document for when rendering in sandbox mode
+  let sandboxElement;
+  if (securityLevel === 'sandbox') {
+    sandboxElement = d3.select('#i' + id);
+  }
+  const root =
+    securityLevel === 'sandbox'
+      ? d3.select(sandboxElement.nodes()[0].contentDocument.body)
+      : d3.select('body');
+  // Parse the graph definition
+
+  var svg = root.select('#' + id);
+
+  const graph = db.getGraph();
+
+  const nodes = graph.nodes.map((node) => ({ id: node.id, name: node.id }));
+  const links = graph.edges.map((edge) => {
+    return mapEdgeLabels(edge);
+  });
+
+  const width = conf.width;
+  const height = conf.height;
+  const fontConfig = conf.font;
+  const config = new Configuration(
+    height,
+    width,
+    fontConfig,
+    (text) => calculateTextWidth(text, fontConfig),
+    (text) => calculateTextHeight(text, fontConfig),
+    { rx: conf.nodePadding.horizontal, ry: conf.nodePadding.vertical },
+    { horizontal: conf.nodeMargin.horizontal, vertical: conf.nodeMargin.vertical }
+  );
+
+  buildGraph(svg, { nodes, links }, config);
+
+  configureSvgSize(svg, width, height, true);
+};
+
+export default {
+  draw,
+};
diff --git a/packages/mermaid/src/diagrams/context-map/detector.ts b/packages/mermaid/src/diagrams/context-map/detector.ts
new file mode 100644
index 0000000000..edfc5289e4
--- /dev/null
+++ b/packages/mermaid/src/diagrams/context-map/detector.ts
@@ -0,0 +1,23 @@
+import type {
+  DiagramDetector,
+  DiagramLoader,
+  ExternalDiagramDefinition,
+} from '../../diagram-api/types.js';
+const id = 'contextMap';
+
+const detector: DiagramDetector = (txt) => {
+  return /^\s*ContextMap/.test(txt);
+};
+
+const loader: DiagramLoader = async () => {
+  const { diagram } = await import('./contextMap-definition.js');
+  return { id, diagram };
+};
+
+const plugin: ExternalDiagramDefinition = {
+  id,
+  detector,
+  loader,
+};
+
+export default plugin;
diff --git a/packages/mermaid/src/diagrams/context-map/drawSvg.spec.ts b/packages/mermaid/src/diagrams/context-map/drawSvg.spec.ts
new file mode 100644
index 0000000000..0518f03fb9
--- /dev/null
+++ b/packages/mermaid/src/diagrams/context-map/drawSvg.spec.ts
@@ -0,0 +1,263 @@
+import * as d3 from 'd3';
+
+import { Configuration, ContextMap, ContextMapLink, ContextMapNode } from './drawSvg.js';
+import { describe, test, expect } from 'vitest';
+
+describe('graph construction', () => {
+  const fakeFont = { fontSize: 0, fontFamily: 'any', fontWeight: 0 };
+
+  test('svg size', () => {
+    const svg = d3.create('svg');
+    const config = new Configuration(
+      500,
+      500,
+      fakeFont,
+      () => 0,
+      () => 0,
+      { rx: 0, ry: 0 },
+      { horizontal: 0, vertical: 0 }
+    );
+    const contextMap = new ContextMap(config);
+
+    contextMap.create(svg, [], []);
+
+    expect(svg.attr('viewBox')).toBe('-250,-250,500,500');
+  });
+
+  test('node size', () => {
+    const svg = d3.create('svg');
+    const nodeId = 'CustomerSelfServiceContext';
+    const nodes = [{ id: nodeId }];
+    const calculateTextWidth = (text?: string): number => text?.length ?? 0;
+    const textHeight = 15;
+    const fontSize = 10;
+    const fontFamily = 'arial';
+    const fontWeight = 8;
+    const ellipseSize = { rx: 50, ry: 10 };
+    const config = new Configuration(
+      500,
+      500,
+      { fontSize, fontFamily, fontWeight },
+      calculateTextWidth,
+      (_) => textHeight,
+      ellipseSize,
+      { horizontal: 0, vertical: 0 }
+    );
+
+    const contextMap = new ContextMap(config);
+
+    contextMap.create(svg, [], nodes);
+
+    const d3Nodes = svg.selectAll('g').nodes();
+    expect(d3.select(d3Nodes[0]).attr('transform')).toBe('translate(0,0)');
+
+    const ellipses = svg.selectAll('ellipse').nodes();
+    expect(d3.select(ellipses[0]).attr('rx')).toBe(
+      ((ellipseSize.rx + calculateTextWidth(nodeId)) / 2).toString()
+    );
+    expect(d3.select(ellipses[0]).attr('ry')).toBe(((ellipseSize.ry + textHeight) / 2).toString());
+
+    const texts = svg.selectAll('text').nodes();
+    expect(d3.select(texts[0]).text()).toBe('CustomerSelfServiceContext');
+    expect(d3.select(texts[0]).attr('font-size')).toBe(fontSize.toString());
+    expect(d3.select(texts[0]).attr('font-weight')).toBe(fontWeight.toString());
+    expect(d3.select(texts[0]).attr('font-family')).toBe(fontFamily);
+    expect(d3.select(texts[0]).attr('x')).toBe((-calculateTextWidth(nodeId) / 2).toString());
+    expect(d3.select(texts[0]).attr('y')).toBe((textHeight / 4).toString());
+  });
+
+  // const textWidth = configuration.calculateTextWidth(node.id)
+  // const textHeight = configuration.calculateTextHeight(node.id)
+  // const width =  configuration.ellipseSize.rx+textWidth
+  // const height =  configuration.ellipseSize.ry+textHeight
+  // const textX = -(textWidth/2)
+  // const textY = textHeight/4
+
+  test('distribute nodes in the plane', () => {
+    const svg = d3.create('svg');
+    const nodes = [
+      { id: 'CustomerSelfServiceContext' },
+      { id: 'CustomerSelfServiceContext' },
+      { id: 'CustomerManagementContext' },
+      { id: 'PrintingContext' },
+    ];
+
+    const config = new Configuration(
+      500,
+      500,
+      fakeFont,
+      (text?: string): number => 0,
+      (_) => 15,
+      { rx: 50, ry: 10 },
+      { horizontal: 0, vertical: 0 }
+    );
+
+    const contextMap = new ContextMap(config);
+
+    contextMap.create(svg, [], nodes);
+
+    const d3Nodes = svg.selectAll('g').nodes();
+    const [topLeftNodeX, topLeftNodeY] = pickD3TransformTranslatePos(d3Nodes[0]);
+    const [topRightNodeX, topRightNodeY] = pickD3TransformTranslatePos(d3Nodes[1]);
+    const [botLeftNodeX, botLeftNodeY] = pickD3TransformTranslatePos(d3Nodes[2]);
+    const [botRightNodeX, botRightNodeY] = pickD3TransformTranslatePos(d3Nodes[3]);
+
+    expect(topLeftNodeX + topRightNodeX).toBe(0);
+    expect(topLeftNodeY).toBe(topRightNodeY);
+    expect(botLeftNodeX + botRightNodeX).toBe(0);
+    expect(botLeftNodeY).toBe(botRightNodeY);
+  });
+
+  test('position a link in the plane', () => {
+    const svg = d3.create('svg');
+    const links = [
+      {
+        source: { id: 'CustomerSelfServiceContext', boxText: undefined, bodyText: undefined },
+        target: { id: 'PrintingContext', boxText: undefined, bodyText: undefined },
+        middleText: 'Shared Kernel',
+      },
+    ];
+    const nodes = [{ id: 'CustomerSelfServiceContext' }, { id: 'PrintingContext' }];
+
+    const config = new Configuration(
+      500,
+      500,
+      fakeFont,
+      (text?: string): number => text?.length ?? 0,
+      (_) => 15,
+      { rx: 50, ry: 10 },
+      { horizontal: 0, vertical: 0 }
+    );
+
+    const contextMap = new ContextMap(config);
+
+    contextMap.create(svg, links, nodes);
+
+    const d3Nodes = svg.selectAll('g').nodes();
+    // expect(d3.select(d3Nodes[0]).attr("transform")).toBe("translate(-57.5,0)")
+    // expect(d3.select(d3Nodes[1]).attr("transform")).toBe("translate(63,0)")
+
+    // const paths = svg.selectAll("path").nodes()
+    // expect(d3.select(paths[0]).attr("d")).toBe("M-57.5,0A0,0 0 0,1 63,0")
+  });
+
+  test('distribute 2 nodes in the plane', () => {
+    const nodes = [
+      new ContextMapNode(100, 20, 0, 0, fakeFont, 'any'),
+      new ContextMapNode(100, 20, 0, 0, fakeFont, 'any'),
+    ];
+
+    ContextMapNode.disposeNodesInThePlane(nodes, { width: 500, height: 500 });
+
+    expect(nodes[0].position).toStrictEqual({ x: -50, y: 0 });
+    expect(nodes[1].position).toStrictEqual({ x: 50, y: 0 });
+  });
+
+  test('distribute 4 nodes in the plane', () => {
+    const nodes = [
+      new ContextMapNode(100, 20, 0, 0, fakeFont, 'any'),
+      new ContextMapNode(100, 20, 0, 0, fakeFont, 'any'),
+      new ContextMapNode(100, 20, 0, 0, fakeFont, 'any'),
+      new ContextMapNode(100, 20, 0, 0, fakeFont, 'any'),
+    ];
+
+    ContextMapNode.disposeNodesInThePlane(nodes, { width: 500, height: 500 });
+
+    expect(nodes[0].position).toStrictEqual({ x: -50, y: +10 });
+
+    expect(nodes[1].position).toStrictEqual({ x: +50, y: 10 });
+
+    expect(nodes[2].position).toStrictEqual({ x: -50, y: -10 });
+
+    expect(nodes[3].position).toStrictEqual({ x: +50, y: -10 });
+  });
+
+  test('distribute 4 nodes in the plane with little width', () => {
+    const nodes = [
+      new ContextMapNode(100, 20, 0, 0, fakeFont, 'any'),
+      new ContextMapNode(100, 20, 0, 0, fakeFont, 'any'),
+      new ContextMapNode(100, 20, 0, 0, fakeFont, 'any'),
+      new ContextMapNode(100, 20, 0, 0, fakeFont, 'any'),
+    ];
+
+    ContextMapNode.disposeNodesInThePlane(nodes, { width: 120, height: 800 });
+
+    expect(nodes[0].position).toStrictEqual({ x: 0, y: 30 });
+
+    expect(nodes[1].position).toStrictEqual({ x: 0, y: 10 });
+
+    expect(nodes[2].position).toStrictEqual({ x: 0, y: -10 });
+
+    expect(nodes[3].position).toStrictEqual({ x: 0, y: -30 });
+  });
+
+  test('distribute 4 nodes in the plane considering margins', () => {
+    const nodes = [
+      new ContextMapNode(100, 20, 0, 0, fakeFont, 'any'),
+      new ContextMapNode(100, 20, 0, 0, fakeFont, 'any'),
+      new ContextMapNode(100, 20, 0, 0, fakeFont, 'any'),
+      new ContextMapNode(100, 20, 0, 0, fakeFont, 'any'),
+    ];
+
+    ContextMapNode.disposeNodesInThePlane(
+      nodes,
+      { width: 400, height: 800 },
+      { horizontal: 10, vertical: 10 }
+    );
+
+    expect(nodes[0].position).toStrictEqual({ x: -60, y: 20 });
+
+    expect(nodes[1].position).toStrictEqual({ x: +60, y: 20 });
+
+    expect(nodes[2].position).toStrictEqual({ x: -60, y: -20 });
+
+    expect(nodes[3].position).toStrictEqual({ x: +60, y: -20 });
+  });
+
+  test('crete link between two nodes', () => {
+    const config = new Configuration(
+      500,
+      500,
+      fakeFont,
+      (text?: string): number => text?.length ?? 0,
+      (_) => 15,
+      { rx: 50, ry: 10 },
+      { horizontal: 0, vertical: 0 }
+    );
+
+    const nodes = [
+      new ContextMapNode(100, 20, 0, 0, fakeFont, 'A', { x: -100, y: 0 }),
+      new ContextMapNode(100, 20, 0, 0, fakeFont, 'B', { x: 100, y: 0 }),
+      new ContextMapNode(100, 20, 0, 0, fakeFont, 'C', { x: 200, y: 200 }),
+    ];
+    const links = [
+      {
+        source: { id: 'A', boxText: undefined, bodyText: undefined },
+        target: { id: 'B', boxText: undefined, bodyText: undefined },
+        middleText: undefined,
+      },
+      {
+        source: { id: 'A', boxText: undefined, bodyText: undefined },
+        target: { id: 'C', boxText: undefined, bodyText: undefined },
+        middleText: undefined,
+      },
+    ];
+
+    const contextMapLinks = ContextMapLink.createContextMapLinksFromNodes(nodes, links, config);
+
+    expect(contextMapLinks[0].link.source.id).toBe('A');
+    expect(contextMapLinks[0].link.target.id).toBe('B');
+
+    expect(contextMapLinks[1].link.source.id).toBe('A');
+    expect(contextMapLinks[1].link.target.id).toBe('C');
+  });
+
+  function parseTranslate(translate: string): [number, number] {
+    const [_text, x, y] = translate.split(/[(),]/);
+    return [parseInt(x), parseInt(y)];
+  }
+
+  function pickD3TransformTranslatePos(d3Node: d3.BaseType): [number, number] {
+    return parseTranslate(d3.select(d3Node).attr('transform'));
+  }
+});
diff --git a/packages/mermaid/src/diagrams/context-map/drawSvg.ts b/packages/mermaid/src/diagrams/context-map/drawSvg.ts
new file mode 100644
index 0000000000..c92c12372f
--- /dev/null
+++ b/packages/mermaid/src/diagrams/context-map/drawSvg.ts
@@ -0,0 +1,426 @@
+import type * as d3 from 'd3';
+import type { Link } from './contextMap.js';
+
+export function buildGraph(
+  svg: D3Svg,
+  { nodes, links }: { nodes: Node[]; links: Link[] },
+  configuration: Configuration
+) {
+  const contextMap = new ContextMap(configuration);
+  contextMap.create(svg, links, nodes);
+
+  // show the center svg.append("circle").attr("x", 0).attr("y", 0).attr("r", 5).attr("fill", "green")
+}
+
+export type D3Svg = d3.Selection<SVGSVGElement, any, any, any>;
+export class ContextMap {
+  constructor(private configuration: Configuration) {}
+
+  create(svg: D3Svg, links: Link[], nodes: Node[]) {
+    const height = this.configuration.height;
+    const width = this.configuration.width;
+
+    svg.attr('viewBox', [-width / 2, -height / 2, width, height]);
+
+    const contextMapNodes = nodes.map((node) =>
+      ContextMapNode.createContextMapNode(node, this.configuration)
+    );
+    ContextMapNode.disposeNodesInThePlane(
+      contextMapNodes,
+      { width, height },
+      this.configuration.nodeMargin
+    );
+
+    const contextMapLinks = ContextMapLink.createContextMapLinksFromNodes(
+      contextMapNodes,
+      links,
+      this.configuration
+    );
+
+    contextMapLinks.forEach((contextMapLink) => contextMapLink.appendPathTo(svg));
+    contextMapNodes.forEach((contextMapNode) => contextMapNode.appendTo(svg));
+    contextMapLinks.forEach((contextMapLink) => contextMapLink.appendLabelsTo(svg));
+  }
+}
+
+export type Node = { id: string };
+
+export type Font = { fontFamily: string; fontSize: number; fontWeight: number };
+type EllipseSize = { rx: number; ry: number };
+type NodeMargin = { horizontal: number; vertical: number };
+export class Configuration {
+  constructor(
+    public width: number,
+    public height: number,
+    public font: Font,
+    public calculateTextWidth: (text?: string) => number,
+    public calculateTextHeight: (text?: string) => number,
+    public ellipseSize: EllipseSize,
+    public nodeMargin: NodeMargin
+  ) {}
+}
+
+type Point = { x: number; y: number };
+type LabelSize = {
+  labelCenter: Point;
+  labelPosition: Point;
+  boxWidth: number;
+  boxHeight: number;
+  boxTextPosition: Point;
+  bodyWidth: number;
+  bodyPosition: Point;
+  bodyTextPosition: Point;
+  font: Font;
+};
+export class ContextMapLink {
+  constructor(
+    public link: Link,
+    private targetLabelSize: LabelSize,
+    private sourceLabelSize: LabelSize,
+    private middleLabelSize: { font: Font },
+    public targetPoint: Point,
+    public sourcePoint: Point
+  ) {}
+
+  static createContextMapLinksFromNodes(
+    nodes: ContextMapNode[],
+    links: Link[],
+    config: Configuration
+  ): ContextMapLink[] {
+    const nodeMap = nodes.reduce((map, node) => {
+      map.set(node.id, node);
+      return map;
+    }, new Map<string, ContextMapNode>());
+
+    const contextMapLinks: ContextMapLink[] = [];
+    for (const link of links) {
+      const sourceNode = nodeMap.get(link.source.id);
+      const targetNode = nodeMap.get(link.target.id);
+      if (sourceNode && targetNode) {
+        contextMapLinks.push(
+          ContextMapLink.createContextMapLink(sourceNode, targetNode, link, config)
+        );
+      }
+    }
+    return contextMapLinks;
+  }
+
+  static createContextMapLink(
+    sourceNode: ContextMapNode,
+    targetNode: ContextMapNode,
+    link: Link,
+    config: Configuration
+  ) {
+    const sourceLabelIntersection = targetNode.calculateIntersection(sourceNode.position);
+    const targetLabelSize: LabelSize = ContextMapLink.calculateLabelSize(
+      config,
+      link.target.boxText,
+      link.target.bodyText,
+      sourceLabelIntersection
+    );
+
+    const targetLabelIntersection = sourceNode.calculateIntersection(targetNode.position);
+    const sourceLabelSize: LabelSize = ContextMapLink.calculateLabelSize(
+      config,
+      link.source.boxText,
+      link.source.bodyText,
+      targetLabelIntersection
+    );
+
+    const contextMapLink = new ContextMapLink(
+      link,
+      targetLabelSize,
+      sourceLabelSize,
+      { font: config.font },
+      targetNode.position,
+      sourceNode.position
+    );
+    return contextMapLink;
+  }
+
+  appendLabelsTo(svg: D3Svg) {
+    this.appendLabel(
+      svg,
+      this.targetLabelSize,
+      this.link.target.boxText,
+      this.link.target.bodyText
+    );
+    this.appendLabel(
+      svg,
+      this.sourceLabelSize,
+      this.link.source.boxText,
+      this.link.source.bodyText
+    );
+    this.appendMiddleLabel(svg);
+  }
+
+  appendPathTo(svg: D3Svg) {
+    this.appendPath(svg);
+  }
+
+  private static calculateLabelSize(
+    config: Configuration,
+    boxText: string | undefined,
+    bodyText: string | undefined,
+    labelIntersection: Point
+  ) {
+    const boxTextWidth = config.calculateTextWidth(boxText);
+    const bodyTextWidth = config.calculateTextWidth(bodyText);
+    const boxHeight = Math.max(
+      config.calculateTextHeight(boxText),
+      config.calculateTextHeight(bodyText)
+    );
+    const targetWidth = boxTextWidth + bodyTextWidth;
+    const targetLabelSize: LabelSize = {
+      labelCenter: labelIntersection,
+      labelPosition: {
+        x: labelIntersection.x - targetWidth / 2,
+        y: labelIntersection.y - boxHeight / 2,
+      },
+      boxWidth: targetWidth,
+      boxHeight: boxHeight,
+      boxTextPosition: { x: 1, y: boxHeight - 3 },
+      bodyWidth: bodyTextWidth + 2,
+      bodyPosition: { x: boxTextWidth, y: 0 },
+      bodyTextPosition: { x: boxTextWidth + 1, y: boxHeight - 3 },
+      font: config.font,
+    };
+    return targetLabelSize;
+  }
+
+  private appendPath(svg: D3Svg) {
+    const sourceLabelPos = this.sourceLabelSize.labelCenter;
+    const targetLabelPos = this.targetLabelSize.labelCenter;
+
+    svg
+      .append('path')
+      .attr('stroke', 'black')
+      .attr('stroke-width', 0.5)
+      .attr(
+        'd',
+        `M${sourceLabelPos.x},${sourceLabelPos.y}A0,0 0 0,1 ${targetLabelPos.x},${targetLabelPos.y}`
+      );
+  }
+
+  private appendMiddleLabel(svg: D3Svg) {
+    const calculateMidPoint = ([x1, y1]: [number, number], [x2, y2]: [number, number]) => [
+      (x1 + x2) / 2,
+      (y1 + y2) / 2,
+    ];
+
+    const midPoint = calculateMidPoint(
+      [this.sourcePoint.x, this.sourcePoint.y],
+      [this.targetPoint.x, this.targetPoint.y]
+    );
+
+    const middleLabel = svg.append('g');
+    middleLabel
+      .append('text')
+      .attr('font-size', this.middleLabelSize.font.fontSize)
+      .attr('font-family', this.middleLabelSize.font.fontFamily)
+      .attr('font-weight', this.middleLabelSize.font.fontWeight)
+      .text(this.link.middleText ?? '')
+      .attr('x', midPoint[0])
+      .attr('y', midPoint[1]);
+  }
+
+  private appendLabel(
+    svg: D3Svg,
+    {
+      boxWidth,
+      bodyWidth,
+      boxHeight,
+      font,
+      labelPosition,
+      boxTextPosition,
+      bodyPosition,
+      bodyTextPosition,
+    }: LabelSize,
+    boxText?: string,
+    bodyText?: string
+  ) {
+    const label = svg
+      .append('g')
+      .attr('transform', `translate(${labelPosition.x},${labelPosition.y})`);
+
+    label
+      .append('rect')
+      .attr('height', boxHeight)
+      .attr('width', boxWidth)
+      .attr('fill', 'white')
+      .attr('x', 0)
+      .attr('y', 0)
+      .attr('display', boxText?.length ?? 0 ? null : 'none');
+
+    label
+      .append('text')
+      .attr('font-size', font.fontSize)
+      .attr('font-family', font.fontFamily)
+      .attr('font-weight', font.fontWeight)
+      .attr('x', boxTextPosition.x)
+      .attr('y', boxTextPosition.y)
+      .text(boxText ?? '');
+
+    label
+      .append('rect')
+      .attr('width', bodyWidth)
+      .attr('height', boxHeight)
+      .attr('stroke-width', 1)
+      .attr('stroke', 'black')
+      .attr('fill', 'white')
+      .attr('x', bodyPosition.x)
+      .attr('y', bodyPosition.y)
+      .attr('display', bodyText?.length ?? 0 ? null : 'none');
+
+    label
+      .append('text')
+      .attr('font-size', font.fontSize)
+      .attr('font-family', font.fontFamily)
+      .attr('font-weight', font.fontWeight)
+      .attr('x', bodyTextPosition.x)
+      .attr('y', bodyTextPosition.y)
+      .text(bodyText ?? '');
+  }
+}
+
+export class ContextMapNode {
+  private rx: number;
+  private ry: number;
+  constructor(
+    public width: number,
+    public height: number,
+
+    private textWidth: number,
+    private textHeight: number,
+    private font: Font,
+
+    public id: string,
+
+    public textPosition: Point = { x: 0, y: 0 },
+    public position: Point = { x: 0, y: 0 }
+  ) {
+    this.rx = width / 2;
+    this.ry = height / 2;
+  }
+
+  static disposeNodesInThePlane(
+    nodes: ContextMapNode[],
+    boxSize: { width: number; height: number },
+    margin = { horizontal: 0, vertical: 0 }
+  ) {
+    const center = { x: 0, y: 0 };
+    const nodeNumber = nodes.length;
+
+    const proposedColumns = Math.ceil(Math.sqrt(nodeNumber));
+
+    const perRowTotalWidth: number[] = [];
+    const perRowTotalHeight: number[] = [];
+    let totalHeight: number = 0;
+
+    let counter = 0;
+    let row = 0;
+    while (counter < nodes.length) {
+      let maxRowHeight = 0;
+      for (let column = 0; column < proposedColumns; column++) {
+        const node = nodes?.[counter];
+        if (!node) {
+          break;
+        }
+        const largerWidth = (perRowTotalWidth[row] ?? 0) + node.width + margin.horizontal * 2;
+        if (largerWidth < boxSize.width) {
+          perRowTotalWidth[row] = largerWidth;
+          if (node.height > maxRowHeight) {
+            maxRowHeight = node.height + margin.vertical * 2;
+          }
+          counter++;
+        }
+      }
+      perRowTotalHeight[row] = (perRowTotalHeight?.[row] ?? 0) + maxRowHeight;
+      totalHeight += maxRowHeight;
+      row++;
+    }
+
+    row = 0;
+    let inCurrentRowUsedWidth = 0;
+    let inCurrentRowWidthStartingPoint = center.x - perRowTotalWidth[row] / 2;
+    let heightStartingPoint = center.y + totalHeight / 2;
+
+    for (const node of nodes) {
+      if (perRowTotalWidth[row] <= inCurrentRowUsedWidth) {
+        row++;
+        inCurrentRowUsedWidth = 0;
+        inCurrentRowWidthStartingPoint = center.x - perRowTotalWidth[row] / 2;
+        heightStartingPoint -= perRowTotalHeight[row];
+      }
+
+      const width = node.width + margin.horizontal * 2;
+      const x = inCurrentRowWidthStartingPoint + width / 2;
+      const y = heightStartingPoint - perRowTotalHeight[row] / 2;
+      inCurrentRowWidthStartingPoint += width;
+      inCurrentRowUsedWidth += width;
+
+      node.setPosition({ x, y });
+    }
+
+    return nodes;
+  }
+
+  static createContextMapNode(node: Node, configuration: Configuration) {
+    const textWidth = configuration.calculateTextWidth(node.id);
+    const textHeight = configuration.calculateTextHeight(node.id);
+    const width = configuration.ellipseSize.rx + textWidth;
+    const height = configuration.ellipseSize.ry + textHeight;
+    const textX = -(textWidth / 2);
+    const textY = textHeight / 4;
+    return new ContextMapNode(width, height, textWidth, textHeight, configuration.font, node.id, {
+      x: textX,
+      y: textY,
+    });
+  }
+
+  calculateIntersection(
+    { x: x2, y: y2 }: Point,
+    { x: centerX, y: centerY }: Point = { x: this.position.x, y: this.position.y },
+    { rx: a, ry: b }: EllipseSize = { rx: this.rx, ry: this.ry }
+  ) {
+    const deltaX = x2 - centerX;
+    const deltaY = y2 - centerY;
+    const angle = Math.atan((deltaY / deltaX) * (a / b));
+    let x: number, y: number;
+    if (deltaX >= 0) {
+      x = centerX + a * Math.cos(angle);
+      y = centerY + b * Math.sin(angle);
+    } else {
+      x = centerX - a * Math.cos(angle);
+      y = centerY - b * Math.sin(angle);
+    }
+    return { x: x, y: y };
+  }
+
+  setPosition(position: Point) {
+    this.position = position;
+  }
+
+  appendTo(svg: D3Svg) {
+    const node = svg
+      .append('g')
+      .attr('transform', `translate(${this.position.x},${this.position.y})`);
+
+    node
+      .append('ellipse')
+      .attr('stroke', 'black')
+      .attr('stroke-width', 1.5)
+      .attr('rx', this.rx)
+      .attr('ry', this.ry)
+      .attr('fill', 'white');
+
+    node
+      .append('text')
+      .attr('font-size', this.font.fontSize)
+      .attr('font-family', this.font.fontFamily)
+      .attr('font-weight', this.font.fontWeight)
+      .attr('x', this.textPosition.x)
+      .attr('y', this.textPosition.y)
+      .text(this.id);
+  }
+}
diff --git a/packages/mermaid/src/diagrams/context-map/parser/contextMap.jison b/packages/mermaid/src/diagrams/context-map/parser/contextMap.jison
new file mode 100644
index 0000000000..9d7db9a653
--- /dev/null
+++ b/packages/mermaid/src/diagrams/context-map/parser/contextMap.jison
@@ -0,0 +1,74 @@
+
+%{
+  function edgeType(text){
+    return text.includes(",") ? text.split(",") : [text]
+  }  
+%}
+
+%lex
+
+%x comment
+%s context
+
+%%
+
+\s+                                { /* skip */ }
+"/*"                               { this.pushState("comment"); }
+<comment>"*/"                      { this.popState(); }
+<comment>[^"*"]|[^"/"]             { /* skip */ }
+
+"ContextMap"                       { this.pushState("context"); return "ContextMap"; }
+<context>"contains"                { return "contains"; }
+
+(\w|",")+                          { return "WORD"; }
+
+<context>"{"                       { return "{"; }
+<context>"}"                       { this.popState(); return "}"; }
+<context>"["                       { return "["; }
+<context>"]"                       { return "]"; }
+<context>"<->"                     { return "<->"; }
+<context>"<-"                      { return "<-"; }
+<context>"->"                      { return "->"; }
+
+<<EOF>>	                           { return "EOF"; }
+
+/lex
+
+
+%start e
+
+%% 
+
+arrow 
+  : "<-"   {$$ = ["left"]}
+  | "->"   {$$ = ["right"]}
+  | "<->"  {$$ = ["left", "right"]}
+  ;    
+
+edge 
+  : "WORD" "[" "WORD" "]" arrow "[" "WORD" "]" "WORD" { yy.addEdge({  source: { id: $1, type: edgeType($3) }, target: { id: $9, type: edgeType($7) }, arrow: $5 })  }
+  | "WORD" arrow "WORD" { yy.addEdge({  source: { id: $1, type: [] }, target: { id: $3, type: [] }, arrow: $2 })  }
+  ;
+
+node 
+  :  "contains" "WORD" { yy.addNode($2) }
+  ;
+
+contextMap 
+  : "ContextMap" "WORD" { yy.setContextMapName($2) }
+  ;
+
+w 
+  : contextMap
+  | node
+  | edge
+  | "{"
+  | "}" 
+  ;
+
+e 
+  : w
+  | e w 
+  | EOF { return yy.getGraph() }   
+  | e EOF { return yy.getGraph() }   
+  ;
diff --git a/packages/mermaid/src/diagrams/context-map/parser/contextMap.spec.js b/packages/mermaid/src/diagrams/context-map/parser/contextMap.spec.js
new file mode 100644
index 0000000000..43256165f0
--- /dev/null
+++ b/packages/mermaid/src/diagrams/context-map/parser/contextMap.spec.js
@@ -0,0 +1,287 @@
+import parser from './contextMap.jison';
+import contextMapDb from '../contextMapDb.js';
+
+describe('check context map syntax', function () {
+  beforeEach(() => {
+    parser.parser.yy = contextMapDb;
+    parser.parser.yy.clear();
+  });
+
+  it('comments are ignored', function () {
+    const grammar = `
+	/* Note that the splitting of the LocationContext is not mentioned in the original DDD sample of Evans.
+	 * However, locations and the management around them, can somehow be seen as a separated concept which is used by other
+	 * bounded contexts. But this is just an example, since we want to demonstrate our DSL with multiple bounded contexts.
+	 */
+`;
+    const result = parser.parser.parse(grammar);
+    expect(result).toEqual({ contextMap: undefined, nodes: [], edges: [] });
+  });
+
+  it('recognize empty contextMap block', function () {
+    const grammar = `
+ContextMap DDDSampleMap {
+      
+}
+`;
+    const result = parser.parser.parse(grammar);
+    expect(result).toEqual({ contextMap: 'DDDSampleMap', nodes: [], edges: [] });
+  });
+
+  it('recognize contains as nodes', function () {
+    const grammar = `
+ContextMap DDDSampleMap {
+  contains CargoBookingContext
+	contains VoyagePlanningContext
+	contains LocationContext
+}
+`;
+    const result = parser.parser.parse(grammar);
+    expect(result).toEqual({
+      contextMap: 'DDDSampleMap',
+      nodes: [
+        { id: 'CargoBookingContext' },
+        { id: 'VoyagePlanningContext' },
+        { id: 'LocationContext' },
+      ],
+      edges: [],
+    });
+  });
+
+  it('recognize simple edges', function () {
+    const grammar = `
+ContextMap DDDSampleMap {
+    contains CargoBookingContext
+    contains VoyagePlanningContext
+
+    CargoBookingContext <-> VoyagePlanningContext
+    CargoBookingContext <- VoyagePlanningContext 
+    CargoBookingContext -> VoyagePlanningContext
+}
+`;
+    const result = parser.parser.parse(grammar);
+    expect(result).toEqual({
+      contextMap: 'DDDSampleMap',
+      nodes: [{ id: 'CargoBookingContext' }, { id: 'VoyagePlanningContext' }],
+      edges: [
+        {
+          source: { id: 'CargoBookingContext', type: [] },
+          target: { id: 'VoyagePlanningContext', type: [] },
+          arrow: ['left', 'right'],
+        },
+        {
+          source: { id: 'CargoBookingContext', type: [] },
+          target: { id: 'VoyagePlanningContext', type: [] },
+          arrow: ['left'],
+        },
+        {
+          source: { id: 'CargoBookingContext', type: [] },
+          target: { id: 'VoyagePlanningContext', type: [] },
+          arrow: ['right'],
+        },
+      ],
+    });
+  });
+
+  it('recognize complex edge', function () {
+    const grammar = `
+ContextMap DDDSampleMap {
+  contains CargoBookingContext
+	contains VoyagePlanningContext
+	contains LocationContext
+
+  CargoBookingContext [SK]<->[SK] VoyagePlanningContext
+}
+`;
+    const result = parser.parser.parse(grammar);
+    expect(result).toEqual({
+      contextMap: 'DDDSampleMap',
+      nodes: [
+        { id: 'CargoBookingContext' },
+        { id: 'VoyagePlanningContext' },
+        { id: 'LocationContext' },
+      ],
+      edges: [
+        {
+          source: { id: 'CargoBookingContext', type: ['SK'] },
+          target: { id: 'VoyagePlanningContext', type: ['SK'] },
+          arrow: ['left', 'right'],
+        },
+      ],
+    });
+  });
+
+  it('recognize mutiple edges and multiple types', function () {
+    const grammar = `
+ContextMap DDDSampleMap {
+  contains CargoBookingContext
+	contains VoyagePlanningContext
+	contains LocationContext
+
+	CargoBookingContext [SK]<->[SK] VoyagePlanningContext
+	CargoBookingContext [D]<-[U,OHS,PL] LocationContext
+	VoyagePlanningContext [D]<-[U,OHS,PL] LocationContext
+}
+`;
+    const result = parser.parser.parse(grammar);
+    expect(result).toEqual({
+      contextMap: 'DDDSampleMap',
+      nodes: [
+        { id: 'CargoBookingContext' },
+        { id: 'VoyagePlanningContext' },
+        { id: 'LocationContext' },
+      ],
+      edges: [
+        {
+          source: { id: 'CargoBookingContext', type: ['SK'] },
+          target: { id: 'VoyagePlanningContext', type: ['SK'] },
+          arrow: ['left', 'right'],
+        },
+        {
+          source: { id: 'CargoBookingContext', type: ['D'] },
+          target: { id: 'LocationContext', type: ['U', 'OHS', 'PL'] },
+          arrow: ['left'],
+        },
+        {
+          source: { id: 'VoyagePlanningContext', type: ['D'] },
+          target: { id: 'LocationContext', type: ['U', 'OHS', 'PL'] },
+          arrow: ['left'],
+        },
+      ],
+    });
+  });
+
+  it('recognize edges and nodes with comments', function () {
+    const grammar = `
+/* The DDD Cargo sample application modeled in CML. Note that we split the application into multiple bounded contexts. */
+ContextMap DDDSampleMap {
+	contains CargoBookingContext
+	contains VoyagePlanningContext
+	contains LocationContext
+	
+	/* As Evans mentions in his book (Bounded Context chapter): The voyage planning can be seen as 
+	 * separated bounded context. However, it still shares code with the booking application (CargoBookingContext).
+	 * Thus, they are in a 'Shared-Kernel' relationship.
+	 */
+	CargoBookingContext [SK]<->[SK] VoyagePlanningContext
+	
+	/* Note that the splitting of the LocationContext is not mentioned in the original DDD sample of Evans.
+	 * However, locations and the management around them, can somehow be seen as a separated concept which is used by other
+	 * bounded contexts. But this is just an example, since we want to demonstrate our DSL with multiple bounded contexts.
+	 */
+	CargoBookingContext [D]<-[U,OHS,PL] LocationContext
+	
+	VoyagePlanningContext [D]<-[U,OHS,PL] LocationContext
+	
+}
+`;
+    const result = parser.parser.parse(grammar);
+    expect(result).toEqual({
+      contextMap: 'DDDSampleMap',
+      nodes: [
+        { id: 'CargoBookingContext' },
+        { id: 'VoyagePlanningContext' },
+        { id: 'LocationContext' },
+      ],
+      edges: [
+        {
+          source: { id: 'CargoBookingContext', type: ['SK'] },
+          target: { id: 'VoyagePlanningContext', type: ['SK'] },
+          arrow: ['left', 'right'],
+        },
+        {
+          source: { id: 'CargoBookingContext', type: ['D'] },
+          target: { id: 'LocationContext', type: ['U', 'OHS', 'PL'] },
+          arrow: ['left'],
+        },
+        {
+          source: { id: 'VoyagePlanningContext', type: ['D'] },
+          target: { id: 'LocationContext', type: ['U', 'OHS', 'PL'] },
+          arrow: ['left'],
+        },
+      ],
+    });
+  });
+
+  it('recognize edges and nodes of another example', function () {
+    const grammar = `
+/* Example Context Map written with 'ContextMapper DSL' */
+ContextMap InsuranceContextMap {
+	
+	/* Add bounded contexts to this context map: */
+	contains CustomerManagementContext
+	contains CustomerSelfServiceContext
+	contains PrintingContext
+	contains PolicyManagementContext
+	contains RiskManagementContext
+	contains DebtCollection
+	
+	/* Define the context relationships: */ 
+
+	CustomerSelfServiceContext [D,C]<-[U,S] CustomerManagementContext
+	
+	CustomerManagementContext [D,ACL]<-[U,OHS,PL] PrintingContext
+	
+	PrintingContext [U,OHS,PL]->[D,ACL] PolicyManagementContext
+	
+	RiskManagementContext [P]<->[P] PolicyManagementContext
+
+	PolicyManagementContext [D,CF]<-[U,OHS,PL] CustomerManagementContext
+
+	DebtCollection [D,ACL]<-[U,OHS,PL] PrintingContext
+
+	PolicyManagementContext [SK]<->[SK] DebtCollection	
+}
+
+`;
+    const result = parser.parser.parse(grammar);
+    expect(result).toEqual({
+      contextMap: 'InsuranceContextMap',
+      nodes: [
+        { id: 'CustomerManagementContext' },
+        { id: 'CustomerSelfServiceContext' },
+        { id: 'PrintingContext' },
+        { id: 'PolicyManagementContext' },
+        { id: 'RiskManagementContext' },
+        { id: 'DebtCollection' },
+      ],
+      edges: [
+        {
+          source: { id: 'CustomerSelfServiceContext', type: ['D', 'C'] },
+          target: { id: 'CustomerManagementContext', type: ['U', 'S'] },
+          arrow: ['left'],
+        },
+        {
+          source: { id: 'CustomerManagementContext', type: ['D', 'ACL'] },
+          target: { id: 'PrintingContext', type: ['U', 'OHS', 'PL'] },
+          arrow: ['left'],
+        },
+        {
+          source: { id: 'PrintingContext', type: ['U', 'OHS', 'PL'] },
+          target: { id: 'PolicyManagementContext', type: ['D', 'ACL'] },
+          arrow: ['right'],
+        },
+        {
+          source: { id: 'RiskManagementContext', type: ['P'] },
+          target: { id: 'PolicyManagementContext', type: ['P'] },
+          arrow: ['left', 'right'],
+        },
+        {
+          source: { id: 'PolicyManagementContext', type: ['D', 'CF'] },
+          target: { id: 'CustomerManagementContext', type: ['U', 'OHS', 'PL'] },
+          arrow: ['left'],
+        },
+        {
+          source: { id: 'DebtCollection', type: ['D', 'ACL'] },
+          target: { id: 'PrintingContext', type: ['U', 'OHS', 'PL'] },
+          arrow: ['left'],
+        },
+        {
+          source: { id: 'PolicyManagementContext', type: ['SK'] },
+          target: { id: 'DebtCollection', type: ['SK'] },
+          arrow: ['left', 'right'],
+        },
+      ],
+    });
+  });
+});
diff --git a/packages/mermaid/src/diagrams/mindmap/parser/mindmap.jison b/packages/mermaid/src/diagrams/mindmap/parser/mindmap.jison
index afd5e2300d..fee52f0913 100644
--- a/packages/mermaid/src/diagrams/mindmap/parser/mindmap.jison
+++ b/packages/mermaid/src/diagrams/mindmap/parser/mindmap.jison
@@ -10,6 +10,7 @@
 %{
 	// Pre-lexer code can go here
 %}
+
 %x NODE
 %x NSTR
 %x NSTR2
diff --git a/packages/mermaid/src/schemas/config.schema.yaml b/packages/mermaid/src/schemas/config.schema.yaml
index 3e7fd58ec5..8f1ffac572 100644
--- a/packages/mermaid/src/schemas/config.schema.yaml
+++ b/packages/mermaid/src/schemas/config.schema.yaml
@@ -46,6 +46,7 @@ required:
   - xyChart
   - requirement
   - mindmap
+  - contextMap
   - gitGraph
   - c4
   - sankey
@@ -219,6 +220,8 @@ properties:
     $ref: '#/$defs/RequirementDiagramConfig'
   mindmap:
     $ref: '#/$defs/MindmapDiagramConfig'
+  contextMap:
+    $ref: '#/$defs/MiniContextMapLanguageDiagramConfig'
   gitGraph:
     $ref: '#/$defs/GitGraphDiagramConfig'
   c4:
@@ -871,6 +874,69 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
         type: number
         default: 200
 
+  MiniContextMapLanguageDiagramConfig:
+    title: Mini Context Map Language Diagram Config
+    allOf: [{ $ref: '#/$defs/BaseDiagramConfig' }]
+    description: The object containing configurations specific for mini context map language diagrams
+    type: object
+    unevaluatedProperties: false
+    required:
+      - height
+      - width
+      - useMaxWidth
+      - nodeMargin
+      - nodePadding
+      - font
+    properties:
+      width:
+        type: number
+        default: 600 
+      height:
+        type: number
+        default: 600 
+      nodeMargin:  
+        title: Context Map Node Margin
+        description: margins of nodes
+        type: object
+        unevaluatedProperties: false
+        properties:
+          horizontal:
+            type: number
+          vertical:  
+            type: number
+        default: 
+          horizontal: 20
+          vertical: 30
+      nodePadding:  
+        title: Context Map Node Padding
+        description: padding of nodes
+        type: object
+        unevaluatedProperties: false
+        properties:
+          horizontal:
+            type: number
+          vertical:  
+            type: number
+        default:
+          horizontal: 100
+          vertical: 40
+      font:
+        title: Context Map Font
+        description: Font of all Context Map texts
+        type: object
+        unevaluatedProperties: false
+        properties:
+          fontFamily:
+            type: string
+          fontSize: 
+            type: number
+          fontWeight: 
+            type: number
+        default:
+          fontFamily: "Arial"
+          fontSize: 12  
+          fontWeight: 400 
+ 
   PieDiagramConfig:
     title: Pie Diagram Config
     allOf: [{ $ref: '#/$defs/BaseDiagramConfig' }]