Refactor frontend into ES modules with modern tooling
CI / lint (push) Successful in 25s
CI / security (push) Successful in 19s
CI / test (push) Successful in 29s
CI / build (push) Successful in 8s
CI / e2e (push) Failing after 21s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled
CI / lint (push) Successful in 25s
CI / security (push) Successful in 19s
CI / test (push) Successful in 29s
CI / build (push) Successful in 8s
CI / e2e (push) Failing after 21s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,845 @@
|
||||
/**
|
||||
* 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 =
|
||||
'<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-svg" title="Copier SVG"><i data-lucide="copy" style="width:14px;height:14px"></i></button>' +
|
||||
'<button class="mermaid-btn mermaid-copy-code" title="Copier le code Mermaid"><i data-lucide="file-code-2" 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);
|
||||
// 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 = '<strong>Erreur Mermaid</strong><br>' + 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 =
|
||||
'<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-svg" title="Copier SVG"><i data-lucide="copy" style="width:14px;height:14px"></i></button>' +
|
||||
'<button class="mermaid-btn mermaid-fs-copy-code" title="Copier le code Mermaid"><i data-lucide="file-code-2" 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');
|
||||
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 =
|
||||
'<div class="mermaid-focus-header">' +
|
||||
'<span>Diagramme</span>' +
|
||||
'<button class="mermaid-focus-close" title="Fermer"><i data-lucide="x" style="width:14px;height:14px"></i></button>' +
|
||||
'</div>' +
|
||||
'<div class="mermaid-focus-content"></div>';
|
||||
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 '<img src="' + src + '" width="' + width + '" />';
|
||||
}
|
||||
return '<img src="' + src + '" />';
|
||||
})
|
||||
.replace(/\[\[([^\]]+?)\]\]/g, '<a href="$1">$1</a>')
|
||||
.replace(/==([^=]+)==/g, '<mark>$1</mark>');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Utility
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function escapeHtml(str) {
|
||||
var div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
Reference in New Issue
Block a user