Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Conso8e" Version="10.0.0" />
<PackageVersion Include="Markdig" Version="1.1.2" />
<PackageVersion Include="Mermaider" Version="0.8.0" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.16.0" />
<PackageVersion Include="ModelContextProtocol" Version="1.0.0" />
<PackageVersion Include="ModelContextProtocol.AspNetCore" Version="1.0.0" />
Expand Down
10 changes: 10 additions & 0 deletions src/Elastic.Documentation.Site/Assets/markdown/mermaid.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
187 changes: 14 additions & 173 deletions src/Elastic.Documentation.Site/Assets/mermaid.ts
Original file line number Diff line number Diff line change
@@ -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<string>
}
}
}

let mermaidLoaded = false
let mermaidLoading: Promise<void> | 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<string, string> = {
'--_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
Expand Down Expand Up @@ -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<void> {
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
*/
Expand Down Expand Up @@ -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)
}
}
Loading