/** * Mermaid.js integration for ObsiGate. * * Handles initialization, rendering, templates, zoom/pan, * fullscreen, focus mode, theme sync, and Obsidian compatibility. * * Usage: import { initMermaid, renderMermaidBlocks } from './mermaid-viewer.js'; */ import { safeCreateIcons } from './utils.js'; // --------------------------------------------------------------------------- // Configuration (#20) // --------------------------------------------------------------------------- var _mermaidReady = false; var _currentTheme = 'default'; export function initMermaid() { if (typeof mermaid === 'undefined') { console.warn('[Mermaid] library not loaded'); return; } mermaid.initialize(_getMermaidConfig(_currentTheme === 'dark')); _mermaidReady = true; } export function updateMermaidTheme(isDark) { _currentTheme = isDark ? 'dark' : 'default'; if (_mermaidReady) { mermaid.initialize(_getMermaidConfig(isDark)); } // Re-render all existing Mermaid containers with the new theme var containers = document.querySelectorAll('.mermaid-container'); for (var i = 0; i < containers.length; i++) { reRenderContainer(containers[i]); } } // Unified theme config — used by both init and theme toggle function _getMermaidConfig(isDark) { return { startOnLoad: false, securityLevel: 'strict', theme: 'base', fontFamily: 'inherit', themeVariables: isDark ? DARK_THEME : LIGHT_THEME, }; } // Dark theme — optimized for readability on dark backgrounds var DARK_THEME = { // Flowchart / general shapes primaryColor: '#1e3a5f', primaryTextColor: '#c8daf0', primaryBorderColor: '#4a7ab5', lineColor: '#5a7a9a', secondaryColor: '#2a4a6a', tertiaryColor: '#3a6090', // Background & containers background: '#0f1117', mainBkg: '#1a2030', nodeBorder: '#4a7ab5', clusterBkg: '#1a2235', clusterBorder: '#3a5a80', titleColor: '#a0c0e0', edgeLabelBackground: '#1a2d47', nodeTextColor: '#c8daf0', // Sequence diagram actorBkg: '#1e3a5f', actorBorder: '#4a7ab5', actorTextColor: '#c8daf0', actorLineColor: '#4a7ab5', signalColor: '#c8daf0', signalTextColor: '#c8daf0', labelBoxBkgColor: '#1e3a5f', labelBoxBorderColor: '#4a7ab5', labelTextColor: '#c8daf0', loopTextColor: '#c8daf0', noteBkgColor: '#2a3550', noteBorderColor: '#4a7ab5', noteTextColor: '#c8daf0', activationBkgColor: '#3a5a80', // Gantt sectionBkgColor: '#1a2d47', altSectionBkgColor: '#1e3350', sectionBkgColor2: '#1a2d47', taskBkgColor: '#1e3a5f', taskBorderColor: '#4a7ab5', taskTextColor: '#c8daf0', taskTextOutsideColor: '#8aacca', taskTextLightColor: '#e0e8f0', gridColor: '#2a3a55', activeTaskBkgColor: '#2a5a8a', activeTaskBorderColor: '#6a9ad0', doneTaskBkgColor: '#1a3a4f', doneTaskBorderColor: '#3a7a9a', critBkgColor: '#5a2a3a', critBorderColor: '#a04050', todayLineColor: '#4a7ab5', // Git graph git0: '#4a7ab5', git1: '#3a6a9a', git2: '#5a8ac0', git3: '#2a5a8a', git4: '#6a9ad0', git5: '#3a7aaa', git6: '#4a8abf', git7: '#5a9acf', gitInv0: '#c8daf0', gitInv1: '#b0c8e0', gitInv2: '#d0e0f0', gitInv3: '#a0b8d0', gitBranchLabel0: '#c8daf0', gitBranchLabel1: '#b0c8e0', gitBranchLabel2: '#d0e0f0', commitLabelColor: '#c8daf0', commitLabelBackground: '#1e3a5f', commitLabelFontSize: '11px', tagLabelColor: '#a0c0e0', tagLabelBackground: '#1a2d47', tagLabelBorder: '#4a7ab5', // Pie chart pie1: '#1e3a5f', pie2: '#2a5a8a', pie3: '#153050', pie4: '#3a6a9a', pie5: '#204070', pie6: '#4a7ab5', pie7: '#255080', pie8: '#5a8ac0', pieTitleTextColor: '#c8daf0', pieSectionTextColor: '#c8daf0', pieLegendTextColor: '#a0c0e0', pieStrokeColor: '#0f1117', // Class diagram classText: '#c8daf0', // State diagram labelColor: '#c8daf0', // ER diagram — dark alternating rows for readability fillType0: '#111d30', fillType1: '#152238', fillType2: '#111d30', fillType3: '#152238', fillType4: '#111d30', fillType5: '#152238', fillType6: '#111d30', fillType7: '#152238', entityBorder: '#4a7ab5', entityBackground: '#0d1520', attributeEvenColor: '#ffffff', attributeOddColor: '#c8daf0', // Timeline — section colors cScale0: '#1e3a5f', cScale1: '#2a5a8a', cScale2: '#153050', cScale3: '#3a6a9a', cScale4: '#204070', cScale5: '#4a7ab5', cScaleLabel0: '#c8daf0', cScaleLabel1: '#c8daf0', cScaleLabel2: '#c8daf0', cScaleLabel3: '#c8daf0', cScaleLabel4: '#c8daf0', cScaleLabel5: '#c8daf0', // XY Chart — bar and line colors xyChartBackgroundColor: '#0f1117', xyChartTitleColor: '#c8daf0', xyChartXaxisTitleColor: '#8aacca', xyChartYaxisTitleColor: '#8aacca', plotColor1: '#1e3a5f', plotColor2: '#4a7ab5', plotColor3: '#2a5a8a', plotColor4: '#5a8ac0', plotColor5: '#3a6a9a', plotColor6: '#6a9ad0', plotColor7: '#153050', plotColor8: '#204070', // General text textColor: '#c8daf0', fontSize: '14px', }; // Light theme — clean, minimal var LIGHT_THEME = { primaryColor: '#f0f0f5', primaryTextColor: '#1a1a2e', primaryBorderColor: '#ccc', lineColor: '#666', secondaryColor: '#e8e8f0', tertiaryColor: '#fafafa', background: '#ffffff', mainBkg: '#f5f5fa', nodeBorder: '#ccc', clusterBkg: '#f0f0f5', clusterBorder: '#ddd', titleColor: '#333', edgeLabelBackground: '#f5f5fa', nodeTextColor: '#1a1a2e', fontSize: '14px', }; // --------------------------------------------------------------------------- // Templates for all diagram types (#22) // --------------------------------------------------------------------------- export var MERMAID_TEMPLATES = { flowchart: 'flowchart TD\n A[Start] --> B{Decision?}\n B -->|Yes| C[OK]\n B -->|No| D[Cancel]', sequenceDiagram: 'sequenceDiagram\n Alice->>Bob: Hello\n Bob-->>Alice: Hi!', classDiagram: 'classDiagram\n class Animal {\n +name: string\n +age: int\n +makeSound()\n }\n class Dog {\n +bark()\n }\n Animal <|-- Dog', 'stateDiagram-v2': 'stateDiagram-v2\n [*] --> Idle\n Idle --> Running : start\n Running --> Idle : stop\n Running --> [*] : done', erDiagram: 'erDiagram\n CUSTOMER ||--o{ ORDER : places\n ORDER ||--|{ LINE-ITEM : contains', journey: 'journey\n title My day\n section Morning\n Wake up: 5: Me\n Breakfast: 3: Me\n section Work\n Code: 5: Me, Cat', gantt: 'gantt\n title Project Plan\n dateFormat YYYY-MM-DD\n section Design\n Mockups :a1, 2024-01-01, 7d\n section Dev\n Backend :a2, after a1, 14d', pie: 'pie title Languages\n "Python" : 45\n "JavaScript" : 30\n "Go" : 15\n "Rust" : 10', gitGraph: 'gitGraph\n commit\n branch feature\n checkout feature\n commit\n checkout main\n merge feature', requirementDiagram: 'requirementDiagram\n requirement req1 {\n id: 1\n text: System shall do X\n }\n element Component {\n type: package\n }\n Component - satisfies -> req1', mindmap: 'mindmap\n root((Project))\n Frontend\n HTML\n CSS\n JavaScript\n Backend\n API\n Database', timeline: 'timeline\n title History\n 2020 : Started\n 2022 : v1.0\n 2024 : v2.0', quadrantChart: 'quadrantChart\n title Products\n x-axis Low Reach --> High Reach\n y-axis Low Value --> High Value\n quadrant-1 Stars\n quadrant-2 Question Marks\n Product A: [0.8, 0.6]\n Product B: [0.3, 0.8]', C4Context: 'C4Context\n title System Context\n Person(user, "User")\n System(app, "My App")\n System_Ext(db, "Database")\n Rel(user, app, "Uses")\n Rel(app, db, "Reads/Writes")', C4Container: 'C4Container\n title Containers\n Container(web, "Web App", "React")\n Container(api, "API", "FastAPI")\n ContainerDb(db, "DB", "PostgreSQL")\n Rel(web, api, "REST")\n Rel(api, db, "SQL")', C4Component: 'C4Component\n title API Components\n Container_Boundary(api, "API") {\n Component(auth, "Auth Service")\n Component(search, "Search Engine")\n }', C4Dynamic: 'C4Dynamic\n title Login Flow\n Person(user, "User")\n Container(web, "Web App")\n Container(api, "API")\n Rel(user, web, "POST /login")\n Rel(web, api, "authenticate")', C4Deployment: 'C4Deployment\n title Deployment\n Deployment_Node(cloud, "Cloud") {\n Container(web, "Web")\n Container(api, "API")\n }', 'xychart-beta': 'xychart-beta\n title "Sales"\n x-axis [Jan, Feb, Mar, Apr]\n y-axis "Revenue" 0 --> 100\n bar [45, 60, 75, 90]\n line [30, 45, 55, 70]', 'block-beta': 'block-beta\n columns 3\n A B C\n D E F', 'sankey-beta': 'sankey-beta\n A, B, 50\n A, C, 30\n B, D, 20\n C, D, 30', }; // --------------------------------------------------------------------------- // Rendering engine (#21) // --------------------------------------------------------------------------- var _renderCounter = 0; export async function renderMermaidBlocks(container) { if (!_mermaidReady || typeof mermaid === 'undefined') return; var blocks = container.querySelectorAll('pre code.language-mermaid, .mermaid-code'); if (blocks.length === 0) return; for (var i = 0; i < blocks.length; i++) { var codeBlock = blocks[i]; var originalCode = codeBlock.textContent || codeBlock.innerText || ''; if (!originalCode.trim()) continue; var wrapper = codeBlock.closest('pre') || codeBlock.parentElement; if (!wrapper) continue; // Check if already rendered if (wrapper.querySelector('.mermaid-rendered')) continue; try { // Preprocess Obsidian syntax before Mermaid parsing (#24) var code = preprocessObsidian(originalCode); await mermaid.parse(code); var id = 'mermaid-' + (++_renderCounter) + '-' + Date.now(); var result = await mermaid.render(id, code); // Extract diagram type from first non-comment, non-empty line var diagramType = extractDiagramType(code); // Build container with header + body var containerDiv = document.createElement('div'); containerDiv.className = 'mermaid-container'; containerDiv.setAttribute('data-mermaid-code', originalCode); // Header bar var header = document.createElement('div'); header.className = 'mermaid-header'; header.innerHTML = '' + escapeHtml(diagramType) + '' + '
' + '' + '' + '' + '' + '' + '' + '
' + '' + '' + '
' + '
'; // Body var body = document.createElement('div'); body.className = 'mermaid-body'; body.innerHTML = '
' + result.svg + '
' + ''; containerDiv.appendChild(header); containerDiv.appendChild(body); wrapper.parentNode.replaceChild(containerDiv, wrapper); // Wire up header buttons wireHeaderButtons(containerDiv, originalCode); setupZoomPan(containerDiv); setupCodePreviewToggle(containerDiv); // Patch beta diagram colors for dark mode if (_currentTheme === 'dark') patchBetaColors(containerDiv, diagramType); safeCreateIcons(); } catch (err) { var errorDiv = document.createElement('div'); errorDiv.className = 'mermaid-error'; errorDiv.innerHTML = 'Erreur Mermaid
' + escapeHtml(err.message || String(err)); wrapper.parentNode.replaceChild(errorDiv, wrapper); } } } // --------------------------------------------------------------------------- // Header button wiring // --------------------------------------------------------------------------- function wireHeaderButtons(container, originalCode) { // Copy SVG var copySvgBtn = container.querySelector('.mermaid-copy-svg'); if (copySvgBtn) { copySvgBtn.addEventListener('click', function() { copySvgToClipboard(container); }); } // Copy code var copyCodeBtn = container.querySelector('.mermaid-copy-code'); if (copyCodeBtn) { copyCodeBtn.addEventListener('click', function() { copyToClipboard(originalCode); }); } // Download PNG var downloadBtn = container.querySelector('.mermaid-download'); if (downloadBtn) { downloadBtn.addEventListener('click', function() { exportMermaidPNG(container); }); } } // --------------------------------------------------------------------------- // Code / Preview toggle // --------------------------------------------------------------------------- function setupCodePreviewToggle(container) { var toggleBtns = container.querySelectorAll('.mermaid-toggle-btn'); var rendered = container.querySelector('.mermaid-rendered'); var source = container.querySelector('.mermaid-source'); var zoomBtns = container.querySelectorAll('.mermaid-zoom-in, .mermaid-zoom-out'); var fullscreenBtn = container.querySelector('.mermaid-fullscreen'); for (var i = 0; i < toggleBtns.length; i++) { toggleBtns[i].addEventListener('click', function() { var mode = this.getAttribute('data-mode'); // Update active state for (var j = 0; j < toggleBtns.length; j++) { toggleBtns[j].classList.remove('active'); } this.classList.add('active'); if (mode === 'preview') { rendered.style.display = ''; source.style.display = 'none'; // Show zoom/fullscreen buttons in preview mode for (var k = 0; k < zoomBtns.length; k++) zoomBtns[k].style.display = ''; if (fullscreenBtn) fullscreenBtn.style.display = ''; } else { rendered.style.display = 'none'; source.style.display = ''; // Hide zoom/fullscreen in code mode for (var k = 0; k < zoomBtns.length; k++) zoomBtns[k].style.display = 'none'; if (fullscreenBtn) fullscreenBtn.style.display = 'none'; } }); } } // --------------------------------------------------------------------------- // Diagram type extraction // --------------------------------------------------------------------------- function extractDiagramType(code) { var lines = code.trim().split('\n'); for (var i = 0; i < lines.length; i++) { var line = lines[i].trim(); // Skip comments and empty lines if (!line || line.indexOf('%%') === 0) continue; // Return the first meaningful word(s) var words = line.split(/\s+/); if (words.length >= 2) { return words[0] + ' ' + words[1]; } return words[0] || 'mermaid'; } return 'mermaid'; } // --------------------------------------------------------------------------- // Re-render a container after theme change // --------------------------------------------------------------------------- async function reRenderContainer(container) { var code = container.getAttribute('data-mermaid-code'); if (!code) return; var rendered = container.querySelector('.mermaid-rendered'); if (!rendered) return; try { var id = 'mermaid-' + (++_renderCounter) + '-' + Date.now(); var result = await mermaid.render(id, preprocessObsidian(code)); rendered.innerHTML = result.svg; // Reset zoom/pan state on the SVG var svg = rendered.querySelector('svg'); if (svg) { svg.style.transform = ''; svg.style.cursor = 'grab'; } // Patch beta diagram colors for dark mode if (_currentTheme === 'dark') { var diagramType = extractDiagramType(code); patchBetaColors(container, diagramType); } } catch (err) { // Keep old rendering on error console.warn('[Mermaid] Re-render failed:', err.message); } } // --------------------------------------------------------------------------- // Patch beta diagram colors for dark mode (inline SVG attributes beat CSS) // --------------------------------------------------------------------------- function patchBetaColors(container, diagramType) { var svg = container.querySelector('.mermaid-rendered svg'); if (!svg) return; // packet-beta — override white block backgrounds if (diagramType.indexOf('packet') !== -1) { var rects = svg.querySelectorAll('rect'); for (var i = 0; i < rects.length; i++) { var r = rects[i]; var fill = r.getAttribute('fill'); // Only override if the fill is light/white (preserve dark fills) if (!fill || fill === '#f4f4f4' || fill === '#fff' || fill === '#ffffff' || fill === 'white' || fill === '#e8e8e8') { r.setAttribute('fill', '#1e3a5f'); r.setAttribute('stroke', '#4a7ab5'); } } // Text color var texts = svg.querySelectorAll('text'); for (var j = 0; j < texts.length; j++) { var t = texts[j]; var tfill = t.getAttribute('fill'); if (!tfill || tfill === '#000' || tfill === '#333' || tfill === 'black') { t.setAttribute('fill', '#c8daf0'); } } } // xychart-beta — override cream bars and orange line if (diagramType.indexOf('xychart') !== -1) { var allRects = svg.querySelectorAll('rect'); for (var k = 0; k < allRects.length; k++) { var rr = allRects[k]; var f = rr.getAttribute('fill'); if (!f || f === '#f4f4f4' || f === '#fff' || f === '#ffffff' || f === 'white' || f.indexOf('cream') !== -1 || f === '#ececec') { rr.setAttribute('fill', '#1e3a5f'); rr.setAttribute('stroke', '#4a7ab5'); } } // Line color var paths = svg.querySelectorAll('path'); for (var m = 0; m < paths.length; m++) { var p = paths[m]; var ps = p.getAttribute('stroke'); if (!ps || ps === '#ffa500' || ps === 'orange' || ps === '#ff8c00') { p.setAttribute('stroke', '#6a9ad0'); } } } } // --------------------------------------------------------------------------- // Zoom & Pan (#27) // --------------------------------------------------------------------------- function setupZoomPan(container) { var svgWrapper = container.querySelector('.mermaid-rendered'); if (!svgWrapper) return; var svg = svgWrapper.querySelector('svg'); if (!svg) return; var scale = 1; var panX = 0, panY = 0; var dragging = false, startX, startY; svg.style.transformOrigin = '0 0'; svg.style.cursor = 'grab'; function update() { svg.style.transform = 'translate(' + panX + 'px, ' + panY + 'px) scale(' + scale + ')'; } svg.addEventListener('mousedown', function(e) { dragging = true; startX = e.clientX - panX; startY = e.clientY - panY; svg.style.cursor = 'grabbing'; e.preventDefault(); }); document.addEventListener('mousemove', function(e) { if (!dragging) return; panX = e.clientX - startX; panY = e.clientY - startY; update(); }); document.addEventListener('mouseup', function() { dragging = false; if (svg) svg.style.cursor = 'grab'; }); svg.addEventListener('wheel', function(e) { e.preventDefault(); var delta = e.deltaY > 0 ? 0.9 : 1.1; scale = Math.max(0.2, Math.min(5, scale * delta)); update(); }, { passive: false }); var zoomIn = container.querySelector('.mermaid-zoom-in'); var zoomOut = container.querySelector('.mermaid-zoom-out'); if (zoomIn) zoomIn.addEventListener('click', function() { scale = Math.min(5, scale * 1.3); update(); }); if (zoomOut) zoomOut.addEventListener('click', function() { scale = Math.max(0.2, scale * 0.7); update(); }); // Fullscreen (#28) var fsBtn = container.querySelector('.mermaid-fullscreen'); if (fsBtn) { fsBtn.addEventListener('click', function() { openFullscreen(container); }); } } // --------------------------------------------------------------------------- // Fullscreen mode (#28) — with header bar + full-size diagram // --------------------------------------------------------------------------- function openFullscreen(container) { var svgWrapper = container.querySelector('.mermaid-rendered'); var diagramType = container.querySelector('.mermaid-diagram-type'); var typeLabel = diagramType ? diagramType.textContent : 'mermaid'; var overlay = document.createElement('div'); overlay.className = 'mermaid-fullscreen-overlay'; // Fullscreen header bar overlay.innerHTML = '
' + '' + escapeHtml(typeLabel) + '' + '
' + '' + '' + '' + '' + '' + '' + '
' + '
' + '
'; var content = overlay.querySelector('.mermaid-fullscreen-content'); content.innerHTML = svgWrapper.innerHTML; var svg = content.querySelector('svg'); if (svg) { svg.style.cursor = 'default'; } document.body.appendChild(overlay); // Fullscreen zoom var fsScale = 1, fsPanX = 0, fsPanY = 0, fsDragging = false, fsStartX, fsStartY; if (svg) { svg.style.transformOrigin = '0 0'; function fsUpdate() { svg.style.transform = 'translate(' + fsPanX + 'px, ' + fsPanY + 'px) scale(' + fsScale + ')'; } svg.addEventListener('mousedown', function(e) { fsDragging = true; fsStartX = e.clientX - fsPanX; fsStartY = e.clientY - fsPanY; svg.style.cursor = 'grabbing'; e.preventDefault(); }); document.addEventListener('mousemove', function(e) { if (!fsDragging) return; fsPanX = e.clientX - fsStartX; fsPanY = e.clientY - fsStartY; fsUpdate(); }); document.addEventListener('mouseup', function() { fsDragging = false; if (svg) svg.style.cursor = 'grab'; }); svg.addEventListener('wheel', function(e) { e.preventDefault(); var delta = e.deltaY > 0 ? 0.9 : 1.1; fsScale = Math.max(0.2, Math.min(5, fsScale * delta)); fsUpdate(); }, { passive: false }); var fsZoomIn = overlay.querySelector('.mermaid-fs-zoom-in'); var fsZoomOut = overlay.querySelector('.mermaid-fs-zoom-out'); if (fsZoomIn) fsZoomIn.addEventListener('click', function() { fsScale = Math.min(5, fsScale * 1.3); fsUpdate(); }); if (fsZoomOut) fsZoomOut.addEventListener('click', function() { fsScale = Math.max(0.2, fsScale * 0.7); fsUpdate(); }); } // Copy SVG in fullscreen var fsCopySvg = overlay.querySelector('.mermaid-fs-copy-svg'); if (fsCopySvg) { fsCopySvg.addEventListener('click', function() { copySvgFromElement(content); }); } // Copy code in fullscreen var fsCopyCode = overlay.querySelector('.mermaid-fs-copy-code'); if (fsCopyCode) { fsCopyCode.addEventListener('click', function() { var code = container.getAttribute('data-mermaid-code') || ''; copyToClipboard(code); }); } // Download PNG in fullscreen var fsDownload = overlay.querySelector('.mermaid-fs-download'); if (fsDownload) { fsDownload.addEventListener('click', function() { exportMermaidPNGFromElement(svg); }); } safeCreateIcons(); function close() { overlay.remove(); document.removeEventListener('keydown', escHandler); } var escHandler = function(e) { if (e.key === 'Escape') close(); }; document.addEventListener('keydown', escHandler); overlay.addEventListener('click', function(e) { if (e.target === overlay) close(); }); overlay.querySelector('.mermaid-fs-close').addEventListener('click', close); } // --------------------------------------------------------------------------- // Copy SVG to clipboard // --------------------------------------------------------------------------- function copySvgToClipboard(container) { copySvgFromElement(container.querySelector('.mermaid-rendered')); } function copySvgFromElement(el) { if (!el) return; var svg = el.querySelector('svg'); if (!svg) return; var data = new XMLSerializer().serializeToString(svg); copyToClipboard(data); } function fallbackCopy(text) { var textarea = document.createElement('textarea'); textarea.value = text; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); try { document.execCommand('copy'); } catch (e) {} document.body.removeChild(textarea); } // --------------------------------------------------------------------------- // Export SVG/PNG (#24) // --------------------------------------------------------------------------- export function exportMermaidSVG(container) { var svg = container.querySelector('.mermaid-rendered svg'); if (!svg) return; var data = new XMLSerializer().serializeToString(svg); var blob = new Blob([data], { type: 'image/svg+xml' }); downloadBlob(blob, 'diagram.svg'); } export function exportMermaidPNG(container) { var svg = container.querySelector('.mermaid-rendered svg'); exportMermaidPNGFromElement(svg); } function exportMermaidPNGFromElement(svg) { if (!svg) return; var data = new XMLSerializer().serializeToString(svg); var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); var img = new Image(); var blob = new Blob([data], { type: 'image/svg+xml' }); var url = URL.createObjectURL(blob); img.onload = function() { canvas.width = img.width * 2; canvas.height = img.height * 2; ctx.scale(2, 2); ctx.drawImage(img, 0, 0); URL.revokeObjectURL(url); canvas.toBlob(function(pngBlob) { downloadBlob(pngBlob, 'diagram.png'); }, 'image/png'); }; img.src = url; } function downloadBlob(blob, filename) { var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; a.click(); URL.revokeObjectURL(a.href); } // --------------------------------------------------------------------------- // Copy text to clipboard // --------------------------------------------------------------------------- function copyToClipboard(text) { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).catch(function() { fallbackCopy(text); }); } else { fallbackCopy(text); } } // --------------------------------------------------------------------------- // Focus mode (#29) — click diagram to enlarge in a side panel // --------------------------------------------------------------------------- var _focusPanel = null; export function setupFocusMode() { document.addEventListener('click', function(e) { var container = e.target.closest('.mermaid-container'); if (!container) return; // Don't trigger focus if clicking header or controls if (e.target.closest('.mermaid-header') || e.target.closest('button')) return; var svgWrapper = container.querySelector('.mermaid-rendered'); if (!svgWrapper) return; if (_focusPanel && _focusPanel.style.display !== 'none') { updateFocusPanel(svgWrapper); } else { showFocusPanel(svgWrapper); } }); } function showFocusPanel(svgWrapper) { if (!_focusPanel) { _focusPanel = document.createElement('div'); _focusPanel.className = 'mermaid-focus-panel'; _focusPanel.innerHTML = '
' + 'Diagramme' + '' + '
' + '
'; document.body.appendChild(_focusPanel); _focusPanel.querySelector('.mermaid-focus-close').addEventListener('click', function() { _focusPanel.style.display = 'none'; }); } _focusPanel.style.display = 'block'; updateFocusPanel(svgWrapper); safeCreateIcons(); } function updateFocusPanel(svgWrapper) { if (!_focusPanel) return; var content = _focusPanel.querySelector('.mermaid-focus-content'); content.innerHTML = svgWrapper.innerHTML; var svg = content.querySelector('svg'); if (svg) { svg.style.width = '100%'; svg.style.height = 'auto'; svg.style.maxHeight = '80vh'; } } // --------------------------------------------------------------------------- // Obsidian compatibility pre-processor (#24) // --------------------------------------------------------------------------- export function preprocessObsidian(code) { return code .replace(/!\[\[([^\]]+?)\]\]/g, function(_, m) { var parts = m.split('|'); var src = parts[0].trim(); var width = parts[1] ? parts[1].trim() : null; if (width && /^\d+$/.test(width)) { return ''; } return ''; }) .replace(/\[\[([^\]]+?)\]\]/g, '$1') .replace(/==([^=]+)==/g, '$1'); } // --------------------------------------------------------------------------- // Utility // --------------------------------------------------------------------------- function escapeHtml(str) { var div = document.createElement('div'); div.textContent = str; return div.innerHTML; }