(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 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 mobileNavBtn = document.getElementById('mobileNavBtn'); const themeStylesheet = document.getElementById('themeStylesheet'); const themeHighlightStylesheet = document.getElementById('themeHighlightStylesheet'); let currentFile = null; 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'); localStorage.setItem('webook-sidebar', 'visible'); btnToggleSidebar.textContent = '☰'; } else { sidebar.classList.add('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; } // 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; currentFile = filePath; 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 = ''; articleTitle.textContent = data.title; articleBody.innerHTML = data.html; buildTOC(); renderArticleMeta(data.meta); // 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 renderArticleMeta(meta) { const created = formatDate(meta.created); const modified = formatDate(meta.modified); const wordCount = meta.wordCount; articleMeta.innerHTML = ` 创建时间: ${created} 修改时间: ${modified} 字数统计: ${wordCount.toLocaleString()} `; } 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 = '
当前页面没有可用目录
'; return; } tocPanel.classList.remove('toc-empty'); const items = []; headings.forEach((heading, index) => { const level = Number(heading.tagName.slice(1)); if (!heading.id) { heading.id = slugifyHeading(heading.textContent || 'section', index); } items.push({ id: heading.id, text: heading.textContent.trim(), level }); }); tocNav.innerHTML = items.map((item) => ` `).join(''); } function scrollToHeading(targetId) { if (!targetId) return; const target = articleBody.querySelector(`#${CSS.escape(targetId)}`); if (!target) return; target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } // === Initial Load === if (initialContent) { emptyState.style.display = 'none'; markdownContent.style.display = ''; articleTitle.textContent = initialContent.title; articleBody.innerHTML = initialContent.html; buildTOC(); renderArticleMeta(initialContent.meta); currentFile = defaultFile; if (defaultFile && history.replaceState) { history.replaceState(null, '', '#' + encodeURIComponent(defaultFile)); } } // === Hash-based navigation === function handleHashChange() { const hash = decodeURIComponent(window.location.hash.slice(1)); if (!hash || hash === currentFile) { return; } const fileItem = findNavFileItem(hash); if (fileItem) { loadFile(hash); setActiveFile(fileItem); return; } if (hash.startsWith('toc-')) { scrollToHeading(hash); } } window.addEventListener('hashchange', handleHashChange); // === Event Listeners === btnToggleTheme.addEventListener('click', toggleColorMode); btnToggleSidebar.addEventListener('click', toggleSidebar); mobileNavBtn.addEventListener('click', showMobileSidebar); if (themeSelect) { themeSelect.addEventListener('change', function () { applyTheme(this.value, getStoredColorMode(this.value)); }); } if (tocNav) { tocNav.addEventListener('click', function (e) { const link = e.target.closest('[data-target]'); if (!link) return; e.preventDefault(); scrollToHeading(link.getAttribute('data-target')); }); } // === Keyboard Shortcuts === document.addEventListener('keydown', function (e) { // Ctrl+K or Cmd+K: focus search if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); searchInput.focus(); searchInput.select(); } // Escape: clear search if (e.key === 'Escape' && document.activeElement === searchInput) { searchInput.value = ''; searchInput.dispatchEvent(new Event('input')); searchInput.blur(); } }); // Close mobile sidebar on Escape document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && sidebar.classList.contains('mobile-visible')) { hideMobileSidebar(); } }); })();