diff --git a/Directory.Packages.props b/Directory.Packages.props index a00ac8f6d..5ba306174 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -82,6 +82,7 @@ + diff --git a/src/Elastic.Documentation.Site/Assets/markdown/mermaid.css b/src/Elastic.Documentation.Site/Assets/markdown/mermaid.css index a76a0dd77..068f64786 100644 --- a/src/Elastic.Documentation.Site/Assets/markdown/mermaid.css +++ b/src/Elastic.Documentation.Site/Assets/markdown/mermaid.css @@ -197,6 +197,16 @@ height: 1.125rem; } +/* Error fallback for diagrams that failed to render server-side */ +.mermaid-error { + padding: 1rem; + border: 1px solid #e5e5e5; + border-radius: 0.5rem; + background-color: #fafafa; + color: #69707d; + font-size: 0.875rem; +} + /* Hide controls when printing */ @media print { .mermaid-controls { diff --git a/src/Elastic.Documentation.Site/Assets/mermaid.ts b/src/Elastic.Documentation.Site/Assets/mermaid.ts index 7bc5f1edf..37f13bce9 100644 --- a/src/Elastic.Documentation.Site/Assets/mermaid.ts +++ b/src/Elastic.Documentation.Site/Assets/mermaid.ts @@ -1,57 +1,3 @@ -// Beautiful Mermaid is loaded from local _static/ to avoid client-side CDN calls -// The file is copied from node_modules during build (see package.json copy:mermaid) - -// Type declaration for beautiful-mermaid browser global -declare global { - interface Window { - __mermaid: { - renderMermaid: ( - code: string, - options?: { - bg?: string - fg?: string - font?: string - transparent?: boolean - line?: string - accent?: string - muted?: string - surface?: string - border?: string - } - ) => Promise - } - } -} - -let mermaidLoaded = false -let mermaidLoading: Promise | null = null - -// High-contrast theme configuration -// beautiful-mermaid generates CSS vars that don't resolve correctly in all contexts, -// so we resolve them to actual colors during post-processing -const colors = { - background: '#FFFFFF', - foreground: '#000000', - nodeFill: '#F5F5F5', - nodeStroke: '#000000', - line: '#000000', - innerStroke: '#333333', -} - -// Map CSS variables to resolved colors -const variableReplacements: Record = { - '--_text': colors.foreground, - '--_text-sec': colors.foreground, - '--_text-muted': colors.foreground, - '--_text-faint': colors.foreground, // "+ ", ": ", "(no attributes)" - '--_line': colors.line, - '--_arrow': colors.foreground, - '--_node-fill': colors.nodeFill, - '--_node-stroke': colors.nodeStroke, - '--_inner-stroke': colors.innerStroke, - '--bg': colors.background, -} - // Zoom configuration const ZOOM_MIN = 0.5 const ZOOM_MAX = 3 @@ -84,74 +30,6 @@ interface DiagramState { startY: number } -/** - * Resolve CSS variables to actual colors in the SVG output - */ -function resolveVariables(svg: string): string { - let result = svg - for (const [variable, color] of Object.entries(variableReplacements)) { - const pattern = new RegExp( - `(fill|stroke)="var\\(${variable.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\)"`, - 'g' - ) - result = result.replace(pattern, `$1="${color}"`) - } - return result -} - -/** - * Remove Google Fonts @import to avoid external network dependency - */ -function removeGoogleFonts(svg: string): string { - return svg.replace( - /@import url\('https:\/\/fonts\.googleapis\.com[^']*'\);\s*/g, - '' - ) -} - -/** - * Get the base path for _static/ assets by finding main.js script location - */ -function getStaticBasePath(): string { - // Find the main.js script element to get the correct path prefix - const scripts = document.querySelectorAll('script[src*="main.js"]') - for (const script of scripts) { - const src = script.getAttribute('src') - if (src) { - // Extract path up to and including _static/ - const match = src.match(/^(.*\/_static\/)/) - if (match) { - return match[1] - } - } - } - // Fallback for local development - return '/_static/' -} - -/** - * Lazy-load Beautiful Mermaid from local _static/ only when diagrams exist on the page - */ -async function loadMermaid(): Promise { - 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(" Html.Should().Contain("-->"); + public void ContainsNodeLabels() + { + Html.Should().Contain("Start"); + Html.Should().Contain("Process"); + Html.Should().Contain("End"); + } } public class MermaidSequenceTests(ITestOutputHelper output) : DirectiveTest(output, @@ -54,10 +59,17 @@ participant B as Bob public void ParsesSequenceDiagram() => Block.Should().NotBeNull(); [Fact] - public void RendersPreMermaidTag() => Html.Should().Contain("
");
+	public void RendersMermaidContainer() => Html.Should().Contain("
"); [Fact] - public void PreservesIndentation() => Html.Should().Contain("participant A as Alice"); + public void RendersInlineSvg() => Html.Should().Contain(" Block.Should().NotBeNull(); [Fact] - public void RendersPreMermaidTag() => Html.Should().Contain("
");
+	public void RendersMermaidContainer() => Html.Should().Contain("
"); + + [Fact] + public void RendersInlineSvg() => Html.Should().Contain(" Html.Should().Contain("[*]"); + public void ContainsStateLabels() + { + Html.Should().Contain("Idle"); + Html.Should().Contain("Processing"); + Html.Should().Contain("Complete"); + } } public class MermaidClassDiagramTests(ITestOutputHelper output) : DirectiveTest(output, @@ -101,10 +121,18 @@ public class MermaidClassDiagramTests(ITestOutputHelper output) : DirectiveTest( public void ParsesClassDiagram() => Block.Should().NotBeNull(); [Fact] - public void RendersPreMermaidTag() => Html.Should().Contain("
");
+	public void RendersMermaidContainer() => Html.Should().Contain("
"); + + [Fact] + public void RendersInlineSvg() => Html.Should().Contain(" Html.Should().Contain("<|--"); + public void ContainsClassLabels() + { + Html.Should().Contain("Animal"); + Html.Should().Contain("Duck"); + Html.Should().Contain("Fish"); + } } public class MermaidErDiagramTests(ITestOutputHelper output) : DirectiveTest(output, @@ -123,8 +151,35 @@ public class MermaidErDiagramTests(ITestOutputHelper output) : DirectiveTest(out public void ParsesErDiagram() => Block.Should().NotBeNull(); [Fact] - public void RendersPreMermaidTag() => Html.Should().Contain("
");
+	public void RendersMermaidContainer() => Html.Should().Contain("
"); + + [Fact] + public void RendersInlineSvg() => Html.Should().Contain(" B[Process] +classDef elasticBlue fill:#0B64DD,stroke:#333,stroke-width:2px,color:#fff +class A elasticBlue +style B fill:#0A52B3,color:#fff +``` +""" +) +{ + [Fact] + public void RendersInlineSvg() => Html.Should().Contain(" Html.Should().Contain("||--o{"); + public void EmitsNoDiagnostics() => Collector.Diagnostics.Should().BeEmpty(); }