(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 btnToggleSidebarToolbar = document.getElementById('btnToggleSidebarToolbar'); const btnToggleTheme = document.getElementById('btnToggleTheme'); const btnToggleToc = document.getElementById('btnToggleToc'); 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 articleLayout = document.querySelector('.article-layout'); 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'); const sidebarToggleButtons = [btnToggleSidebarToolbar].filter(Boolean); function createSidebarToggleIcon() { return ''; } function createThemeToggleIcon(mode) { return mode === 'dark' ? '' : ''; } function createTocToggleIcon(collapsed) { return collapsed ? '' : ''; } function refreshLucideIcons() { if (window.lucide && typeof window.lucide.createIcons === 'function') { window.lucide.createIcons(); } } 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.innerHTML = createThemeToggleIcon(mode); btnToggleTheme.title = mode === 'dark' ? '切换到浅色模式' : '切换到深色模式'; btnToggleTheme.setAttribute('aria-label', btnToggleTheme.title); refreshLucideIcons(); } 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'); } else { sidebar.classList.add('collapsed'); app.classList.add('sidebar-collapsed'); localStorage.setItem('webook-sidebar', 'hidden'); } const title = visible ? '隐藏侧边栏' : '显示侧边栏'; const iconMarkup = createSidebarToggleIcon(); sidebarToggleButtons.forEach((button) => { button.innerHTML = iconMarkup; button.title = title; button.setAttribute('aria-label', title); }); refreshLucideIcons(); } 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'); } if (mobileNavBtn) { mobileNavBtn.innerHTML = createSidebarToggleIcon(); refreshLucideIcons(); } // === TOC Toggle === function getTocCollapsed() { return localStorage.getItem('webook-toc') === 'collapsed'; } function setTocCollapsed(collapsed) { if (!tocPanel || !articleLayout || !btnToggleToc) return; tocPanel.classList.toggle('is-collapsed', collapsed); articleLayout.classList.toggle('toc-collapsed', collapsed); localStorage.setItem('webook-toc', collapsed ? 'collapsed' : 'expanded'); const title = collapsed ? '展开目录' : '收起目录'; btnToggleToc.innerHTML = createTocToggleIcon(collapsed); btnToggleToc.title = title; btnToggleToc.setAttribute('aria-label', title); refreshLucideIcons(); } function toggleToc() { setTocCollapsed(!getTocCollapsed()); } setTocCollapsed(getTocCollapsed()); 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 = ['Docs']; 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(`${escapeHtml(part)}`); }); if (fileTitle) { segments.push('/'); segments.push(`${escapeHtml(fileTitle)}`); } 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 = ` 创建时间:${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; enhanceCodeBlocks(); buildTOC(); renderArticleMeta(initialContent.meta); currentFile = defaultFile; renderToolbarBreadcrumb(defaultFile, initialContent.title); 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); handleHashChange(); // === Event Listeners === btnToggleTheme.addEventListener('click', toggleColorMode); sidebarToggleButtons.forEach((button) => { button.addEventListener('click', toggleSidebar); }); mobileNavBtn.addEventListener('click', showMobileSidebar); if (btnToggleToc) { btnToggleToc.addEventListener('click', toggleToc); } 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')); }); } if (articleBody) { articleBody.addEventListener('click', function (e) { const button = e.target.closest('.code-copy-btn'); if (!button) return; e.preventDefault(); copyCodeFromButton(button); }); } // === 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(); } }); })();