{
- if (mermaidLoaded) return
- if (mermaidLoading) return mermaidLoading
-
- mermaidLoading = new Promise((resolve, reject) => {
- const script = document.createElement('script')
- script.src = getStaticBasePath() + 'mermaid.min.js'
- script.async = true
- script.onload = () => {
- mermaidLoaded = true
- resolve()
- }
- script.onerror = () =>
- reject(new Error('Failed to load Beautiful Mermaid'))
- document.head.appendChild(script)
- })
-
- return mermaidLoading
-}
-
/**
* Create a control button with icon and tooltip
*/
@@ -506,65 +384,28 @@ function openFullscreenModal(svgContent: string): void {
}
/**
- * Initialize Mermaid diagram rendering for elements with class 'mermaid'
+ * Initialize interactive controls for server-rendered Mermaid diagrams.
+ * SVGs are already rendered inline by the C# pipeline via Mermaider.
*/
-export async function initMermaid() {
- const mermaidElements = document.querySelectorAll(
- 'pre.mermaid:not([data-mermaid-processed])'
+export function initMermaid() {
+ const containers = document.querySelectorAll(
+ '.mermaid-container:not([data-mermaid-initialized])'
)
- if (mermaidElements.length === 0) {
+ if (containers.length === 0) {
return
}
- try {
- // Lazy-load Beautiful Mermaid only when diagrams exist
- await loadMermaid()
-
- // Render each diagram individually
- for (let i = 0; i < mermaidElements.length; i++) {
- const element = mermaidElements[i]
- const content = element.textContent?.trim()
-
- if (!content) continue
-
- // Mark as processed to prevent double rendering
- element.setAttribute('data-mermaid-processed', 'true')
-
- try {
- // Render the diagram using Beautiful Mermaid
- let svg = await window.__mermaid.renderMermaid(content)
+ for (const container of containers) {
+ const el = container as HTMLElement
+ el.setAttribute('data-mermaid-initialized', 'true')
- // Post-process the SVG
- svg = resolveVariables(svg)
- svg = removeGoogleFonts(svg)
+ const viewport = el.querySelector('.mermaid-viewport') as HTMLElement
+ const rendered = el.querySelector('.mermaid-rendered') as HTMLElement
- // Create container structure with controls
- const container = document.createElement('div')
- container.className = 'mermaid-container'
+ if (!viewport || !rendered) continue
- const viewport = document.createElement('div')
- viewport.className = 'mermaid-viewport'
-
- const rendered = document.createElement('div')
- rendered.className = 'mermaid-rendered'
- rendered.innerHTML = svg
-
- viewport.appendChild(rendered)
- container.appendChild(viewport)
-
- // Set up interactive controls
- setupControls(container, viewport, rendered, svg)
-
- // Replace the pre element with the new container
- element.replaceWith(container)
- } catch (err) {
- console.warn('Mermaid rendering error for diagram:', err)
- // Keep the original content as fallback
- element.classList.add('mermaid-error')
- }
- }
- } catch (error) {
- console.warn('Mermaid initialization error:', error)
+ const svgContent = rendered.innerHTML
+ setupControls(el, viewport, rendered, svgContent)
}
}
diff --git a/src/Elastic.Documentation.Site/package-lock.json b/src/Elastic.Documentation.Site/package-lock.json
index 4b2f71b02..7139dc63c 100644
--- a/src/Elastic.Documentation.Site/package-lock.json
+++ b/src/Elastic.Documentation.Site/package-lock.json
@@ -28,7 +28,6 @@
"@opentelemetry/semantic-conventions": "^1.40.0",
"@r2wc/react-to-web-component": "2.1.1",
"@tanstack/react-query": "^5.100.9",
- "@theletterf/beautiful-mermaid": "0.1.5",
"@uidotdev/usehooks": "2.4.1",
"dompurify": "^3.4.2",
"highlight.js": "11.11.1",
@@ -156,6 +155,7 @@
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -2038,6 +2038,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=18"
},
@@ -2061,28 +2062,11 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=18"
}
},
- "node_modules/@dagrejs/dagre": {
- "version": "1.1.8",
- "resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-1.1.8.tgz",
- "integrity": "sha512-5SEDlndt4W/LaVzPYJW+bSmSEZc9EzTf8rJ20WCKvjS5EAZAN0b+x0Yww7VMT4R3Wootkg+X9bUfUxazYw6Blw==",
- "license": "MIT",
- "dependencies": {
- "@dagrejs/graphlib": "2.2.4"
- }
- },
- "node_modules/@dagrejs/graphlib": {
- "version": "2.2.4",
- "resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-2.2.4.tgz",
- "integrity": "sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==",
- "license": "MIT",
- "engines": {
- "node": ">17.0.0"
- }
- },
"node_modules/@elastic/datemath": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@elastic/datemath/-/datemath-5.0.3.tgz",
@@ -2393,6 +2377,7 @@
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.13.5.tgz",
"integrity": "sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w==",
+ "peer": true,
"dependencies": {
"@emotion/babel-plugin": "^11.13.5",
"@emotion/cache": "^11.13.5",
@@ -2415,6 +2400,7 @@
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -4683,6 +4669,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz",
"integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==",
"license": "Apache-2.0",
+ "peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -6514,6 +6501,7 @@
"integrity": "sha512-a0CgrW5A5kwuSu5J1RFRoMQaMs9yagvfH2jJMYVw56+/7NRI4KOtu612SG9Y1ERWfY55ZwzyFxtLWvD6LO+Anw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@mischnic/json-sourcemap": "^0.1.1",
"@parcel/cache": "2.16.4",
@@ -24301,15 +24289,6 @@
"@testing-library/dom": ">=7.21.4"
}
},
- "node_modules/@theletterf/beautiful-mermaid": {
- "version": "0.1.5",
- "resolved": "https://registry.npmjs.org/@theletterf/beautiful-mermaid/-/beautiful-mermaid-0.1.5.tgz",
- "integrity": "sha512-46MIkRlUYrAJbGDB7zbUkK9D/oOPQg5XKs9Xq3dn61O1MTqXU1aX0FPhc2ZOK2DNETXHKcFEo0f5MKyMPCoSyg==",
- "license": "MIT",
- "dependencies": {
- "@dagrejs/dagre": "^1.1.8"
- }
- },
"node_modules/@trivago/prettier-plugin-sort-imports": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-6.0.2.tgz",
@@ -24413,8 +24392,7 @@
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
- "dev": true,
- "peer": true
+ "dev": true
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -24641,6 +24619,7 @@
"version": "18.3.23",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz",
"integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
+ "peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -24771,6 +24750,7 @@
"integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.59.3",
"@typescript-eslint/types": "8.59.3",
@@ -25313,6 +25293,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -26018,6 +25999,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -26723,8 +26705,7 @@
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
- "dev": true,
- "peer": true
+ "dev": true
},
"node_modules/dom-serializer": {
"version": "2.0.0",
@@ -27076,6 +27057,7 @@
"integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.2",
@@ -29944,6 +29926,7 @@
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
@@ -30521,7 +30504,6 @@
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
- "peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -30767,6 +30749,7 @@
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
+ "peer": true,
"engines": {
"node": "*"
}
@@ -31760,6 +31743,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -31807,6 +31791,7 @@
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -31901,7 +31886,6 @@
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
- "peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -31916,7 +31900,6 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=10"
},
@@ -32037,6 +32020,7 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -32059,6 +32043,7 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -32397,6 +32382,7 @@
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.9.2"
}
@@ -33389,6 +33375,7 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -33535,8 +33522,7 @@
"node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
- "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
- "peer": true
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/type-check": {
"version": "0.4.0",
@@ -33583,6 +33569,7 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
diff --git a/src/Elastic.Documentation.Site/package.json b/src/Elastic.Documentation.Site/package.json
index a1a421ea6..a358e8bf7 100644
--- a/src/Elastic.Documentation.Site/package.json
+++ b/src/Elastic.Documentation.Site/package.json
@@ -6,8 +6,7 @@
"source": "Assets/index.js",
"scripts": {
"watch": "parcel watch",
- "build": "parcel build && npm run copy:mermaid",
- "copy:mermaid": "cp node_modules/@theletterf/beautiful-mermaid/dist/beautiful-mermaid.browser.global.js _static/mermaid.min.js",
+ "build": "parcel build",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
@@ -119,7 +118,6 @@
"@r2wc/react-to-web-component": "2.1.1",
"@tanstack/react-query": "^5.100.9",
"@uidotdev/usehooks": "2.4.1",
- "@theletterf/beautiful-mermaid": "0.1.5",
"dompurify": "3.4.2",
"highlight.js": "11.11.1",
"htmx-ext-head-support": "2.0.5",
diff --git a/src/Elastic.Markdown/Elastic.Markdown.csproj b/src/Elastic.Markdown/Elastic.Markdown.csproj
index bdfe215ef..cef312ce6 100644
--- a/src/Elastic.Markdown/Elastic.Markdown.csproj
+++ b/src/Elastic.Markdown/Elastic.Markdown.csproj
@@ -20,6 +20,7 @@
+
diff --git a/src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlockHtmlRenderer.cs b/src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlockHtmlRenderer.cs
index 1af15968e..e45062b5a 100644
--- a/src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlockHtmlRenderer.cs
+++ b/src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlockHtmlRenderer.cs
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information
using System.Diagnostics.CodeAnalysis;
+using System.Text;
using Elastic.Documentation.AppliesTo;
using Elastic.Markdown.Diagnostics;
using Elastic.Markdown.Helpers;
@@ -14,6 +15,8 @@
using Markdig.Renderers;
using Markdig.Renderers.Html;
using Markdig.Syntax;
+using Mermaider;
+using Mermaider.Models;
using Microsoft.AspNetCore.Html;
using RazorSlices;
@@ -134,7 +137,6 @@ protected override void Write(HtmlRenderer renderer, EnhancedCodeBlock block)
return;
}
- // Render Mermaid diagrams as pre.mermaid for client-side rendering
if (block.Language == "mermaid")
{
RenderMermaidBlock(renderer, block);
@@ -310,32 +312,61 @@ private static void RenderContributorsHtml(HtmlRenderer renderer, ContributorsBl
RenderRazorSlice(slice, renderer);
}
- ///
- /// Renders a Mermaid code block as a pre.mermaid element for client-side rendering by Mermaid.js.
- ///
+ private static readonly RenderOptions MermaidRenderOptions = new()
+ {
+ Bg = "#FFFFFF",
+ Fg = "#000000"
+ };
+
+ /// Renders a Mermaid code block as an inline SVG using Mermaider.
private static void RenderMermaidBlock(HtmlRenderer renderer, EnhancedCodeBlock block)
{
- _ = renderer.Write("");
+ var mermaidText = ExtractMermaidText(block);
+
+ string svg;
+ try
+ {
+ svg = MermaidRenderer.RenderSvg(mermaidText, MermaidRenderOptions);
+ // Keep Mermaid style directives compatible with existing docs while stripping unsafe SVG output.
+ svg = SvgSanitizer.Sanitize(svg).Svg;
+ }
+ catch (SystemException e)
+ {
+ block.EmitWarning($"Failed to render Mermaid diagram: {e.Message}");
+ _ = renderer.Write("");
+ _ = renderer.WriteEscape(mermaidText);
+ _ = renderer.Write("
");
+ return;
+ }
+ _ = renderer.Write("");
+ _ = renderer.Write("
");
+ _ = renderer.Write("
");
+ _ = renderer.Write(svg);
+ _ = renderer.Write("
");
+ }
+
+ private static string ExtractMermaidText(EnhancedCodeBlock block)
+ {
var commonIndent = GetCommonIndent(block);
+ var sb = new StringBuilder();
+
for (var i = 0; i < block.Lines.Count; i++)
{
var line = block.Lines.Lines[i];
var slice = line.Slice;
- // Skip empty lines at beginning and end
if ((i == 0 || i == block.Lines.Count - 1) && slice.IsEmptyOrWhitespace())
continue;
- // Remove common indentation
var indent = CountIndentation(slice);
if (indent >= commonIndent)
slice.Start += commonIndent;
- _ = renderer.WriteEscape(slice);
- _ = renderer.WriteLine();
+ _ = sb.Append(slice.Text, slice.Start, slice.Length);
+ _ = sb.AppendLine();
}
- _ = renderer.Write("");
+ return sb.ToString().TrimEnd();
}
}
diff --git a/tests/Elastic.Markdown.Tests/Directives/MermaidTests.cs b/tests/Elastic.Markdown.Tests/Directives/MermaidTests.cs
index 61450ee38..a81bc4d01 100644
--- a/tests/Elastic.Markdown.Tests/Directives/MermaidTests.cs
+++ b/tests/Elastic.Markdown.Tests/Directives/MermaidTests.cs
@@ -27,13 +27,18 @@ flowchart LR
public void HasMermaidLanguage() => Block!.Language.Should().Be("mermaid");
[Fact]
- public void RendersPreMermaidTag() => Html.Should().Contain("");
+ public void RendersMermaidContainer() => Html.Should().Contain("");
[Fact]
- public void ContainsDiagramContent() => Html.Should().Contain("flowchart LR");
+ public void RendersInlineSvg() => Html.Should().Contain("