<!DOCTYPE html>
<html lang="en" {{THEME_ATTR}}>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{FILENAME}} — birta</title>
<link rel="icon" type="image/png" href="/favicon.png">
<style>{{GITHUB_CSS}}</style>
<style>{{THEME_OVERRIDES}}</style>
<style>{{PAGE_CSS}}</style>
<style>{{SYNTAX_CSS}}</style>
<style>{{ALERTS_CSS}}</style>
<style id="theme-vars">{{THEME_VARS_CSS}}</style>
<style>{{FONT_CSS}}</style>
{{CUSTOM_CSS}}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css" crossorigin="anonymous">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.js" crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
</head>
<body class="{{BODY_CLASS}}">
<header class="header{{HEADER_CLASS}}">
<div class="header-left">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z"/>
</svg>
<span class="filename" id="filename">{{FILENAME}}</span>
</div>
<div class="header-right">
<div class="theme-controls">
<select class="theme-select" id="theme-select" title="Switch theme">
{{THEME_OPTIONS}}
</select>
<button class="theme-toggle" id="theme-toggle" title="Toggle light/dark">
<svg id="icon-sun" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm0-1.5a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5ZM8 0a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0V.75A.75.75 0 0 1 8 0Zm0 13a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 8 13ZM2.343 2.343a.75.75 0 0 1 1.061 0l1.06 1.061a.75.75 0 0 1-1.06 1.06l-1.06-1.06a.75.75 0 0 1 0-1.06Zm8.193 8.192a.75.75 0 0 1 1.06 0l1.061 1.061a.75.75 0 0 1-1.06 1.061l-1.06-1.061a.75.75 0 0 1 0-1.06ZM0 8a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 8Zm13 0a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 0 1.5h-1.5A.75.75 0 0 1 13 8ZM2.343 13.657a.75.75 0 0 1 0-1.06l1.06-1.061a.75.75 0 0 1 1.061 1.06l-1.06 1.061a.75.75 0 0 1-1.061 0Zm8.193-8.192a.75.75 0 0 1 0-1.061l1.061-1.06a.75.75 0 1 1 1.06 1.06l-1.06 1.06a.75.75 0 0 1-1.06 0Z"/>
</svg>
<svg id="icon-moon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M9.598 1.591a.749.749 0 0 1 .785-.175 7.001 7.001 0 1 1-8.967 8.967.75.75 0 0 1 .961-.96 5.5 5.5 0 0 0 7.046-7.046.75.75 0 0 1 .175-.786Zm1.616 1.945a7 7 0 0 1-7.678 7.678 5.499 5.499 0 1 0 7.678-7.678Z"/>
</svg>
</button>
</div>
<button class="reading-toggle" id="reading-toggle" title="Reading mode (r)">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M0 1.75A.75.75 0 0 1 .75 1h4.253c1.227 0 2.317.59 3 1.501A3.743 3.743 0 0 1 11.006 1h4.245a.75.75 0 0 1 .75.75v10.5a.75.75 0 0 1-.75.75h-4.507a2.25 2.25 0 0 0-1.591.659l-.622.621a.75.75 0 0 1-1.06 0l-.622-.621A2.25 2.25 0 0 0 5.258 13H.75a.75.75 0 0 1-.75-.75Zm7.251 10.324.004-5.073-.002-2.253A2.25 2.25 0 0 0 5.003 2.5H1.5v9h3.757a3.75 3.75 0 0 1 1.994.574ZM8.755 4.75l-.004 7.322a3.752 3.752 0 0 1 1.992-.572H14.5v-9h-3.495a2.25 2.25 0 0 0-2.25 2.25Z"/>
</svg>
</button>
</div>
</header>
<main class="container">
<div class="file-header">
<svg class="file-header-icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25Zm7.47 3.97a.75.75 0 0 1 1.06 0l2 2a.75.75 0 0 1 0 1.06l-2 2a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L10.69 8 9.22 6.53a.75.75 0 0 1 0-1.06ZM6.78 6.53 5.31 8l1.47 1.47a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215l-2-2a.75.75 0 0 1 0-1.06l2-2a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z"/>
</svg>
<span class="file-header-name">{{FILENAME}}</span>
</div>
<article class="markdown-body" id="content">{{CONTENT}}</article>
</main>
<div class="reading-progress" id="reading-progress"></div>
<div class="reading-exit-zone" id="reading-exit-zone"></div>
<div class="reading-exit-bar" id="reading-exit-bar">
<button class="reading-exit-btn" id="reading-exit-btn" title="Exit reading mode (Esc)">
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
<path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z"/>
</svg>
</button>
</div>
<div class="status" id="status"></div>
<script>
// --- State ---
var STATIC_MODE = {{STATIC_MODE}};
var THEME_MODE = '{{THEME_MODE}}';
var activeVariant = '{{ACTIVE_VARIANT}}';
var variants = {{VARIANTS_JSON}};
var KEYBINDINGS = {{KEYBINDINGS_JSON}};
var html = document.documentElement;
var mdBody = document.getElementById('content');
var toggleBtn = document.getElementById('theme-toggle');
var iconSun = document.getElementById('icon-sun');
var iconMoon = document.getElementById('icon-moon');
var themeSelect = document.getElementById('theme-select');
var themeVarsEl = document.getElementById('theme-vars');
var currentWs = null;
// --- Reading Mode ---
var readingToggle = document.getElementById('reading-toggle');
var readingProgress = document.getElementById('reading-progress');
var readingExitZone = document.getElementById('reading-exit-zone');
var readingExitBar = document.getElementById('reading-exit-bar');
var readingExitBtn = document.getElementById('reading-exit-btn');
var isReadingMode = document.body.classList.contains('reading-mode');
function toggleReadingMode(force) {
isReadingMode = typeof force === 'boolean' ? force : !isReadingMode;
document.body.classList.toggle('reading-mode', isReadingMode);
readingProgress.style.display = isReadingMode ? 'block' : 'none';
readingExitZone.style.display = isReadingMode ? 'block' : 'none';
if (isReadingMode) updateProgress();
}
function updateProgress() {
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
var scrollHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
var pct = scrollHeight > 0 ? (scrollTop / scrollHeight) * 100 : 0;
readingProgress.style.width = pct + '%';
}
window.addEventListener('scroll', function() {
if (isReadingMode) updateProgress();
}, { passive: true });
readingToggle.addEventListener('click', function() { toggleReadingMode(); });
readingExitBtn.addEventListener('click', function() { toggleReadingMode(false); });
// Initialize reading mode UI if started via --reading-mode
if (isReadingMode) {
readingProgress.style.display = 'block';
readingExitZone.style.display = 'block';
updateProgress();
}
// --- Keybinding matcher ---
// Uses e.key for plain keys (e.g. "r", "Escape") and e.code for modified
// keys (e.g. "Alt+r") because macOS Option/Alt produces special characters
// in e.key (e.g. Option+r = "®"), making e.key unreliable with modifiers.
function matchesBinding(e, binding) {
if (!binding) return false;
var parts = binding.split('+');
var key = parts[parts.length - 1];
var needAlt = parts.indexOf('Alt') !== -1;
var needCtrl = parts.indexOf('Ctrl') !== -1;
var needShift = parts.indexOf('Shift') !== -1;
var needMeta = parts.indexOf('Meta') !== -1;
var hasModifiers = needAlt || needCtrl || needMeta;
var keyMatches;
if (!hasModifiers) {
keyMatches = e.key === key;
} else if (key.length === 1 && key >= 'a' && key <= 'z') {
keyMatches = e.code === 'Key' + key.toUpperCase();
} else if (key.length === 1 && key >= '0' && key <= '9') {
keyMatches = e.code === 'Digit' + key;
} else {
// Special keys (Escape, etc.) or unknown — fall back to e.key,
// then e.code if that fails
keyMatches = e.key === key || e.code === key;
}
return keyMatches
&& e.altKey === needAlt
&& e.ctrlKey === needCtrl
&& e.shiftKey === needShift
&& e.metaKey === needMeta;
}
document.addEventListener('keydown', function(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT' || e.target.tagName === 'TEXTAREA') return;
if (matchesBinding(e, KEYBINDINGS.toggle_reading)) {
e.preventDefault();
toggleReadingMode();
} else if (matchesBinding(e, KEYBINDINGS.exit_reading) && isReadingMode) {
e.preventDefault();
toggleReadingMode(false);
} else if (matchesBinding(e, KEYBINDINGS.toggle_dark)) {
e.preventDefault();
if (!toggleBtn.classList.contains('disabled')) toggleBtn.click();
} else if (matchesBinding(e, KEYBINDINGS.focus_theme)) {
e.preventDefault();
themeSelect.focus();
}
});
// --- Theme UI ---
function applyVariant(variant) {
activeVariant = variant;
html.setAttribute('data-theme', variant);
mdBody.setAttribute('data-theme', variant);
updateToggleIcon();
}
function updateToggleIcon() {
var isDark = activeVariant === 'dark';
iconSun.style.display = isDark ? 'block' : 'none';
iconMoon.style.display = isDark ? 'none' : 'block';
}
function updateToggleVisibility() {
var canToggle = variants.length === 2 && THEME_MODE === 'toggle';
toggleBtn.classList.toggle('disabled', !canToggle);
}
function updateThemeSelect(themeName) {
themeSelect.value = themeName;
}
// Initial setup
(function() {
// If only one option in select, hide it
if (STATIC_MODE || themeSelect.options.length <= 1) {
themeSelect.style.display = 'none';
}
// Respect OS light/dark preference for dual-variant themes
if (THEME_MODE === 'toggle' && window.matchMedia) {
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
activeVariant = prefersDark ? 'dark' : 'light';
}
applyVariant(activeVariant);
updateToggleVisibility();
// Toggle sends variant change to server
toggleBtn.addEventListener('click', function() {
if (toggleBtn.classList.contains('disabled')) return;
var next = activeVariant === 'dark' ? 'light' : 'dark';
if (STATIC_MODE) {
applyVariant(next);
return;
}
if (!currentWs || currentWs.readyState !== WebSocket.OPEN) return;
currentWs.send(JSON.stringify({ type: 'variant_change', variant: next }));
});
// Dropdown sends theme change to server
if (!STATIC_MODE) {
themeSelect.addEventListener('change', function() {
if (!currentWs || currentWs.readyState !== WebSocket.OPEN) return;
currentWs.send(JSON.stringify({ type: 'theme_change', theme: themeSelect.value }));
});
}
})();
// --- Math rendering via KaTeX ---
function renderMath() {
if (typeof katex === 'undefined') return;
document.querySelectorAll('[data-math-style]').forEach(function(el) {
if (el.hasAttribute('data-math-rendered')) return;
var displayMode = el.getAttribute('data-math-style') === 'display';
var tex = el.textContent;
try {
katex.render(tex, el, { displayMode: displayMode, throwOnError: false });
el.setAttribute('data-math-rendered', '');
} catch (e) { /* leave raw text on error */ }
});
}
// --- Mermaid diagram rendering ---
var mermaidReady = false;
function initMermaid() {
if (typeof mermaid === 'undefined') return;
if (mermaidReady) return;
var theme = activeVariant === 'dark' ? 'dark' : 'default';
mermaid.initialize({ startOnLoad: false, theme: theme });
mermaidReady = true;
}
function renderMermaid() {
if (typeof mermaid === 'undefined') return;
initMermaid();
mermaid.run({ querySelector: 'pre.mermaid' });
}
// Render on initial load
function renderAll() { renderMath(); renderMermaid(); }
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', renderAll);
} else {
setTimeout(renderAll, 0);
}
// --- Scroll synchronization ---
function scrollToLine(targetLine) {
var els = mdBody.querySelectorAll('[data-sourcepos]');
if (!els.length) return;
var best = null;
var lo = 0, hi = els.length - 1;
while (lo <= hi) {
var mid = (lo + hi) >> 1;
var line = parseInt(els[mid].getAttribute('data-sourcepos'), 10);
if (line <= targetLine) {
best = els[mid];
lo = mid + 1;
} else {
hi = mid - 1;
}
}
if (best) {
best.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
// --- Checkbox write-back ---
function enableCheckboxes() {
mdBody.querySelectorAll('input[type="checkbox"][disabled]').forEach(function(cb) {
cb.disabled = false;
cb.addEventListener('change', function(e) {
var li = e.target.closest('li[data-sourcepos]');
if (!li || !currentWs || currentWs.readyState !== WebSocket.OPEN) return;
var line = parseInt(li.getAttribute('data-sourcepos'), 10);
currentWs.send(JSON.stringify({
type: 'checkbox', line: line, checked: e.target.checked
}));
});
});
}
// --- WebSocket ---
if (!STATIC_MODE) (function() {
var status = document.getElementById('status');
var reconnectDelay = 1000;
var maxDelay = 10000;
function showStatus(msg) {
status.textContent = msg;
status.classList.add('visible');
}
function hideStatus() {
status.classList.remove('visible');
}
function connect() {
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
var ws = new WebSocket(proto + '//' + location.host + '/ws');
currentWs = ws;
ws.onopen = function() {
reconnectDelay = 1000;
hideStatus();
};
ws.onmessage = function(event) {
try {
var msg = JSON.parse(event.data);
} catch (e) {
return;
}
switch (msg.type) {
case 'content':
mdBody.innerHTML = msg.html;
renderMath();
renderMermaid();
enableCheckboxes();
break;
case 'theme_update':
// Update CSS variables
themeVarsEl.textContent = msg.css_vars;
// Update theme attribute
if (msg.theme_attr) {
html.setAttribute('data-birta-theme', msg.theme_attr);
} else {
html.removeAttribute('data-birta-theme');
}
// Update variant state
variants = msg.variants;
activeVariant = msg.active_variant;
THEME_MODE = msg.has_toggle ? 'toggle' : 'fixed-' + msg.active_variant;
applyVariant(activeVariant);
updateToggleVisibility();
updateThemeSelect(msg.theme_name);
// Update content HTML (re-rendered with new syntax theme)
mdBody.innerHTML = msg.html;
renderMath();
// Re-init mermaid with correct theme
mermaidReady = false;
renderMermaid();
enableCheckboxes();
break;
case 'scroll':
scrollToLine(msg.line);
break;
}
};
ws.onclose = function() {
currentWs = null;
showStatus('Disconnected \u2014 reconnecting\u2026');
setTimeout(function() {
reconnectDelay = Math.min(reconnectDelay * 2, maxDelay);
connect();
}, reconnectDelay);
};
}
connect();
})();
// Enable checkboxes on initial page load (not in static mode)
if (!STATIC_MODE) enableCheckboxes();
</script>
</body>
</html>