feat: Mermaid header bar, Code/Preview toggle, Copy SVG, Download PNG, fullscreen header
CI / lint (push) Successful in 14s
CI / security (push) Successful in 8s
CI / test (push) Successful in 23s
CI / build (push) Successful in 2s

This commit is contained in:
2026-06-05 08:38:21 -04:00
parent fff658d551
commit 28ced406bc
2 changed files with 369 additions and 105 deletions
+292 -79
View File
@@ -83,8 +83,8 @@ export async function renderMermaidBlocks(container) {
for (var i = 0; i < blocks.length; i++) {
var codeBlock = blocks[i];
var code = codeBlock.textContent || codeBlock.innerText || '';
if (!code.trim()) continue;
var originalCode = codeBlock.textContent || codeBlock.innerText || '';
if (!originalCode.trim()) continue;
var wrapper = codeBlock.closest('pre') || codeBlock.parentElement;
if (!wrapper) continue;
@@ -94,30 +94,53 @@ export async function renderMermaidBlocks(container) {
try {
// Preprocess Obsidian syntax before Mermaid parsing (#24)
code = preprocessObsidian(code);
var code = preprocessObsidian(originalCode);
await mermaid.parse(code);
var id = 'mermaid-' + (++_renderCounter) + '-' + Date.now();
var result = await mermaid.render(id, code);
var container_1 = document.createElement('div');
container_1.className = 'mermaid-container';
container_1.innerHTML = '<div class="mermaid-rendered">' + result.svg + '</div>';
// Extract diagram type from first non-comment, non-empty line
var diagramType = extractDiagramType(code);
// Add zoom/pan controls (#27)
var controls = document.createElement('div');
controls.className = 'mermaid-controls';
controls.innerHTML =
'<button class="mermaid-btn mermaid-zoom-in" title="Zoom +"><i data-lucide="zoom-in" style="width:14px;height:14px"></i></button>' +
'<button class="mermaid-btn mermaid-zoom-out" title="Zoom -"><i data-lucide="zoom-out" style="width:14px;height:14px"></i></button>' +
'<button class="mermaid-btn mermaid-reset" title="Reset"><i data-lucide="maximize" style="width:14px;height:14px"></i></button>' +
'<button class="mermaid-btn mermaid-fullscreen" title="Plein ecran"><i data-lucide="expand" style="width:14px;height:14px"></i></button>';
container_1.appendChild(controls);
// Build container with header + body
var containerDiv = document.createElement('div');
containerDiv.className = 'mermaid-container';
wrapper.parentNode.replaceChild(container_1, wrapper);
setupZoomPan(container_1);
// Header bar
var header = document.createElement('div');
header.className = 'mermaid-header';
header.innerHTML =
'<span class="mermaid-diagram-type">' + escapeHtml(diagramType) + '</span>' +
'<div class="mermaid-header-actions">' +
'<button class="mermaid-btn mermaid-zoom-in" title="Zoom +"><i data-lucide="zoom-in" style="width:14px;height:14px"></i></button>' +
'<button class="mermaid-btn mermaid-zoom-out" title="Zoom -"><i data-lucide="zoom-out" style="width:14px;height:14px"></i></button>' +
'<button class="mermaid-btn mermaid-fullscreen" title="Plein ecran"><i data-lucide="expand" style="width:14px;height:14px"></i></button>' +
'<button class="mermaid-btn mermaid-copy" title="Copier SVG"><i data-lucide="copy" style="width:14px;height:14px"></i></button>' +
'<button class="mermaid-btn mermaid-download" title="Telecharger PNG"><i data-lucide="download" style="width:14px;height:14px"></i></button>' +
'<div class="mermaid-toggle">' +
'<button class="mermaid-toggle-btn active" data-mode="preview">Preview</button>' +
'<button class="mermaid-toggle-btn" data-mode="code">Code</button>' +
'</div>' +
'</div>';
// Body
var body = document.createElement('div');
body.className = 'mermaid-body';
body.innerHTML =
'<div class="mermaid-rendered">' + result.svg + '</div>' +
'<div class="mermaid-source" style="display:none"><pre><code class="language-mermaid">' + escapeHtml(originalCode) + '</code></pre></div>';
containerDiv.appendChild(header);
containerDiv.appendChild(body);
wrapper.parentNode.replaceChild(containerDiv, wrapper);
// Wire up header buttons
wireHeaderButtons(containerDiv, originalCode);
setupZoomPan(containerDiv);
setupCodePreviewToggle(containerDiv);
safeCreateIcons();
} catch (err) {
// Render error block
var errorDiv = document.createElement('div');
errorDiv.className = 'mermaid-error';
errorDiv.innerHTML = '<strong>Erreur Mermaid</strong><br>' + escapeHtml(err.message || String(err));
@@ -126,6 +149,86 @@ export async function renderMermaidBlocks(container) {
}
}
// ---------------------------------------------------------------------------
// Header button wiring
// ---------------------------------------------------------------------------
function wireHeaderButtons(container, originalCode) {
// Copy SVG
var copyBtn = container.querySelector('.mermaid-copy');
if (copyBtn) {
copyBtn.addEventListener('click', function() {
copySvgToClipboard(container);
});
}
// 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';
}
// ---------------------------------------------------------------------------
// Zoom & Pan (#27)
// ---------------------------------------------------------------------------
@@ -174,31 +277,44 @@ function setupZoomPan(container) {
update();
}, { passive: false });
container.querySelector('.mermaid-zoom-in').addEventListener('click', function() {
scale = Math.min(5, scale * 1.3); update();
});
container.querySelector('.mermaid-zoom-out').addEventListener('click', function() {
scale = Math.max(0.2, scale * 0.7); update();
});
container.querySelector('.mermaid-reset').addEventListener('click', function() {
scale = 1; panX = 0; panY = 0; update();
});
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)
container.querySelector('.mermaid-fullscreen').addEventListener('click', function() {
openFullscreen(svgWrapper);
});
var fsBtn = container.querySelector('.mermaid-fullscreen');
if (fsBtn) {
fsBtn.addEventListener('click', function() {
openFullscreen(container);
});
}
}
// ---------------------------------------------------------------------------
// Fullscreen mode (#28)
// Fullscreen mode (#28) — with header bar + full-size diagram
// ---------------------------------------------------------------------------
function openFullscreen(svgWrapper) {
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 =
'<button class="mermaid-fullscreen-close" title="Fermer (Echap)"><i data-lucide="x" style="width:18px;height:18px"></i></button>' +
'<div class="mermaid-fullscreen-header">' +
'<span class="mermaid-diagram-type">' + escapeHtml(typeLabel) + '</span>' +
'<div class="mermaid-header-actions">' +
'<button class="mermaid-btn mermaid-fs-zoom-in" title="Zoom +"><i data-lucide="zoom-in" style="width:14px;height:14px"></i></button>' +
'<button class="mermaid-btn mermaid-fs-zoom-out" title="Zoom -"><i data-lucide="zoom-out" style="width:14px;height:14px"></i></button>' +
'<button class="mermaid-btn mermaid-fs-copy" title="Copier SVG"><i data-lucide="copy" style="width:14px;height:14px"></i></button>' +
'<button class="mermaid-btn mermaid-fs-download" title="Telecharger PNG"><i data-lucide="download" style="width:14px;height:14px"></i></button>' +
'<button class="mermaid-btn mermaid-fs-close" title="Fermer (Echap)"><i data-lucide="x" style="width:14px;height:14px"></i></button>' +
'</div>' +
'</div>' +
'<div class="mermaid-fullscreen-content"></div>';
var content = overlay.querySelector('.mermaid-fullscreen-content');
@@ -209,6 +325,64 @@ function openFullscreen(svgWrapper) {
}
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 fsCopy = overlay.querySelector('.mermaid-fs-copy');
if (fsCopy) {
fsCopy.addEventListener('click', function() {
copySvgFromElement(content);
});
}
// Download PNG in fullscreen
var fsDownload = overlay.querySelector('.mermaid-fs-download');
if (fsDownload) {
fsDownload.addEventListener('click', function() {
exportMermaidPNGFromElement(svg);
});
}
safeCreateIcons();
function close() {
@@ -224,7 +398,89 @@ function openFullscreen(svgWrapper) {
overlay.addEventListener('click', function(e) {
if (e.target === overlay) close();
});
overlay.querySelector('.mermaid-fullscreen-close').addEventListener('click', 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);
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(data).then(function() {
/* copied */
}).catch(function() {
fallbackCopy(data);
});
} else {
fallbackCopy(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('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('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);
}
// ---------------------------------------------------------------------------
@@ -237,8 +493,8 @@ export function setupFocusMode() {
document.addEventListener('click', function(e) {
var container = e.target.closest('.mermaid-container');
if (!container) return;
// Don't trigger focus if clicking controls
if (e.target.closest('.mermaid-controls') || e.target.closest('button')) 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;
@@ -285,49 +541,6 @@ function updateFocusPanel(svgWrapper) {
}
}
// ---------------------------------------------------------------------------
// Export SVG/PNG (#24)
// ---------------------------------------------------------------------------
export function exportMermaidSVG(container) {
var svg = container.querySelector('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('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);
}
// ---------------------------------------------------------------------------
// Obsidian compatibility pre-processor (#24)
// ---------------------------------------------------------------------------
+77 -26
View File
@@ -7621,7 +7621,6 @@ body.popup-mode .content-area {
========================================================================== */
.mermaid-container {
position: relative;
margin: 12px 0;
border: 1px solid var(--border);
border-radius: 8px;
@@ -7629,6 +7628,55 @@ body.popup-mode .content-area {
background: var(--bg-secondary);
}
/* ── Header bar ── */
.mermaid-header {
display: flex; align-items: center; justify-content: space-between;
padding: 6px 10px;
background: var(--bg-tertiary, #f0f0f5);
border-bottom: 1px solid var(--border);
min-height: 34px;
}
.mermaid-diagram-type {
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
font-size: 0.75rem; font-weight: 600;
color: var(--text-secondary);
}
.mermaid-header-actions {
display: flex; align-items: center; gap: 2px;
}
/* ── Buttons ── */
.mermaid-btn {
width: 28px; height: 28px;
border: 1px solid transparent; border-radius: 4px;
background: transparent; color: var(--text-secondary);
cursor: pointer; display: flex; align-items: center; justify-content: center;
transition: background 0.12s, color 0.12s;
}
.mermaid-btn:hover { background: var(--bg-primary); color: var(--accent); border-color: var(--border); }
/* ── Code / Preview toggle ── */
.mermaid-toggle {
display: flex; margin-left: 8px;
border: 1px solid var(--border); border-radius: 5px;
overflow: hidden;
}
.mermaid-toggle-btn {
padding: 2px 10px; font-size: 0.7rem; font-weight: 500;
border: none; background: transparent;
color: var(--text-muted, #999); cursor: pointer;
transition: background 0.12s, color 0.12s;
}
.mermaid-toggle-btn.active {
background: var(--bg-primary); color: var(--text-primary);
box-shadow: 0 0 0 1px var(--border);
}
.mermaid-toggle-btn:first-child { border-right: 1px solid var(--border); }
/* ── Body ── */
.mermaid-body {
position: relative;
}
.mermaid-rendered {
padding: 16px;
overflow: hidden;
@@ -7636,25 +7684,17 @@ body.popup-mode .content-area {
display: flex;
justify-content: center;
}
.mermaid-rendered svg { max-width: 100%; height: auto; }
.mermaid-controls {
position: absolute;
top: 6px; right: 6px;
display: flex; gap: 2px;
opacity: 0; transition: opacity 0.15s;
.mermaid-source { padding: 16px; overflow: auto; }
.mermaid-source pre {
margin: 0; padding: 12px;
background: var(--bg-tertiary, #f5f5f5);
border-radius: 4px;
font-size: 0.78rem; line-height: 1.5;
}
.mermaid-container:hover .mermaid-controls { opacity: 1; }
.mermaid-btn {
width: 28px; height: 28px;
border: 1px solid var(--border); border-radius: 4px;
background: var(--bg-primary); color: var(--text-secondary);
cursor: pointer; display: flex; align-items: center; justify-content: center;
}
.mermaid-btn:hover { color: var(--accent); border-color: var(--accent); }
/* ── Error ── */
.mermaid-error {
margin: 12px 0; padding: 12px 16px;
border: 1px solid rgba(239,68,68,0.3); border-radius: 6px;
@@ -7663,22 +7703,33 @@ body.popup-mode .content-area {
}
.mermaid-error strong { display: block; margin-bottom: 4px; }
/* ── Fullscreen ── */
.mermaid-fullscreen-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
z-index: 10000; background: rgba(0,0,0,0.85);
display: flex; align-items: center; justify-content: center;
z-index: 10000; background: rgba(0,0,0,0.92);
display: flex; flex-direction: column;
}
.mermaid-fullscreen-close {
position: absolute; top: 16px; right: 16px;
width: 40px; height: 40px; border: 1px solid rgba(255,255,255,0.2);
border-radius: 8px; background: rgba(0,0,0,0.5); color: #fff;
cursor: pointer; display: flex; align-items: center; justify-content: center;
z-index: 1;
.mermaid-fullscreen-header {
display: flex; align-items: center; justify-content: space-between;
padding: 10px 16px;
background: rgba(255,255,255,0.06);
border-bottom: 1px solid rgba(255,255,255,0.08);
min-height: 46px;
flex-shrink: 0;
}
.mermaid-fullscreen-header .mermaid-diagram-type { color: rgba(255,255,255,0.7); }
.mermaid-fullscreen-header .mermaid-btn {
color: rgba(255,255,255,0.6);
}
.mermaid-fullscreen-header .mermaid-btn:hover {
background: rgba(255,255,255,0.1);
color: #fff;
}
.mermaid-fullscreen-close:hover { background: rgba(255,255,255,0.1); }
.mermaid-fullscreen-content {
width: 95vw; height: 95vh;
flex: 1;
display: flex; align-items: center; justify-content: center;
padding: 16px;
overflow: hidden;
}
.mermaid-fullscreen-content svg {
width: 100%; height: 100%;