From bc1b55e60a3fc60918948029e98bc2a770156e87 Mon Sep 17 00:00:00 2001 From: Andrew Doering Date: Tue, 17 Feb 2026 17:55:03 +0100 Subject: [PATCH] fix: improve Mermaid diagram implementation (#1214) * fix: improve Mermaid diagram implementation (#1213) This commit addresses multiple issues with Mermaid diagram rendering and adds significant improvements to the user experience. - Fix `
` tags rendering as literal text instead of line breaks - Fix flash of raw mermaid code before diagram renders - Fix Gantt chart rendering issues in dark mode - Fix diagrams briefly showing during theme switch re-render - Fix expand modal showing diagram left-aligned before centering - Add fullscreen expand modal with pan/zoom functionality - Add toolbar with zoom controls (zoom in/out, reset, fit to screen) - Add keyboard support (Escape to close modal) - Add per-diagram transparent background via `%%transparent%%` directive - Add comprehensive configuration options: - `look`: classic or handDrawn (sketch style) - `lightTheme`/`darkTheme`: theme selection per mode - `lightThemeVariables`/`darkThemeVariables`: custom theme colors - `securityLevel`, `htmlLabels`, `maxTextSize`, `maxEdges` - `fontSize`, `fontFamily`, `curve`, `logLevel` - Switch from ESM to UMD bundle for faster initial load (~1s vs 2s+) - Lazy-load panzoom library only when expand modal is opened - Pre-render alternate theme during idle time for instant theme switching - Cache rendered diagrams to avoid re-rendering on theme toggle - Move all inline CSS to article.scss using SCSS nesting - Extract helper functions for better maintainability - Use data attributes and event delegation for cleaner handlers - Reduce mermaid.html from 411 to 177 lines (57% reduction) - Add comprehensive mermaid-diagrams example post with all diagram types - Document all configuration options in hugo.yaml * refactor: extract mermaid inline JS to TypeScript module Move ~155 lines of inline JavaScript from mermaid.html partial into assets/ts/mermaid.ts. The partial shrinks from 179 to 36 lines and now only contains modal HTML markup and Hugo Pipes build/import logic. - Deduplicate initWithTheme() and renderOffscreen() helpers - Enable minification via Hugo Pipes js.Build in production - Remove user-configurable mermaid version from params.toml --------- Co-authored-by: delize <4028612+delize@users.noreply.github.com> --- assets/scss/partials/article.scss | 164 +++++ assets/ts/mermaid.ts | 235 +++++++ config/_default/params.toml | 23 + demo/content/post/mermaid-diagrams/index.md | 603 ++++++++++++++++++ layouts/_markup/render-codeblock-mermaid.html | 6 +- .../_partials/article/components/mermaid.html | 47 +- 6 files changed, 1068 insertions(+), 10 deletions(-) create mode 100644 assets/ts/mermaid.ts create mode 100644 demo/content/post/mermaid-diagrams/index.md diff --git a/assets/scss/partials/article.scss b/assets/scss/partials/article.scss index 42afe01..0286dc4 100644 --- a/assets/scss/partials/article.scss +++ b/assets/scss/partials/article.scss @@ -266,3 +266,167 @@ } } } + +.mermaid:not([data-processed]) { + background: var(--card-background); + min-height: 150px; + border-radius: 8px; + position: relative; + overflow: hidden; + font-size: 0 !important; + line-height: 0; + color: transparent; + + &::after { + visibility: visible; + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: 28px; + height: 28px; + margin: -14px 0 0 -14px; + border: 3px solid var(--card-background); + border-top-color: var(--accent-color, #3273dc); + border-radius: 50%; + animation: mermaid-spinner 0.8s linear infinite; + } +} + +@keyframes mermaid-spinner { + to { + transform: rotate(360deg); + } +} + +// Mermaid diagram wrapper with expand toolbar +.mermaid-wrapper { + position: relative; + margin: 1rem 0; + + .mermaid { + display: flex; + justify-content: center; + + svg { + display: block; + max-width: 100%; + height: auto; + } + } +} + +.mermaid-toolbar { + position: absolute; + top: 0.75rem; + right: 0.75rem; + display: flex; + gap: 0.5rem; + z-index: 10; + + button { + padding: 0.5rem 1rem; + background: rgba(255, 255, 255, 0.95); + border: 2px solid #333; + border-radius: 6px; + cursor: pointer; + font-size: 1rem; + font-weight: 600; + color: #333; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + transition: all 0.2s; + + &:hover { + background: #333; + color: #fff; + transform: scale(1.05); + } + } +} + +// Mermaid fullscreen modal +.mermaid-modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.95); + z-index: 9999; + + &.active { + display: flex; + flex-direction: column; + } +} + +.mermaid-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 1rem; + background: rgba(255, 255, 255, 0.1); +} + +.mermaid-modal-controls { + display: flex; + gap: 0.5rem; +} + +.mermaid-modal-controls button, +.mermaid-modal-close { + padding: 0.5rem 1rem; + background: #fff; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + + &:hover { + background: #ddd; + } +} + +.mermaid-modal-body { + flex: 1; + overflow: hidden; + position: relative; +} + +.mermaid-modal-content { + position: absolute; + top: 1rem; + left: 1rem; + right: 1rem; + bottom: 1rem; + border-radius: 8px; + overflow: hidden; + background: #fff; + + [data-scheme="dark"] & { + background: #1e1e1e; + } +} + +.mermaid-panzoom-container { + display: inline-block; + transform-origin: 0 0; + visibility: hidden; + + &.ready { + visibility: visible; + } + + svg { + display: block; + } +} + +// Off-screen container for pre-rendering alternate theme +.mermaid-offscreen { + position: absolute; + left: -9999px; + visibility: hidden; + width: 800px; +} diff --git a/assets/ts/mermaid.ts b/assets/ts/mermaid.ts new file mode 100644 index 0000000..f05dd36 --- /dev/null +++ b/assets/ts/mermaid.ts @@ -0,0 +1,235 @@ +declare const mermaid: { + initialize(config: Record): void; + run(options: { nodes: HTMLElement[] }): Promise; +}; + +interface MermaidConfig { + transparentBackground?: boolean; + lightTheme?: string; + darkTheme?: string; + lightThemeVariables?: Record; + darkThemeVariables?: Record; + securityLevel?: string; + look?: string; + htmlLabels?: boolean; + maxTextSize?: number; + maxEdges?: number; + fontSize?: number; + fontFamily?: string; + curve?: string; + logLevel?: number; +} + +type Scheme = 'light' | 'dark'; + +const PANZOOM_CDN = 'https://cdn.jsdelivr.net/npm/panzoom@9.4.3/+esm'; + +function getScheme(): Scheme { + return document.documentElement.dataset.scheme === 'dark' ? 'dark' : 'light'; +} + +function buildThemeConfig(cfg: MermaidConfig, scheme: Scheme) { + const isLight = scheme === 'light'; + const theme = isLight ? (cfg.lightTheme ?? 'default') : (cfg.darkTheme ?? 'dark'); + const vars = isLight ? (cfg.lightThemeVariables ?? {}) : (cfg.darkThemeVariables ?? {}); + return { + theme, + themeVariables: { ...vars, ...(cfg.transparentBackground ? { background: 'transparent' } : {}) }, + }; +} + +function buildBaseConfig(cfg: MermaidConfig): Record { + const base: Record = { + startOnLoad: false, + securityLevel: cfg.securityLevel ?? 'strict', + look: cfg.look ?? 'classic', + flowchart: { htmlLabels: cfg.htmlLabels ?? true, useMaxWidth: true }, + gantt: { useWidth: 800 }, + }; + const optional: (keyof MermaidConfig)[] = ['maxTextSize', 'maxEdges', 'fontSize', 'fontFamily', 'curve', 'logLevel']; + for (const key of optional) { + if (cfg[key] != null) base[key] = cfg[key]; + } + return base; +} + +function initWithTheme( + scheme: Scheme, + themes: Record>, + baseConfig: Record, +) { + const { theme, themeVariables } = themes[scheme]; + mermaid.initialize({ + ...baseConfig, + theme, + ...(Object.keys(themeVariables).length && { themeVariables }), + }); +} + +async function renderOffscreen(sources: string[]): Promise { + const container = document.createElement('div'); + container.className = 'mermaid-offscreen'; + document.body.appendChild(container); + const nodes = sources.map(src => { + const n = document.createElement('pre'); + n.innerHTML = src; + container.appendChild(n); + return n; + }); + await mermaid.run({ nodes }); + const results = nodes.map(n => n.innerHTML); + container.remove(); + return results; +} + +function setupWrappers(elements: NodeListOf) { + elements.forEach((el, idx) => { + const wrapper = document.createElement('div'); + wrapper.className = 'mermaid-wrapper'; + el.parentNode!.insertBefore(wrapper, el); + wrapper.appendChild(el); + wrapper.insertAdjacentHTML( + 'beforeend', + `
`, + ); + }); +} + +function setupModal(elements: NodeListOf) { + const modal = document.getElementById('mermaid-modal')!; + const modalBody = document.getElementById('mermaid-modal-body')!; + const modalContent = document.getElementById('mermaid-modal-content')!; + let pzInstance: any = null; + let panzoom: any = null; + + const loadPanzoom = async () => { + if (!panzoom) { + const url = PANZOOM_CDN; + panzoom = (await import(url)).default; + } + return panzoom; + }; + + const fitToScreen = () => { + const wrapper = modalContent.querySelector('.mermaid-panzoom-container') as HTMLElement | null; + if (!pzInstance || !wrapper) return; + const w = +(wrapper.dataset.nativeWidth ?? 0); + const h = +(wrapper.dataset.nativeHeight ?? 0); + const rect = modalContent.getBoundingClientRect(); + const scale = Math.min((rect.width - 60) / w, (rect.height - 60) / h); + pzInstance.zoomAbs(0, 0, scale); + pzInstance.moveTo((rect.width - w * scale) / 2, (rect.height - h * scale) / 2); + }; + + const closeModal = () => { + modal.classList.remove('active'); + document.body.style.overflow = ''; + pzInstance?.dispose(); + pzInstance = null; + modalContent.innerHTML = ''; + }; + + const openModal = async (idx: number) => { + const svg = elements[idx].querySelector('svg'); + if (!svg) return; + + const svgClone = svg.cloneNode(true) as SVGElement; + const viewBox = svg.getAttribute('viewBox'); + const [w, h] = viewBox + ? viewBox.split(/[\s,]+/).slice(2).map(Number) + : [svg.getBoundingClientRect().width || 800, svg.getBoundingClientRect().height || 600]; + svgClone.setAttribute('width', String(w)); + svgClone.setAttribute('height', String(h)); + + const wrapper = document.createElement('div'); + wrapper.className = 'mermaid-panzoom-container'; + wrapper.dataset.nativeWidth = String(w); + wrapper.dataset.nativeHeight = String(h); + wrapper.appendChild(svgClone); + + modalContent.innerHTML = ''; + modalContent.appendChild(wrapper); + modal.classList.add('active'); + document.body.style.overflow = 'hidden'; + + const pz = await loadPanzoom(); + setTimeout(() => { + pzInstance = pz(wrapper, { maxZoom: 10, minZoom: 0.05, bounds: false }); + fitToScreen(); + wrapper.classList.add('ready'); + }, 50); + }; + + // Event delegation + document.addEventListener('click', (e) => { + const target = e.target as HTMLElement; + const toolbarBtn = target.closest('.mermaid-toolbar button') as HTMLElement | null; + if (toolbarBtn) return openModal(+(toolbarBtn.dataset.idx!)); + + const zoomBtn = target.closest('.mermaid-modal-controls button') as HTMLElement | null; + if (zoomBtn && pzInstance) { + const z = zoomBtn.dataset.zoom; + const rect = modalBody.getBoundingClientRect(); + if (z === 'fit') fitToScreen(); + else if (z === '0') { pzInstance.moveTo(0, 0); pzInstance.zoomAbs(0, 0, 1); } + else pzInstance.smoothZoom(rect.width / 2, rect.height / 2, z === '1' ? 1.5 : 0.67); + } + }); + + document.getElementById('mermaid-modal-close')!.addEventListener('click', closeModal); + modalBody.addEventListener('click', (e) => { if (e.target === modalBody) closeModal(); }); + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && modal.classList.contains('active')) closeModal(); + }); +} + +export async function initMermaidPage(config: MermaidConfig) { + const elements = document.querySelectorAll('.mermaid') as NodeListOf; + if (!elements.length) return; + + const sources = Array.from(elements).map(el => el.innerHTML); + const perDiagramTransparent = sources.map(src => /%%\s*transparent\s*%%/i.test(src)); + const cache: Record = { light: [], dark: [] }; + + const themes = { + light: buildThemeConfig(config, 'light'), + dark: buildThemeConfig(config, 'dark'), + }; + const baseConfig = buildBaseConfig(config); + + const applyTransparency = (el: HTMLElement, i: number) => { + if (perDiagramTransparent[i]) el.querySelector('svg')?.style.setProperty('background', 'transparent'); + }; + + setupWrappers(elements); + setupModal(elements); + + // Initial render + const scheme = getScheme(); + initWithTheme(scheme, themes, baseConfig); + await mermaid.run({ nodes: Array.from(elements) }); + elements.forEach((el, i) => { + el.style.visibility = ''; + cache[scheme][i] = el.innerHTML; + applyTransparency(el, i); + }); + + // Pre-render alternate theme during idle time + const alt: Scheme = scheme === 'dark' ? 'light' : 'dark'; + const idle = window.requestIdleCallback ?? ((fn: IdleRequestCallback) => setTimeout(fn, 1000)); + idle(() => { + if (cache[alt].length) return; + initWithTheme(alt, themes, baseConfig); + renderOffscreen(sources).then(results => { cache[alt] = results; }); + }); + + // Swap cached diagrams on theme change + window.addEventListener('onColorSchemeChange', async () => { + const newScheme = getScheme(); + if (!cache[newScheme].length) { + initWithTheme(newScheme, themes, baseConfig); + cache[newScheme] = await renderOffscreen(sources); + } + elements.forEach((el, i) => { el.innerHTML = cache[newScheme][i]; applyTransparency(el, i); }); + }); +} diff --git a/config/_default/params.toml b/config/_default/params.toml index 1dbddfd..56ccc99 100644 --- a/config/_default/params.toml +++ b/config/_default/params.toml @@ -29,6 +29,29 @@ SortBy = "default" enabled = false default = "Licensed under CC BY-NC-SA 4.0" + # Mermaid diagram configuration + # Diagrams are only loaded when mermaid code blocks are present + # Theme auto-switches based on site light/dark mode + [article.mermaid] + # Visual style: classic or handDrawn (sketch style) + look = "classic" + # Theme for light mode: default, neutral, dark, forest, base, null + lightTheme = "default" + # Theme for dark mode: default, neutral, dark, forest, base, null + darkTheme = "dark" + # Custom theme variables for light mode (only works with lightTheme: base) + # lightThemeVariables = { primaryColor = "#ff0000" } + # Custom theme variables for dark mode (only works with darkTheme: base) + # darkThemeVariables = { primaryColor = "#00ff00" } + # Security level: strict (default), loose, antiscript, sandbox + # Set to "loose" to enable HTML labels like
+ securityLevel = "strict" + # Enable HTML labels in diagrams (requires securityLevel: loose) + htmlLabels = true + # Make diagram backgrounds transparent (default: false) + # Can also be set per-diagram with %%transparent%% directive + transparentBackground = false + [widgets] homepage = [] page = [] diff --git a/demo/content/post/mermaid-diagrams/index.md b/demo/content/post/mermaid-diagrams/index.md new file mode 100644 index 0000000..0914b6f --- /dev/null +++ b/demo/content/post/mermaid-diagrams/index.md @@ -0,0 +1,603 @@ +--- +author: Hugo Authors +title: Mermaid Diagrams +date: 2025-12-23 +description: A comprehensive guide to creating diagrams with Mermaid in Hugo +categories: + - Themes + - Syntax +tags: + - Mermaid + - Diagrams + - Markdown +--- + +This theme supports [Mermaid](https://mermaid.js.org/) diagrams directly in your Markdown content. Mermaid lets you create diagrams and visualizations using text and code. + + + +## About Mermaid.js + +This theme integrates [Mermaid.js](https://mermaid.js.org/) (v11) to render diagrams from text definitions within Markdown code blocks. Mermaid is a JavaScript-based diagramming and charting tool that uses text-based syntax inspired by Markdown. + +For complete syntax documentation, see the [Mermaid.js documentation](https://mermaid.js.org/intro/syntax-reference.html). + +## Getting Started + +To create a Mermaid diagram, simply use a fenced code block with `mermaid` as the language identifier: + +````markdown +```mermaid +graph TD + A[Start] --> B[Process] + B --> C[End] +``` +```` + +The diagram will be automatically rendered when the page loads. + +## Features + +- **Auto-detection**: Mermaid script only loads on pages that contain diagrams +- **Theme Support**: Diagrams automatically adapt to light/dark mode +- **HTML Labels**: Support for HTML content in labels (like `
` for line breaks) +- **Configurable**: Customize version, security level, and more in your site config + +## Configuration + +You can configure Mermaid in your site config: + +**hugo.yaml:** +```yaml +params: + article: + mermaid: + version: "11" # Mermaid version from CDN + look: classic # classic or handDrawn (sketch style) + lightTheme: default # Theme for light mode + darkTheme: neutral # Theme for dark mode + securityLevel: strict # strict (default), loose, antiscript, sandbox + htmlLabels: true # Enable HTML in labels +``` + +**hugo.toml:** +```toml +[params.article.mermaid] + version = "11" # Mermaid version from CDN + look = "classic" # classic or handDrawn (sketch style) + lightTheme = "default" # Theme for light mode + darkTheme = "neutral" # Theme for dark mode + securityLevel = "strict" # strict (default), loose, antiscript, sandbox + htmlLabels = true # Enable HTML in labels +``` + +### Additional Global Options + +These optional settings use Mermaid's defaults when not specified: + +**hugo.yaml:** +```yaml +params: + article: + mermaid: + maxTextSize: 50000 # Maximum text size (default: 50000) + maxEdges: 500 # Maximum edges allowed (default: 500) + fontSize: 16 # Global font size in pixels (default: 16) + fontFamily: "arial" # Global font family + curve: "basis" # Line curve: basis, cardinal, linear (default: basis) + logLevel: 5 # Debug level 0-5, 0=debug, 5=fatal (default: 5) +``` + +**hugo.toml:** +```toml +[params.article.mermaid] + maxTextSize = 50000 # Maximum text size (default: 50000) + maxEdges = 500 # Maximum edges allowed (default: 500) + fontSize = 16 # Global font size in pixels (default: 16) + fontFamily = "arial" # Global font family + curve = "basis" # Line curve: basis, cardinal, linear (default: basis) + logLevel = 5 # Debug level 0-5, 0=debug, 5=fatal (default: 5) +``` + +For diagram-specific options (like `flowchart.useMaxWidth`), use Mermaid's init directive directly in your diagram: + +````markdown +```mermaid +%%{init: {'flowchart': {'useMaxWidth': false}}}%% +flowchart LR + A --> B +``` +```` + +> **Security Note:** The default `securityLevel: strict` is recommended. Set to `loose` only if you need HTML labels like `
` in your diagrams. + +### Available Themes + +| Theme | Description | +|-------|-------------| +| `default` | Standard colorful theme | +| `neutral` | Grayscale, great for printing and dark mode | +| `dark` | Designed for dark backgrounds | +| `forest` | Green color palette | +| `base` | Minimal theme, customizable with themeVariables | +| `null` | Disable theming entirely | + +### Custom Theme Variables + +For full control, use the `base` theme with custom variables: + +**hugo.yaml:** +```yaml +params: + article: + mermaid: + lightTheme: base + darkTheme: base + lightThemeVariables: + primaryColor: "#4a90d9" + primaryTextColor: "#ffffff" + lineColor: "#333333" + darkThemeVariables: + primaryColor: "#6ab0f3" + primaryTextColor: "#ffffff" + lineColor: "#cccccc" + background: "#1a1a2e" +``` + +**hugo.toml:** +```toml +[params.article.mermaid] + lightTheme = "base" + darkTheme = "base" + + [params.article.mermaid.lightThemeVariables] + primaryColor = "#4a90d9" + primaryTextColor = "#ffffff" + lineColor = "#333333" + + [params.article.mermaid.darkThemeVariables] + primaryColor = "#6ab0f3" + primaryTextColor = "#ffffff" + lineColor = "#cccccc" + background = "#1a1a2e" +``` + +Common variables: `primaryColor`, `secondaryColor`, `tertiaryColor`, `primaryTextColor`, `lineColor`, `background`, `fontFamily` + +> **Note:** Theme variables only work with the `base` theme and must use hex color values (e.g., `#ff0000`). + +## Diagram Types + +### Flowchart + +Flowcharts are the most common diagram type. Use `graph` or `flowchart` with direction indicators: +- `TD` or `TB`: Top to bottom +- `BT`: Bottom to top +- `LR`: Left to right +- `RL`: Right to left + +```mermaid +flowchart LR + A[Hard edge] -->|Link text| B(Round edge) + B --> C{Decision} + C -->|One| D[Result one] + C -->|Two| E[Result two] +``` + +### Sequence Diagram + +Perfect for showing interactions between components: + +```mermaid +sequenceDiagram + participant Alice + participant Bob + Alice->>John: Hello John, how are you? + loop Healthcheck + John->>John: Fight against hypochondria + end + Note right of John: Rational thoughts
prevail! + John-->>Alice: Great! + John->>Bob: How about you? + Bob-->>John: Jolly good! +``` + +### Class Diagram + +Visualize class structures and relationships: + +```mermaid +classDiagram + Animal <|-- Duck + Animal <|-- Fish + Animal <|-- Zebra + Animal : +int age + Animal : +String gender + Animal: +isMammal() + Animal: +mate() + class Duck{ + +String beakColor + +swim() + +quack() + } + class Fish{ + -int sizeInFeet + -canEat() + } + class Zebra{ + +bool is_wild + +run() + } +``` + +### State Diagram + +Model state machines and transitions: + +```mermaid +stateDiagram-v2 + [*] --> Still + Still --> [*] + + Still --> Moving + Moving --> Still + Moving --> Crash + Crash --> [*] +``` + +### Entity Relationship Diagram + +Document database schemas: + +```mermaid +erDiagram + CUSTOMER ||--o{ ORDER : places + ORDER ||--|{ LINE-ITEM : contains + CUSTOMER }|..|{ DELIVERY-ADDRESS : uses + CUSTOMER { + string name + string custNumber + string sector + } + ORDER { + int orderNumber + string deliveryAddress + } +``` + +### Gantt Chart + +Plan and track project schedules: + +```mermaid +gantt + title A Gantt Diagram + dateFormat YYYY-MM-DD + section Section + A task :a1, 2024-01-01, 30d + Another task :after a1, 20d + section Another + Task in Another :2024-01-12, 12d + another task :24d +``` + +### Pie Chart + +Display proportional data: + +```mermaid +pie showData + title Key elements in Product X + "Calcium" : 42.96 + "Potassium" : 50.05 + "Magnesium" : 10.01 + "Iron" : 5 +``` + +### Git Graph + +Visualize Git branching strategies: + +```mermaid +gitGraph + commit + commit + branch develop + checkout develop + commit + commit + checkout main + merge develop + commit + commit +``` + +### Mindmap + +Create hierarchical mindmaps: + +```mermaid +mindmap + root((mindmap)) + Origins + Long history + Popularisation + British popular psychology author Tony Buzan + Research + On effectiveness
and features + On Automatic creation + Uses + Creative techniques + Strategic planning + Argument mapping + Tools + Pen and paper + Mermaid +``` + +### Timeline + +Display chronological events: + +```mermaid +timeline + title History of Social Media Platform + 2002 : LinkedIn + 2004 : Facebook + : Google + 2005 : YouTube + 2006 : Twitter +``` + +## Advanced Features + +### HTML in Labels + +To use HTML in labels, you must set `securityLevel: loose` in your site config: + +**hugo.yaml:** +```yaml +params: + article: + mermaid: + securityLevel: loose + htmlLabels: true +``` + +**hugo.toml:** +```toml +[params.article.mermaid] + securityLevel = "loose" + htmlLabels = true +``` + +Then you can use HTML tags like `
` for line breaks: + +````markdown +```mermaid +graph TD + A[Line 1
Line 2] --> B[Bold text] +``` +```` + +### Per-Diagram Theming + +Override the theme for a specific diagram using Mermaid's frontmatter: + +````markdown +```mermaid +%%{init: {'theme': 'forest'}}%% +graph TD + A[Start] --> B[End] +``` +```` + +```mermaid +%%{init: {'theme': 'forest'}}%% +graph TD + A[Christmas] -->|Get money| B(Go shopping) + B --> C{Let me think} + C -->|One| D[Laptop] + C -->|Two| E[iPhone] + C -->|Three| F[Car] +``` + +### Inline Styling with `style` + +You can style individual nodes directly within your diagram using the `style` directive: + +````markdown +```mermaid +flowchart LR + A[Start] --> B[Process] --> C[End] + style A fill:#4ade80,stroke:#166534,color:#000 + style B fill:#60a5fa,stroke:#1e40af,color:#000 + style C fill:#f87171,stroke:#991b1b,color:#fff +``` +```` + +**Result:** + +```mermaid +flowchart LR + A[Start] --> B[Process] --> C[End] + style A fill:#4ade80,stroke:#166534,color:#000 + style B fill:#60a5fa,stroke:#1e40af,color:#000 + style C fill:#f87171,stroke:#991b1b,color:#fff +``` + +Style properties include: +- `fill` - Background color +- `stroke` - Border color +- `stroke-width` - Border thickness +- `color` - Text color +- `stroke-dasharray` - Dashed border (e.g., `5 5`) + +### Styling with CSS Classes + +You can define reusable styles with `classDef` and apply them using `:::className`: + +````markdown +```mermaid +flowchart LR + A:::success --> B:::info --> C:::warning + classDef success fill:#4ade80,stroke:#166534,color:#000 + classDef info fill:#60a5fa,stroke:#1e40af,color:#000 + classDef warning fill:#fbbf24,stroke:#92400e,color:#000 +``` +```` + +**Result:** + +```mermaid +flowchart LR + A:::success --> B:::info --> C:::warning + classDef success fill:#4ade80,stroke:#166534,color:#000 + classDef info fill:#60a5fa,stroke:#1e40af,color:#000 + classDef warning fill:#fbbf24,stroke:#92400e,color:#000 +``` + +### Subgraphs + +Group related nodes together: + +```mermaid +flowchart TB + subgraph one + a1-->a2 + end + subgraph two + b1-->b2 + end + subgraph three + c1-->c2 + end + one --> two + three --> two + two --> c2 +``` + +## Theme Switching + +This theme automatically detects your site's light/dark mode preference and adjusts the Mermaid diagram theme accordingly: + +- **Light mode**: Uses the `default` Mermaid theme +- **Dark mode**: Uses the `dark` Mermaid theme (configurable) + +Try toggling the theme switcher to see diagrams update in real-time! + +## Complex Example + +Here's an example with subgraphs, HTML labels, emojis, and custom styling: + +```mermaid +flowchart TD + subgraph client["👤 Client"] + A["User Device
192.168.1.10"] + end + + subgraph cloud["☁️ Cloud Gateway"] + B["Load Balancer
(SSL Termination)"] + end + + subgraph server["🖥️ Application Server"] + C["API Gateway
10.0.0.1"] + D["Auth Service
10.0.0.2"] + E["Web Server
10.0.0.3"] + F["Database
10.0.0.4"] + end + + A -- "HTTPS Request" --> B + B -- "Forward
(internal)" --> C + C -- "Authenticate" --> D + D -- "Token" --> C + C -- "Route" --> E + E --> F + + style client fill:#1a365d,stroke:#2c5282,color:#fff + style cloud fill:#f6ad55,stroke:#dd6b20,color:#000 + style server fill:#276749,stroke:#22543d,color:#fff +``` + +> **Note:** This example requires `securityLevel: loose` for HTML labels and styling to work. + +## Known Limitations + +### Dark Mode Theming + +Mermaid.js's built-in themes have some limitations: + +- **`dark` theme** (default): Best text contrast, but some diagram backgrounds may appear brownish (e.g., Gantt charts) +- **`neutral` theme**: Better background colors, but some text (labels, legends) may have reduced contrast + +**For full control**, use the `base` theme with custom variables: + +**hugo.yaml:** +```yaml +params: + article: + mermaid: + darkTheme: base + darkThemeVariables: + primaryColor: "#1f2937" + primaryTextColor: "#ffffff" + lineColor: "#9ca3af" + textColor: "#e5e7eb" +``` + +**hugo.toml:** +```toml +[params.article.mermaid] + darkTheme = "base" + + [params.article.mermaid.darkThemeVariables] + primaryColor = "#1f2937" + primaryTextColor = "#ffffff" + lineColor = "#9ca3af" + textColor = "#e5e7eb" +``` + +We plan to improve dark mode theming in future updates as Mermaid.js evolves. + +## Troubleshooting + +### Diagram not rendering? + +1. Make sure you're using a fenced code block with `mermaid` as the language +2. Check your browser's console for syntax errors +3. Verify your Mermaid syntax at [Mermaid Live Editor](https://mermaid.live/) + +### HTML not working in labels? + +HTML in labels requires `securityLevel: loose`. Update your configuration: + +**hugo.yaml:** +```yaml +params: + article: + mermaid: + securityLevel: loose + htmlLabels: true +``` + +**hugo.toml:** +```toml +[params.article.mermaid] + securityLevel = "loose" + htmlLabels = true +``` + +> **Warning:** Using `loose` security level allows HTML in diagrams. Only use this if you trust your diagram content. + +### Syntax errors? + +Mermaid is strict about syntax. Common issues: +- Missing spaces around arrows +- Unclosed brackets or quotes +- Invalid node IDs (avoid special characters) + +## Resources + +- [Mermaid Documentation](https://mermaid.js.org/intro/) +- [Mermaid Live Editor](https://mermaid.live/) - Test diagrams interactively +- [Mermaid Syntax Reference](https://mermaid.js.org/intro/syntax-reference.html) diff --git a/layouts/_markup/render-codeblock-mermaid.html b/layouts/_markup/render-codeblock-mermaid.html index 42e8abf..bb0f689 100644 --- a/layouts/_markup/render-codeblock-mermaid.html +++ b/layouts/_markup/render-codeblock-mermaid.html @@ -1,4 +1,4 @@ -
-  {{ .Inner | htmlEscape | safeHTML }}
+
-{{ .Page.Store.Set "hasMermaid" true }}
+{{- .Page.Store.Set "hasMermaid" true -}}
diff --git a/layouts/_partials/article/components/mermaid.html b/layouts/_partials/article/components/mermaid.html
index 826e2f0..dd75faf 100644
--- a/layouts/_partials/article/components/mermaid.html
+++ b/layouts/_partials/article/components/mermaid.html
@@ -1,9 +1,42 @@
-{{ if .Store.Get "hasMermaid" }}
+{{- if .Store.Get "hasMermaid" -}}
+{{- $cfg := site.Params.article.mermaid | default dict -}}
+
+
+
+
+ + + + +
+ +
+
+
+
+
+ + +{{- $opts := dict "minify" hugo.IsProduction "format" "esm" -}} +{{- $script := resources.Get "ts/mermaid.ts" | js.Build $opts -}} +{{- $jsConfig := dict + "transparentBackground" ($cfg.transparentBackground | default false) + "lightTheme" ($cfg.lightTheme | default "default") + "darkTheme" ($cfg.darkTheme | default "dark") + "lightThemeVariables" ($cfg.lightThemeVariables | default dict) + "darkThemeVariables" ($cfg.darkThemeVariables | default dict) + "securityLevel" ($cfg.securityLevel | default "strict") + "look" ($cfg.look | default "classic") + "htmlLabels" (not (eq ($cfg.htmlLabels | default true) false)) +-}} +{{- with $cfg.maxTextSize }}{{ $jsConfig = merge $jsConfig (dict "maxTextSize" .) }}{{ end -}} +{{- with $cfg.maxEdges }}{{ $jsConfig = merge $jsConfig (dict "maxEdges" .) }}{{ end -}} +{{- with $cfg.fontSize }}{{ $jsConfig = merge $jsConfig (dict "fontSize" .) }}{{ end -}} +{{- with $cfg.fontFamily }}{{ $jsConfig = merge $jsConfig (dict "fontFamily" .) }}{{ end -}} +{{- with $cfg.curve }}{{ $jsConfig = merge $jsConfig (dict "curve" .) }}{{ end -}} +{{- with $cfg.logLevel }}{{ $jsConfig = merge $jsConfig (dict "logLevel" .) }}{{ end -}} -{{ end }} \ No newline at end of file +{{- end -}}