<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }} - DeepWiki</title>
<style>
:root {
--bg-color: #0d1117;
--text-color: #c9d1d9;
--link-color: #58a6ff;
--border-color: #30363d;
--sidebar-bg: #161b22;
--code-bg: #1f2428;
--heading-color: #f0f6fc;
}
[data-theme="light"] {
--bg-color: #ffffff;
--text-color: #24292f;
--link-color: #0969da;
--border-color: #d0d7de;
--sidebar-bg: #f6f8fa;
--code-bg: #f6f8fa;
--heading-color: #1f2328;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
background: var(--bg-color);
color: var(--text-color);
line-height: 1.6;
display: flex;
min-height: 100vh;
}
.sidebar {
width: 280px;
background: var(--sidebar-bg);
border-right: 1px solid var(--border-color);
padding: 20px;
position: fixed;
height: 100vh;
overflow-y: auto;
}
.sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border-color);
}
.sidebar-header h2 {
color: var(--heading-color);
font-size: 1.2em;
margin: 0;
padding: 0;
border: none;
}
.header-controls {
display: flex;
align-items: center;
gap: 8px;
}
.chat-link {
background: transparent;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 4px 10px;
font-size: 14px;
color: var(--link-color);
text-decoration: none;
transition: background 0.2s;
}
.chat-link:hover {
background: var(--border-color);
text-decoration: none;
}
.theme-toggle {
background: transparent;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 4px 8px;
cursor: pointer;
font-size: 16px;
transition: background 0.2s;
}
.theme-toggle:hover {
background: var(--border-color);
}
.sidebar-toggle {
display: none;
position: fixed;
top: 15px;
left: 15px;
z-index: 1001;
background: var(--sidebar-bg);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 8px 12px;
font-size: 18px;
cursor: pointer;
color: var(--text-color);
transition: background 0.2s;
}
.sidebar-toggle:hover {
background: var(--border-color);
}
.sidebar h2 {
color: var(--heading-color);
font-size: 1.2em;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border-color);
}
.sidebar ul {
list-style: none;
}
.sidebar li {
margin: 5px 0;
}
.sidebar a {
color: var(--link-color);
text-decoration: none;
display: block;
padding: 5px 10px;
border-radius: 6px;
transition: background 0.2s;
}
.sidebar a:hover {
background: var(--border-color);
}
.sidebar a.active {
background: var(--border-color);
font-weight: 600;
}
.sidebar .section {
margin-top: 20px;
}
.sidebar .section-title {
font-size: 0.85em;
text-transform: uppercase;
color: #8b949e;
margin-bottom: 8px;
letter-spacing: 0.5px;
}
.sidebar .toc-number {
color: #6e7681;
font-size: 0.85em;
margin-right: 6px;
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
}
/* Hide deep hierarchical numbers — tree indentation is sufficient */
.sidebar .toc-nested .toc-nested .toc-number {
display: none;
}
.sidebar .toc-nested {
margin-left: 12px;
border-left: 1px solid var(--border-color);
padding-left: 8px;
overflow: hidden;
transition: max-height 0.2s ease-out;
}
.sidebar .toc-item {
margin: 4px 0;
}
.sidebar .toc-item > a,
.sidebar .toc-item > .toc-header {
display: flex;
align-items: baseline;
}
.sidebar .toc-header {
cursor: pointer;
user-select: none;
}
.sidebar .toc-toggle {
width: 16px;
height: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-right: 4px;
font-size: 10px;
color: #6e7681;
transition: transform 0.2s ease;
flex-shrink: 0;
}
.sidebar .toc-item.collapsed > .toc-header .toc-toggle,
.sidebar .toc-item.collapsed > a .toc-toggle {
transform: rotate(-90deg);
}
.sidebar .toc-item.collapsed > .toc-nested {
max-height: 0 !important;
margin-top: 0;
margin-bottom: 0;
}
.sidebar .toc-parent {
font-weight: 500;
color: var(--heading-color);
margin-top: 12px;
margin-bottom: 4px;
}
.sidebar .toc-parent:first-child {
margin-top: 0;
}
.content {
margin-left: 280px;
padding: 40px 60px;
max-width: 900px;
flex: 1;
}
.content h1 {
color: var(--heading-color);
border-bottom: 1px solid var(--border-color);
padding-bottom: 10px;
margin-bottom: 20px;
}
.content h2, .content h3, .content h4 {
color: var(--heading-color);
margin-top: 24px;
margin-bottom: 16px;
}
.content a {
color: var(--link-color);
text-decoration: none;
}
.content a:hover {
text-decoration: underline;
}
.content code {
background: var(--code-bg);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.9em;
}
.content pre {
background: var(--code-bg);
padding: 16px;
border-radius: 8px;
overflow-x: auto;
margin: 16px 0;
}
.content pre code {
background: none;
padding: 0;
}
.content ul, .content ol {
margin: 16px 0;
padding-left: 24px;
}
.content li {
margin: 8px 0;
}
.content blockquote {
border-left: 4px solid var(--border-color);
padding-left: 16px;
margin: 16px 0;
color: #8b949e;
}
.content table {
border-collapse: collapse;
width: 100%;
margin: 16px 0;
}
.content th, .content td {
border: 1px solid var(--border-color);
padding: 8px 12px;
text-align: left;
}
.content th {
background: var(--sidebar-bg);
}
.breadcrumb {
color: #8b949e;
margin-bottom: 20px;
font-size: 0.9em;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0;
}
.breadcrumb a {
color: var(--link-color);
text-decoration: none;
}
.breadcrumb a:hover {
text-decoration: underline;
}
.breadcrumb .separator {
margin: 0 8px;
color: #6e7681;
}
.breadcrumb .current {
color: var(--text-color);
font-weight: 500;
}
/* Search box styles */
.search-container {
margin-bottom: 20px;
position: relative;
}
.search-input {
width: 100%;
padding: 10px 12px;
background: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-color);
font-size: 14px;
outline: none;
transition: border-color 0.2s;
}
.search-input:focus {
border-color: var(--link-color);
}
.search-input::placeholder {
color: #6e7681;
}
.search-results {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--sidebar-bg);
border: 1px solid var(--border-color);
border-radius: 6px;
margin-top: 4px;
max-height: 400px;
overflow-y: auto;
z-index: 100;
display: none;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
}
.search-results.active {
display: block;
}
.search-filters {
display: flex;
gap: 6px;
margin-top: 8px;
flex-wrap: wrap;
}
.search-filter {
padding: 4px 10px;
font-size: 12px;
background: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: 12px;
cursor: pointer;
color: var(--text-secondary);
transition: all 0.15s;
}
.search-filter:hover {
border-color: var(--link-color);
}
.search-filter.active {
background: var(--link-color);
border-color: var(--link-color);
color: white;
}
.search-result {
padding: 10px 12px;
border-bottom: 1px solid var(--border-color);
cursor: pointer;
transition: background 0.15s;
}
.entity-badge {
display: inline-block;
padding: 2px 6px;
font-size: 10px;
border-radius: 4px;
margin-right: 6px;
font-weight: 500;
text-transform: uppercase;
}
.entity-badge.class { background: #3b82f6; color: white; }
.entity-badge.function { background: #10b981; color: white; }
.entity-badge.method { background: #8b5cf6; color: white; }
.entity-badge.async { background: #f59e0b; color: white; }
.search-result-signature {
font-family: 'SF Mono', Monaco, monospace;
font-size: 12px;
color: var(--text-secondary);
margin-top: 2px;
}
.search-result-raises {
font-size: 11px;
color: #ef4444;
margin-top: 2px;
}
.search-result:last-child {
border-bottom: none;
}
.search-result:hover {
background: var(--border-color);
}
.search-result-title {
color: var(--link-color);
font-weight: 500;
margin-bottom: 2px;
}
.search-result-path {
font-size: 12px;
color: #6e7681;
margin-bottom: 4px;
}
.search-result-snippet {
font-size: 13px;
color: #8b949e;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.search-result-match {
background: rgba(88, 166, 255, 0.2);
color: var(--link-color);
border-radius: 2px;
padding: 0 2px;
}
.search-no-results {
padding: 12px;
color: #8b949e;
text-align: center;
}
/* Loading overlay for lazy page generation */
.lazy-loading-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(13, 17, 23, 0.85);
z-index: 9999;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 16px;
}
.lazy-loading-overlay.active {
display: flex;
}
.lazy-loading-spinner {
width: 40px;
height: 40px;
border: 3px solid var(--border-color);
border-top-color: var(--link-color);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.lazy-loading-text {
color: var(--heading-color);
font-size: 1.1em;
font-weight: 500;
}
.lazy-loading-subtext {
color: #8b949e;
font-size: 0.85em;
}
.mermaid-wrapper {
position: relative;
margin: 16px 0;
}
.mermaid {
background: var(--sidebar-bg);
padding: 20px;
border-radius: 8px;
text-align: center;
overflow-x: auto;
overflow-y: hidden;
}
.mermaid-codemap-link {
display: inline-block;
margin-top: 6px;
padding: 3px 10px;
font-size: 0.78em;
color: var(--link-color);
border: 1px solid var(--border-color);
border-radius: 5px;
background: var(--sidebar-bg);
text-decoration: none;
transition: background 0.15s;
}
.mermaid-codemap-link:hover {
background: var(--border-color);
}
.mermaid svg {
max-width: none;
min-width: min-content;
}
/* Hint for scrollable diagrams */
.mermaid::-webkit-scrollbar {
height: 8px;
}
.mermaid::-webkit-scrollbar-track {
background: var(--border-color);
border-radius: 4px;
}
.mermaid::-webkit-scrollbar-thumb {
background: #666;
border-radius: 4px;
}
@media (max-width: 768px) {
.sidebar-toggle {
display: block;
}
.sidebar {
position: fixed;
width: 280px;
height: 100vh;
transform: translateX(-100%);
transition: transform 0.3s ease;
z-index: 1000;
}
.sidebar.open {
transform: translateX(0);
}
.content {
margin-left: 0;
padding: 20px;
padding-top: 60px;
}
body {
flex-direction: column;
}
}
</style>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark.min.css" id="hljs-theme">
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
</head>
<body>
<div class="lazy-loading-overlay" id="lazy-loading-overlay">
<div class="lazy-loading-spinner"></div>
<div class="lazy-loading-text">Generating documentation...</div>
<div class="lazy-loading-subtext">This page is being created for the first time</div>
</div>
<button id="sidebar-toggle" class="sidebar-toggle" title="Toggle sidebar">☰</button>
<nav class="sidebar">
<div class="sidebar-header">
<h2>DeepWiki</h2>
<div class="header-controls">
<a href="/codemap" class="chat-link" title="Explore code execution flows">Codemap</a>
<a href="/chat" class="chat-link" title="Ask questions about the codebase">Chat</a>
<button id="theme-toggle" class="theme-toggle" title="Toggle theme">🌙</button>
</div>
</div>
<div class="search-container">
<input type="text" class="search-input" id="search-input" placeholder="Search docs..." autocomplete="off">
<div class="search-filters">
<span class="search-filter active" data-filter="all">All</span>
<span class="search-filter" data-filter="page">Pages</span>
<span class="search-filter" data-filter="class">Classes</span>
<span class="search-filter" data-filter="function">Functions</span>
<span class="search-filter" data-filter="method">Methods</span>
</div>
<div class="search-results" id="search-results"></div>
</div>
{% if toc_entries %}
{# Hierarchical numbered TOC #}
<div class="toc">
{% macro render_toc_entry(entry, depth=0) %}
<div class="toc-item {% if entry.children %}toc-parent has-children{% endif %}" data-toc-id="{{ entry.number }}">
{% if entry.path %}
<a href="{{ url_for('view_page', path=entry.path) }}"
class="{{ 'active' if entry.path == current_path else '' }}">
{% if entry.children %}<span class="toc-toggle">▼</span>{% endif %}
<span class="toc-number">{{ entry.number }}</span>
<span>{{ entry.title }}</span>
</a>
{% else %}
<div class="toc-header toc-parent">
{% if entry.children %}<span class="toc-toggle">▼</span>{% endif %}
<span class="toc-number">{{ entry.number }}</span>
<span>{{ entry.title }}</span>
</div>
{% endif %}
{% if entry.children %}
<div class="toc-nested">
{% for child in entry.children %}
{{ render_toc_entry(child, depth + 1) }}
{% endfor %}
</div>
{% endif %}
</div>
{% endmacro %}
{% for entry in toc_entries %}
{{ render_toc_entry(entry) }}
{% endfor %}
</div>
{% else %}
{# Fallback to flat structure #}
<ul>
{% for page in pages %}
<li><a href="{{ url_for('view_page', path=page.path) }}"
class="{{ 'active' if page.path == current_path else '' }}">{{ page.title }}</a></li>
{% endfor %}
</ul>
{% for section_name, section_pages in sections.items() %}
<div class="section">
<div class="section-title">{{ section_name }}</div>
<ul>
{% for page in section_pages %}
<li><a href="{{ url_for('view_page', path=page.path) }}"
class="{{ 'active' if page.path == current_path else '' }}">{{ page.title }}</a></li>
{% endfor %}
</ul>
</div>
{% endfor %}
{% endif %}
</nav>
<main class="content">
{% if breadcrumb %}
<div class="breadcrumb">{{ breadcrumb | safe }}</div>
{% endif %}
{{ content | safe }}
</main>
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js" crossorigin="anonymous"></script>
<script>
// Theme-aware Mermaid configuration
// securityLevel: 'strict' prevents XSS from user-controlled diagram content
var mermaidThemes = {
dark: {
primaryColor: '#238636',
primaryTextColor: '#c9d1d9',
primaryBorderColor: '#30363d',
lineColor: '#8b949e',
secondaryColor: '#161b22',
tertiaryColor: '#0d1117',
background: '#0d1117',
mainBkg: '#161b22',
nodeBorder: '#30363d',
clusterBkg: '#161b22',
clusterBorder: '#30363d',
titleColor: '#c9d1d9',
edgeLabelBackground: '#161b22',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif'
},
light: {
primaryColor: '#2da44e',
primaryTextColor: '#24292f',
primaryBorderColor: '#d0d7de',
lineColor: '#57606a',
secondaryColor: '#f6f8fa',
tertiaryColor: '#ffffff',
background: '#ffffff',
mainBkg: '#f6f8fa',
nodeBorder: '#d0d7de',
clusterBkg: '#f6f8fa',
clusterBorder: '#d0d7de',
titleColor: '#1f2328',
edgeLabelBackground: '#ffffff',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif'
}
};
// Store original diagram sources for re-rendering on theme change
var mermaidSources = [];
function getMermaidThemeCSS(mode) {
if (mode === 'light') {
return '.node rect, .node polygon { fill: #f6f8fa; stroke: #d0d7de; } .edgeLabel { background-color: #ffffff; }';
}
return '.node rect, .node polygon { fill: #161b22; stroke: #30363d; } .edgeLabel { background-color: #161b22; }';
}
function initMermaid(mode) {
mermaid.initialize({
startOnLoad: false,
securityLevel: 'strict',
theme: 'base',
themeVariables: mermaidThemes[mode],
themeCSS: getMermaidThemeCSS(mode)
});
}
function renderMermaidDiagrams() {
var currentTheme = document.documentElement.getAttribute('data-theme') || 'dark';
initMermaid(currentTheme);
// On first run, extract sources from code blocks
if (mermaidSources.length === 0) {
var codeBlocks = document.querySelectorAll('pre code.language-mermaid');
codeBlocks.forEach(function(codeBlock) {
mermaidSources.push(codeBlock.textContent);
var pre = codeBlock.parentElement;
var mermaidDiv = document.createElement('div');
mermaidDiv.className = 'mermaid';
mermaidDiv.textContent = codeBlock.textContent;
pre.parentNode.replaceChild(mermaidDiv, pre);
});
} else {
// Re-render: replace rendered SVGs with fresh source text
var containers = document.querySelectorAll('.mermaid');
containers.forEach(function(container, i) {
if (i < mermaidSources.length) {
container.removeAttribute('data-processed');
container.textContent = mermaidSources[i];
}
});
}
mermaid.run();
}
// After Mermaid renders, inject "Open in Codemap" links below each diagram
function addCodemapLinks() {
document.querySelectorAll('.mermaid').forEach(function(el) {
if (el.parentElement && el.parentElement.classList.contains('mermaid-wrapper')) return;
// Find a subject from the nearest preceding heading
var subject = '';
var prev = el.previousElementSibling;
while (prev) {
if (/^H[1-6]$/.test(prev.tagName)) {
subject = prev.textContent.trim();
break;
}
prev = prev.previousElementSibling;
}
if (!subject) subject = document.title || '';
// Wrap in container
var wrapper = document.createElement('div');
wrapper.className = 'mermaid-wrapper';
el.parentNode.insertBefore(wrapper, el);
wrapper.appendChild(el);
// Add link
var link = document.createElement('a');
link.className = 'mermaid-codemap-link';
link.href = '/codemap?query=' + encodeURIComponent(subject);
link.textContent = '\u{1f5fa} Open in Codemap';
link.title = 'Explore this topic in the interactive codemap';
wrapper.appendChild(link);
});
}
// Initial render on DOM ready
document.addEventListener('DOMContentLoaded', function() {
renderMermaidDiagrams();
// Mermaid.run() is async; inject links after a brief delay
setTimeout(addCodemapLinks, 500);
// Glossary expand/collapse — nh3 strips onclick so we bind here
document.querySelectorAll('.content a[href="#"]').forEach(function(a) {
var text = a.textContent.trim();
if (text === 'Expand All') {
a.addEventListener('click', function(e) {
e.preventDefault();
document.querySelectorAll('details').forEach(function(d) { d.open = true; });
});
} else if (text === 'Collapse All') {
a.addEventListener('click', function(e) {
e.preventDefault();
document.querySelectorAll('details').forEach(function(d) { d.open = false; });
});
}
});
});
</script>
<script>
// Search functionality
(function() {
let searchIndex = null;
let currentFilter = 'all';
const searchInput = document.getElementById('search-input');
const searchResults = document.getElementById('search-results');
const filterButtons = document.querySelectorAll('.search-filter');
// Load search index
fetch('/search.json')
.then(response => response.json())
.then(data => {
// Handle both old (array) and new (object) formats
if (Array.isArray(data)) {
searchIndex = { pages: data, entities: [] };
} else {
searchIndex = data;
}
console.log('Search index loaded:', searchIndex.pages?.length, 'pages,', searchIndex.entities?.length, 'entities');
})
.catch(err => console.error('Search index failed to load:', err));
// Filter button click handlers
filterButtons.forEach(btn => {
btn.addEventListener('click', () => {
filterButtons.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentFilter = btn.dataset.filter;
// Re-run search if there's a query
if (searchInput.value.trim().length >= 2) {
search(searchInput.value.trim());
}
});
});
// Simple fuzzy match - checks if all query chars appear in order
function fuzzyMatch(query, text) {
query = query.toLowerCase();
text = text.toLowerCase();
let qi = 0;
for (let ti = 0; ti < text.length && qi < query.length; ti++) {
if (text[ti] === query[qi]) qi++;
}
return qi === query.length;
}
// Score a page result based on match quality
function scorePageResult(query, entry) {
query = query.toLowerCase();
let score = 0;
// Title match (highest priority)
if (entry.title.toLowerCase().includes(query)) {
score += 100;
if (entry.title.toLowerCase().startsWith(query)) score += 50;
} else if (fuzzyMatch(query, entry.title)) {
score += 30;
}
// Heading match
for (const heading of entry.headings || []) {
if (heading.toLowerCase().includes(query)) {
score += 40;
break;
}
}
// Code terms match (class names, functions)
for (const term of entry.terms || []) {
if (term.toLowerCase().includes(query)) {
score += 60;
break;
} else if (fuzzyMatch(query, term)) {
score += 20;
break;
}
}
// Snippet match
if (entry.snippet && entry.snippet.toLowerCase().includes(query)) {
score += 10;
}
return score;
}
// Score an entity result based on match quality
function scoreEntityResult(query, entry) {
query = query.toLowerCase();
let score = 0;
// Name match (highest priority for entities)
const name = entry.name.toLowerCase();
const displayName = entry.display_name.toLowerCase();
if (name === query || displayName === query) {
score += 200; // Exact match
} else if (name.startsWith(query) || displayName.startsWith(query)) {
score += 150;
} else if (name.includes(query) || displayName.includes(query)) {
score += 100;
} else if (fuzzyMatch(query, name) || fuzzyMatch(query, displayName)) {
score += 50;
}
// Keywords match
for (const keyword of entry.keywords || []) {
if (keyword.toLowerCase().includes(query)) {
score += 30;
break;
}
}
// Description match
if (entry.description && entry.description.toLowerCase().includes(query)) {
score += 15;
}
return score;
}
// Perform search
function search(query) {
console.log('Search called with:', query, 'Index loaded:', !!searchIndex);
if (!searchIndex || query.length < 2) {
searchResults.classList.remove('active');
return;
}
let results = [];
// Search pages if filter allows
if (currentFilter === 'all' || currentFilter === 'page') {
const pageResults = (searchIndex.pages || [])
.map(entry => ({
entry: { ...entry, type: 'page' },
score: scorePageResult(query, entry)
}))
.filter(r => r.score > 0);
results = results.concat(pageResults);
}
// Search entities if filter allows
if (currentFilter === 'all' || ['class', 'function', 'method'].includes(currentFilter)) {
const entities = searchIndex.entities || [];
const filteredEntities = currentFilter === 'all'
? entities
: entities.filter(e => e.entity_type === currentFilter);
const entityResults = filteredEntities
.map(entry => ({
entry,
score: scoreEntityResult(query, entry)
}))
.filter(r => r.score > 0);
results = results.concat(entityResults);
}
// Sort by score and limit results
results = results
.sort((a, b) => b.score - a.score)
.slice(0, 10);
if (results.length === 0) {
searchResults.innerHTML = '<div class="search-no-results">No results found</div>';
searchResults.classList.add('active');
return;
}
const html = results.map(r => renderResult(r.entry)).join('');
searchResults.innerHTML = html;
searchResults.classList.add('active');
// Add click handlers with loading overlay for lazy generation
searchResults.querySelectorAll('.search-result').forEach(el => {
el.addEventListener('click', () => {
const targetUrl = '/wiki/' + el.dataset.path;
const overlay = document.getElementById('lazy-loading-overlay');
// Show loading overlay and navigate. The browser keeps
// the current page visible (with overlay) until the
// server responds, providing visual feedback during
// lazy page generation.
overlay.classList.add('active');
searchResults.classList.remove('active');
window.location.href = targetUrl;
});
});
}
// Render a search result
function renderResult(entry) {
if (entry.type === 'page') {
return `
<div class="search-result" data-path="${entry.path}">
<div class="search-result-title">${escapeHtml(entry.title)}</div>
<div class="search-result-path">${escapeHtml(entry.path)}</div>
<div class="search-result-snippet">${escapeHtml(entry.snippet || '')}</div>
</div>
`;
} else {
// Entity result
const badges = [];
badges.push(`<span class="entity-badge ${entry.entity_type}">${entry.entity_type}</span>`);
if (entry.is_async) {
badges.push('<span class="entity-badge async">async</span>');
}
const raisesHtml = entry.raises && entry.raises.length > 0
? `<div class="search-result-raises">Raises: ${escapeHtml(entry.raises.join(', '))}</div>`
: '';
const sigHtml = entry.signature
? `<div class="search-result-signature">${escapeHtml(entry.signature)}</div>`
: '';
return `
<div class="search-result" data-path="${entry.path}">
<div class="search-result-title">
${badges.join('')}${escapeHtml(entry.display_name)}
</div>
<div class="search-result-path">${escapeHtml(entry.file)}</div>
${sigHtml}
${entry.description ? `<div class="search-result-snippet">${escapeHtml(entry.description)}</div>` : ''}
${raisesHtml}
</div>
`;
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Event listeners
let debounceTimer;
searchInput.addEventListener('input', (e) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => search(e.target.value.trim()), 150);
});
searchInput.addEventListener('focus', () => {
if (searchInput.value.trim().length >= 2) {
search(searchInput.value.trim());
}
});
// Close results when clicking outside
document.addEventListener('click', (e) => {
if (!e.target.closest('.search-container')) {
searchResults.classList.remove('active');
}
});
// Keyboard navigation
searchInput.addEventListener('keydown', (e) => {
const results = searchResults.querySelectorAll('.search-result');
const active = searchResults.querySelector('.search-result:hover, .search-result.active');
let index = Array.from(results).indexOf(active);
if (e.key === 'ArrowDown') {
e.preventDefault();
index = Math.min(index + 1, results.length - 1);
results.forEach((r, i) => r.classList.toggle('active', i === index));
if (results[index]) results[index].scrollIntoView({ block: 'nearest' });
} else if (e.key === 'ArrowUp') {
e.preventDefault();
index = Math.max(index - 1, 0);
results.forEach((r, i) => r.classList.toggle('active', i === index));
if (results[index]) results[index].scrollIntoView({ block: 'nearest' });
} else if (e.key === 'Enter' && results.length > 0) {
e.preventDefault();
// If no result selected, click the first one
const targetIndex = index >= 0 ? index : 0;
results[targetIndex].click();
} else if (e.key === 'Escape') {
searchResults.classList.remove('active');
searchInput.blur();
}
});
})();
</script>
<script>
// Theme toggle
(function() {
const themeToggle = document.getElementById('theme-toggle');
const hljsTheme = document.getElementById('hljs-theme');
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('deepwiki-theme', theme);
themeToggle.innerHTML = theme === 'dark' ? '🌙' : '☀';
// Switch highlight.js theme
if (hljsTheme) {
hljsTheme.href = theme === 'dark'
? 'https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark.min.css'
: 'https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github.min.css';
}
// Re-render Mermaid diagrams with new theme colors
if (typeof renderMermaidDiagrams === 'function' && mermaidSources.length > 0) {
renderMermaidDiagrams();
}
}
// Load saved theme or default to dark
const savedTheme = localStorage.getItem('deepwiki-theme') || 'dark';
setTheme(savedTheme);
themeToggle.addEventListener('click', () => {
const current = document.documentElement.getAttribute('data-theme') || 'dark';
setTheme(current === 'dark' ? 'light' : 'dark');
});
})();
// Sidebar toggle for mobile
(function() {
const sidebarToggle = document.getElementById('sidebar-toggle');
const sidebar = document.querySelector('.sidebar');
sidebarToggle.addEventListener('click', (e) => {
e.stopPropagation();
sidebar.classList.toggle('open');
});
// Close sidebar when clicking outside on mobile
document.addEventListener('click', (e) => {
if (window.innerWidth <= 768 &&
!e.target.closest('.sidebar') &&
!e.target.closest('.sidebar-toggle')) {
sidebar.classList.remove('open');
}
});
})();
// TOC collapse/expand functionality
(function() {
const STORAGE_KEY = 'deepwiki-toc-collapsed';
// Load collapsed state from localStorage
function getCollapsedState() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
} catch {
return {};
}
}
// Save collapsed state to localStorage
function saveCollapsedState(state) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
// Initialize TOC items
const tocItems = document.querySelectorAll('.toc-item.has-children');
const collapsedState = getCollapsedState();
tocItems.forEach(item => {
const tocId = item.dataset.tocId;
const nested = item.querySelector(':scope > .toc-nested');
const toggle = item.querySelector(':scope > a .toc-toggle, :scope > .toc-header .toc-toggle');
if (!nested || !toggle) return;
// Set initial height for animation
nested.style.maxHeight = nested.scrollHeight + 'px';
// Restore collapsed state
if (collapsedState[tocId]) {
item.classList.add('collapsed');
}
// Handle toggle click
toggle.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const isCollapsed = item.classList.toggle('collapsed');
// Update stored state
const state = getCollapsedState();
if (isCollapsed) {
state[tocId] = true;
} else {
delete state[tocId];
// Reset max-height for expansion
nested.style.maxHeight = nested.scrollHeight + 'px';
}
saveCollapsedState(state);
});
});
// Expand parent items of the active page
const activeLink = document.querySelector('.toc a.active');
if (activeLink) {
let parent = activeLink.closest('.toc-item');
while (parent) {
parent.classList.remove('collapsed');
const nested = parent.querySelector(':scope > .toc-nested');
if (nested) {
nested.style.maxHeight = nested.scrollHeight + 'px';
}
parent = parent.parentElement.closest('.toc-item');
}
}
})();
// Syntax highlighting
if (typeof hljs !== 'undefined') {
hljs.highlightAll();
}
</script>
</body>
</html>