';
}
return origCode(code, lang);
};
marked.setOptions({ renderer: renderer, headerIds: true, mangle: false, gfm: true });
// Math: pull \[..\] and \(..\) out before marked (which would eat the
// backslashes), leaving code spans and fences alone, then render with KaTeX.
var maths = [];
if (window.katex) {
src = src.replace(/(^```[\s\S]*?^```[ \t]*$)|(`[^`\n]+`)|\\\[([\s\S]+?)\\\]|\\\(([\s\S]+?)\\\)/gm,
function (m, fence, code, disp, inl) {
if (fence || code) return m;
var d = disp !== undefined;
var i = maths.push({ tex: d ? disp : inl, display: d }) - 1;
return d ? '\n\nKXMATH' + i + 'KX\n\n' : 'KXMATH' + i + 'KX';
});
}
var html = marked.parse(src);
if (maths.length) html = html.replace(/KXMATH(\d+)KX/g, function (m, i) {
var x = maths[+i];
return katex.renderToString(x.tex, { displayMode: x.display, throwOnError: false });
});
document.getElementById('doc').innerHTML = html;
// --- table of contents -------------------------------------------------
var toc = document.getElementById('toc');
var heads = document.querySelectorAll('main h2, main h3');
var slug = function (s) {
return s.toLowerCase().replace(/[^\w\s-]/g, '').trim().replace(/\s+/g, '-').slice(0, 60);
};
var seen = {};
heads.forEach(function (h) {
var id = h.id || slug(h.textContent);
if (seen[id]) { id = id + '-' + (++seen[id]); } else { seen[id] = 1; }
h.id = id;
var a = document.createElement('a');
a.href = '#' + id;
a.textContent = h.textContent;
if (h.tagName === 'H3') a.className = 'h3';
toc.appendChild(a);
});
var links = Array.prototype.slice.call(toc.querySelectorAll('a'));
var spy = function () {
var best = null, y = window.scrollY + 90;
heads.forEach(function (h, i) { if (h.offsetTop <= y) best = i; });
links.forEach(function (a, i) { a.classList.toggle('active', i === best); });
document.getElementById('topBtn').style.display = window.scrollY > 700 ? 'block' : 'none';
};
window.addEventListener('scroll', spy, { passive: true });
spy();
document.getElementById('tocBtn').onclick = function () { toc.classList.toggle('open'); };
document.getElementById('topBtn').onclick = function () { window.scrollTo({ top: 0, behavior: 'smooth' }); };
// --- diagram zoom + pan -------------------------------------------------
// Each diagram keeps its zoom level across a theme re-render, keyed by the
// diagram index rather than the element (the element is replaced).
var MIN_SCALE = 0.30, MAX_SCALE = 2.50, STEP = 0.20;
var savedScale = {};
function clamp(n, lo, hi) { return Math.max(lo, Math.min(hi, n)); }
function enhance(wrap) {
if (!wrap || wrap.dataset.zoomReady === '1') return;
var svg = wrap.querySelector('svg');
if (!svg) return;
var vb = (svg.getAttribute('viewBox') || '').trim().split(/[ ,]+/).map(Number);
if (vb.length !== 4 || !isFinite(vb[2]) || !isFinite(vb[3]) || vb[2] <= 0 || vb[3] <= 0) return;
wrap.dataset.zoomReady = '1';
var idx = wrap.getAttribute('data-diagram');
var baseW = vb[2], baseH = vb[3], scale = 1;
var toolbar = document.createElement('div');
toolbar.className = 'mermaid-toolbar';
toolbar.innerHTML =
'' +
'100%' +
'' +
'' +
'' +
'' +
'drag to pan · shift+wheel sideways · ctrl+wheel zoom';
var canvas = document.createElement('div');
canvas.className = 'mermaid-canvas';
svg.parentNode.insertBefore(canvas, svg);
canvas.appendChild(svg);
wrap.insertBefore(toolbar, canvas);
wrap.setAttribute('tabindex', '0');
var readout = toolbar.querySelector('.zoom-readout');
function apply(next) {
scale = clamp(next, MIN_SCALE, MAX_SCALE);
var w = Math.round(baseW * scale);
var h = Math.round(baseH * scale);
svg.style.width = w + 'px';
svg.style.height = h + 'px';
svg.style.maxWidth = 'none';
svg.setAttribute('width', String(w));
svg.setAttribute('height', String(h));
readout.textContent = Math.round(scale * 100) + '%';
savedScale[idx] = scale;
}
function fit() {
var availW = Math.max(280, wrap.clientWidth - 34);
var availH = Math.max(220, wrap.clientHeight - toolbar.offsetHeight - 34);
apply(Math.min(1, availW / baseW, availH / baseH));
wrap.scrollLeft = 0;
wrap.scrollTop = 0;
}
toolbar.addEventListener('click', function (e) {
var b = e.target.closest('button[data-z]');
if (!b) return;
var action = b.dataset.z;
if (action === 'in') apply(scale + STEP);
else if (action === 'out') apply(scale - STEP);
else if (action === 'actual') apply(1);
else if (action === 'fit') fit();
else if (action === 'full') {
if (document.fullscreenElement === wrap && document.exitFullscreen) {
document.exitFullscreen();
} else if (wrap.requestFullscreen) {
wrap.requestFullscreen().then(fit).catch(function () { apply(1); });
} else {
apply(1);
}
}
});
// ctrl/cmd+wheel zooms (Scott's); shift+wheel scrolls sideways, which is
// the other half of reaching a diagram that is wider than the column.
wrap.addEventListener('wheel', function (e) {
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
apply(scale + (e.deltaY < 0 ? STEP : -STEP));
return;
}
if (e.shiftKey && wrap.scrollWidth > wrap.clientWidth) {
e.preventDefault();
wrap.scrollLeft += (e.deltaY || e.deltaX);
}
}, { passive: false });
// Drag to pan. Pointer events cover mouse, pen and touch in one path.
var dragging = false, sx = 0, sy = 0, sl = 0, st = 0, pid = null;
canvas.addEventListener('pointerdown', function (e) {
if (e.button !== 0) return;
dragging = true; pid = e.pointerId;
sx = e.clientX; sy = e.clientY; sl = wrap.scrollLeft; st = wrap.scrollTop;
canvas.classList.add('grabbing');
try { canvas.setPointerCapture(pid); } catch (err) {}
});
canvas.addEventListener('pointermove', function (e) {
if (!dragging) return;
wrap.scrollLeft = sl - (e.clientX - sx);
wrap.scrollTop = st - (e.clientY - sy);
});
function endDrag() {
if (!dragging) return;
dragging = false;
canvas.classList.remove('grabbing');
try { if (pid !== null) canvas.releasePointerCapture(pid); } catch (err) {}
pid = null;
}
canvas.addEventListener('pointerup', endDrag);
canvas.addEventListener('pointercancel', endDrag);
canvas.addEventListener('lostpointercapture', endDrag);
// Arrow keys once the box has focus, for anyone not using a mouse.
wrap.addEventListener('keydown', function (e) {
var d = e.shiftKey ? 220 : 60, used = true;
if (e.key === 'ArrowLeft') wrap.scrollLeft -= d;
else if (e.key === 'ArrowRight') wrap.scrollLeft += d;
else if (e.key === 'ArrowUp') wrap.scrollTop -= d;
else if (e.key === 'ArrowDown') wrap.scrollTop += d;
else if (e.key === '+' || e.key === '=') apply(scale + STEP);
else if (e.key === '-') apply(scale - STEP);
else if (e.key === '0') fit();
else used = false;
if (used) e.preventDefault();
});
// Native 100% is deliberately the default: a large diagram scrolls rather
// than shrinking to illegibility. A restored zoom wins over that default.
apply(savedScale[idx] !== undefined ? savedScale[idx] : 1);
}
function scan(root) {
(root || document).querySelectorAll('.mermaid-wrap').forEach(enhance);
}
// --- diagrams ----------------------------------------------------------
function drawDiagrams(t) {
if (typeof mermaid === 'undefined') {
document.querySelectorAll('[data-diagram]').forEach(function (el) {
var i = +el.getAttribute('data-diagram');
el.innerHTML = 'Diagram renderer did not load — source below.
' +
'';
el.querySelector('pre').textContent = diagrams[i];
});
return;
}
mermaid.initialize({
startOnLoad: false,
securityLevel: 'strict',
theme: t === 'dark' ? 'dark' : 'default',
themeVariables: t === 'dark' ? { background: '#191c24', primaryColor: '#232838',
primaryTextColor: '#e6e8ee', lineColor: '#7aa2f7', fontSize: '14px' } : { fontSize: '14px' },
// useMaxWidth:false is Scott's fix — mermaid otherwise scales every
// diagram down to the column width and the labels go unreadable.
flowchart: { useMaxWidth: false, htmlLabels: true, curve: 'basis' }
});
document.querySelectorAll('[data-diagram]').forEach(function (el, n) {
var i = +el.getAttribute('data-diagram');
el.innerHTML = '';
// The toolbar lives inside this element, so a re-render destroys it.
// Clearing the flag is what lets enhance() rebuild it.
delete el.dataset.zoomReady;
mermaid.render('mmd-' + t + '-' + n + '-' + Date.now(), diagrams[i]).then(function (res) {
el.innerHTML = res.svg;
enhance(el);
}).catch(function (err) {
el.innerHTML = 'This diagram did not render (' +
String(err && err.message ? err.message : err).replace(/[<>&]/g, '') +
'). Source:
';
el.querySelector('pre').textContent = diagrams[i];
});
});
}
drawDiagrams(theme);
scan(document);
document.getElementById('themeBtn').onclick = function () {
theme = (document.documentElement.getAttribute('data-theme') === 'dark') ? 'light' : 'dark';
applyTheme(theme);
drawDiagrams(theme);
};
})();