(function () { 'use strict'; const DATA = window.__WEBOOK__; const navTree = DATA.nav || []; const themes = Array.isArray(DATA.themes) ? DATA.themes : []; const defaultFile = DATA.defaultFile; const initialContent = DATA.initialContent; // === DOM Elements === const sidebarNav = document.getElementById('sidebarNav'); const searchInput = document.getElementById('searchInput'); const btnToggleSidebar = document.getElementById('btnToggleSidebar'); const btnToggleTheme = document.getElementById('btnToggleTheme'); const themeSelect = document.getElementById('themeSelect'); const toolbarBreadcrumb = document.getElementById('toolbarBreadcrumb'); const contentBody = document.getElementById('contentBody'); const emptyState = document.getElementById('emptyState'); const markdownContent = document.getElementById('markdownContent'); const articleTitle = document.getElementById('articleTitle'); const articleMeta = document.getElementById('articleMeta'); const articleBody = document.getElementById('articleBody'); const tocNav = document.getElementById('tocNav'); const tocPanel = document.getElementById('tocPanel'); const loadingBar = document.getElementById('loadingBar'); const sidebar = document.getElementById('sidebar'); const app = document.getElementById('app'); const mobileNavBtn = document.getElementById('mobileNavBtn'); const themeStylesheet = document.getElementById('themeStylesheet'); const themeHighlightStylesheet = document.getElementById('themeHighlightStylesheet'); let currentFile = null; const titleByPath = buildTitleIndex(navTree); const fallbackTheme = themes[0] || { id: 'default', label: 'Default', defaultMode: 'light', stylesheet: '/_webook/themes/default/theme.css', highlight: { light: '/_webook/themes/default/highlight-light.css', dark: '/_webook/themes/default/highlight-dark.css' } }; // === Theme === function getThemeConfig(themeId) { return themes.find((item) => item.id === themeId) || fallbackTheme; } function getStoredThemeId() { const stored = localStorage.getItem('webook-theme-id'); if (stored && themes.some((item) => item.id === stored)) { return stored; } return DATA.initialThemeId || fallbackTheme.id; } function getStoredColorMode(themeId) { const stored = localStorage.getItem('webook-color-mode'); if (stored === 'light' || stored === 'dark') { return stored; } const legacy = localStorage.getItem('webook-theme'); if (legacy === 'light' || legacy === 'dark') { return legacy; } const theme = getThemeConfig(themeId); return theme.defaultMode === 'dark' ? 'dark' : 'light'; } function renderThemeOptions() { if (!themeSelect) return; themeSelect.innerHTML = themes.map((item) => ( `` )).join(''); } function applyTheme(themeId, colorMode) { const theme = getThemeConfig(themeId); const mode = colorMode === 'dark' ? 'dark' : 'light'; document.documentElement.setAttribute('data-theme-id', theme.id); document.documentElement.setAttribute('data-color-mode', mode); localStorage.setItem('webook-theme-id', theme.id); localStorage.setItem('webook-color-mode', mode); localStorage.setItem('webook-theme', mode); if (themeStylesheet) { themeStylesheet.href = theme.stylesheet; } if (themeHighlightStylesheet) { themeHighlightStylesheet.href = mode === 'dark' ? theme.highlight.dark : theme.highlight.light; } if (themeSelect) { themeSelect.value = theme.id; } btnToggleTheme.textContent = mode === 'dark' ? '☀︎' : '☾'; btnToggleTheme.title = mode === 'dark' ? '切换到浅色模式' : '切换到深色模式'; } function toggleColorMode() { const currentThemeId = getStoredThemeId(); const currentMode = getStoredColorMode(currentThemeId); applyTheme(currentThemeId, currentMode === 'dark' ? 'light' : 'dark'); } renderThemeOptions(); applyTheme(getStoredThemeId(), getStoredColorMode(getStoredThemeId())); // === Sidebar Toggle === function getSidebarVisible() { return localStorage.getItem('webook-sidebar') !== 'hidden'; } function setSidebarVisible(visible) { if (visible) { sidebar.classList.remove('collapsed'); app.classList.remove('sidebar-collapsed'); localStorage.setItem('webook-sidebar', 'visible'); btnToggleSidebar.textContent = '☰'; } else { sidebar.classList.add('collapsed'); app.classList.add('sidebar-collapsed'); localStorage.setItem('webook-sidebar', 'hidden'); btnToggleSidebar.textContent = '☷'; } } function toggleSidebar() { setSidebarVisible(sidebar.classList.contains('collapsed')); } // Initialize sidebar state setSidebarVisible(getSidebarVisible()); // === Mobile Sidebar === let sidebarOverlay = null; function createOverlay() { if (sidebarOverlay) return; sidebarOverlay = document.createElement('div'); sidebarOverlay.className = 'sidebar-overlay'; sidebarOverlay.addEventListener('click', hideMobileSidebar); document.body.appendChild(sidebarOverlay); } function showMobileSidebar() { createOverlay(); sidebar.classList.add('mobile-visible'); sidebar.classList.remove('collapsed'); sidebarOverlay.classList.add('visible'); } function hideMobileSidebar() { sidebar.classList.remove('mobile-visible'); if (sidebarOverlay) { sidebarOverlay.classList.remove('visible'); } // Restore collapsed state if needed if (!getSidebarVisible()) { sidebar.classList.add('collapsed'); } } // === Navigation Rendering === function renderNavTree(items, level = 0) { if (!items || items.length === 0) return ''; let html = '
'; return html; } function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } function buildTitleIndex(items, index = {}) { if (!Array.isArray(items)) return index; items.forEach((item) => { if (item.type === 'file') { const title = item.meta && item.meta.title ? item.meta.title : item.name.replace(/\.(md|markdown)$/i, ''); index[item.path] = title; return; } if (item.type === 'dir' && Array.isArray(item.children)) { buildTitleIndex(item.children, index); } }); return index; } function getFileDisplayTitle(filePath, preferredTitle) { if (preferredTitle) return preferredTitle; if (titleByPath[filePath]) return titleByPath[filePath]; if (!filePath) return ''; const fileName = filePath.split('/').pop() || filePath; return fileName.replace(/\.(md|markdown)$/i, ''); } function renderToolbarBreadcrumb(filePath, preferredTitle) { if (!toolbarBreadcrumb) return; const segments = ['']; const parts = typeof filePath === 'string' && filePath ? filePath.split('/').filter(Boolean) : []; if (!parts.length) { toolbarBreadcrumb.innerHTML = segments.join(''); return; } const directoryParts = parts.slice(0, -1); const fileTitle = getFileDisplayTitle(filePath, preferredTitle); directoryParts.forEach((part) => { segments.push(''); segments.push(``); }); if (fileTitle) { segments.push(''); segments.push(``); } toolbarBreadcrumb.innerHTML = segments.join(''); } // Render nav sidebarNav.innerHTML = renderNavTree(navTree); // === Navigation Events === sidebarNav.addEventListener('click', function (e) { // Directory toggle const dirHeader = e.target.closest('.nav-dir-header'); if (dirHeader) { const dirItem = dirHeader.parentElement; dirItem.classList.toggle('collapsed'); return; } // File click const fileItem = e.target.closest('.nav-file'); if (fileItem) { const filePath = fileItem.getAttribute('data-file-path'); loadFile(filePath); setActiveFile(fileItem); // On mobile, hide sidebar after selection if (window.innerWidth <= 768) { hideMobileSidebar(); } } }); function setActiveFile(fileItem) { const prev = sidebarNav.querySelector('.nav-file.active'); if (prev) prev.classList.remove('active'); if (fileItem) fileItem.classList.add('active'); } function findNavFileItem(filePath) { return sidebarNav.querySelector(`[data-file-path="${CSS.escape(filePath)}"]`); } // === Search === searchInput.addEventListener('input', function () { const query = this.value.trim().toLowerCase(); if (!query) { // Show all sidebarNav.querySelectorAll('.nav-hidden').forEach(el => el.classList.remove('nav-hidden')); removeEmptyMessage(); return; } let hasVisible = false; // Process directories const dirs = sidebarNav.querySelectorAll('.nav-dir'); dirs.forEach(dir => { let dirHasVisible = false; const files = dir.querySelectorAll('.nav-file'); files.forEach(file => { const name = file.querySelector('.nav-file-name').textContent.toLowerCase(); if (name.includes(query)) { file.classList.remove('nav-hidden'); dirHasVisible = true; } else { file.classList.add('nav-hidden'); } }); // Show/hide directory if (dirHasVisible) { dir.classList.remove('nav-hidden', 'collapsed'); hasVisible = true; } else { dir.classList.add('nav-hidden'); } }); // Root-level files const rootFiles = sidebarNav.querySelectorAll(':scope > .nav-tree > .nav-file'); rootFiles.forEach(file => { const name = file.querySelector('.nav-file-name').textContent.toLowerCase(); if (name.includes(query)) { file.classList.remove('nav-hidden'); hasVisible = true; } else { file.classList.add('nav-hidden'); } }); if (!hasVisible) { showEmptyMessage('未找到匹配的文件'); } else { removeEmptyMessage(); } }); function showEmptyMessage(msg) { removeEmptyMessage(); const div = document.createElement('div'); div.className = 'nav-empty'; div.id = 'navEmptyMsg'; div.textContent = msg; sidebarNav.appendChild(div); } function removeEmptyMessage() { const existing = document.getElementById('navEmptyMsg'); if (existing) existing.remove(); } // === File Loading === async function loadFile(filePath) { if (currentFile === filePath) return; showLoading(); try { const resp = await fetch('/api/file?path=' + encodeURIComponent(filePath)); const json = await resp.json(); if (!json.success) { throw new Error(json.error || 'Failed to load file'); } const data = json.data; // Update content emptyState.style.display = 'none'; markdownContent.style.display = ''; currentFile = filePath; articleTitle.textContent = data.title; articleBody.innerHTML = data.html; enhanceCodeBlocks(); buildTOC(); renderArticleMeta(data.meta); renderToolbarBreadcrumb(filePath, data.title); // Scroll to top contentBody.scrollTop = 0; // Update URL hash if (history.pushState) { history.pushState(null, '', '#' + encodeURIComponent(filePath)); } // Apply syntax highlighting to code blocks in content applyHighlighting(); } catch (err) { emptyState.style.display = 'flex'; emptyState.querySelector('p').textContent = '加载失败: ' + err.message; markdownContent.style.display = 'none'; } finally { hideLoading(); } } function showLoading() { loadingBar.classList.add('visible'); loadingBar.classList.remove('done'); } function hideLoading() { loadingBar.classList.add('done'); setTimeout(() => { loadingBar.classList.remove('visible', 'done'); }, 350); } function formatDate(isoStr) { if (!isoStr) return '-'; const d = new Date(isoStr); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); const h = String(d.getHours()).padStart(2, '0'); const min = String(d.getMinutes()).padStart(2, '0'); return `${y}-${m}-${day} ${h}:${min}`; } function applyHighlighting() { // highlight.js is loaded via CDN or bundled, but we handle it server-side. // If any code blocks need client-side highlighting, do it here. // For now, server-side rendering handles highlighting. } function enhanceCodeBlocks() { const blocks = articleBody.querySelectorAll('pre.hljs'); blocks.forEach((block) => { if (block.parentElement && block.parentElement.classList.contains('code-block-shell')) return; const shell = document.createElement('div'); shell.className = 'code-block-shell'; block.parentNode.insertBefore(shell, block); shell.appendChild(block); const button = document.createElement('button'); button.type = 'button'; button.className = 'code-copy-btn'; button.textContent = '复制'; button.setAttribute('aria-label', '复制代码'); shell.appendChild(button); }); } async function writeClipboardText(text) { if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text); return true; } const textarea = document.createElement('textarea'); textarea.value = text; textarea.setAttribute('readonly', ''); textarea.style.position = 'fixed'; textarea.style.top = '0'; textarea.style.left = '-9999px'; document.body.appendChild(textarea); textarea.focus(); textarea.select(); let copied = false; try { copied = document.execCommand('copy'); } finally { textarea.remove(); } if (!copied) { throw new Error('copy command failed'); } return true; } async function copyCodeFromButton(button) { const shell = button.closest('.code-block-shell'); const block = shell ? shell.querySelector('pre') : null; const code = block ? block.querySelector('code') : null; if (!code) return; const text = code.textContent || ''; try { await writeClipboardText(text); button.textContent = '已复制'; } catch (_) { button.textContent = '复制失败'; } window.setTimeout(() => { if (button.isConnected) { button.textContent = '复制'; } }, 1600); } function renderArticleMeta(meta) { const created = formatDate(meta.created); const modified = formatDate(meta.modified); const wordCount = meta.wordCount; articleMeta.innerHTML = ` `; } function slugifyHeading(text, index) { return 'toc-' + text .toLowerCase() .replace(/['"`]/g, '') .replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-') .replace(/^-+|-+$/g, '') + '-' + index; } function buildTOC() { if (!tocNav || !tocPanel) return; const headings = articleBody.querySelectorAll('h2, h3, h4'); if (!headings.length) { tocPanel.classList.add('toc-empty'); tocNav.innerHTML = '