/** * app.js — Main logic for sunvpy static documentation site. * Handles routing, markdown rendering, navigation, TOC, and theme. */ (function () { "use strict"; // --- State --- let navData = null; let flatPages = []; // ordered array of all slugs let currentPage = null; let isDark = false; let mermaidInitialized = false; // --- DOM refs --- const sidebarNav = document.getElementById("sidebarNav"); const article = document.getElementById("article"); const pageNav = document.getElementById("pageNav"); const tocNav = document.getElementById("tocNav"); const contentEl = document.getElementById("content"); // --- Init --- async function init() { // Theme const saved = localStorage.getItem("sunvpy-theme"); if (saved === "dark" || (!saved && window.matchMedia("(prefers-color-scheme: dark)").matches)) { setTheme(true, false); } // Load nav try { const resp = await fetch("data/nav.json"); navData = await resp.json(); } catch (e) { article.innerHTML = '
无法加载导航数据。请通过 HTTP 服务器访问(如 python -m http.server)。
'; return; } // Build flat page list flatPages = []; navData.sections.forEach(function (section) { if (section.pages) { section.pages.forEach(function (p) { flatPages.push(p.slug); }); } if (section.groups) { section.groups.forEach(function (group) { group.pages.forEach(function (p) { flatPages.push(p.slug); }); }); } }); // Build sidebar renderSidebar(); // Route handleRoute(); window.addEventListener("hashchange", handleRoute); // Theme toggle buttons document.getElementById("themeToggle").addEventListener("click", function () { setTheme(!isDark, true); }); document.getElementById("themeToggleMobile").addEventListener("click", function () { setTheme(!isDark, true); }); // Mobile sidebar document.getElementById("hamburger").addEventListener("click", toggleMobileSidebar); document.getElementById("sidebarBackdrop").addEventListener("click", closeMobileSidebar); } // --- Theme --- function setTheme(dark, save) { isDark = dark; document.documentElement.setAttribute("data-theme", dark ? "dark" : "light"); const lightSheet = document.getElementById("hljs-light-theme"); const darkSheet = document.getElementById("hljs-dark-theme"); lightSheet.disabled = dark; darkSheet.disabled = !dark; if (save) localStorage.setItem("sunvpy-theme", dark ? "dark" : "light"); // Re-render mermaid if already initialized if (mermaidInitialized && currentPage) { rerenderMermaid(); } } // --- Sidebar --- function renderSidebar() { var html = ""; navData.sections.forEach(function (section) { var isActive = currentPage && isSectionActive(section); html += '"; }); sidebarNav.innerHTML = html; } function renderPageLink(page) { var active = currentPage === page.slug ? " active" : ""; var levelClass = page.level ? page.level.toLowerCase() : ""; var badge = page.level ? ' ' + escHtml(page.level.charAt(0)) + "" : ""; return '' + escHtml(page.title) + badge + ""; } function isSectionActive(section) { if (!currentPage) return false; if (section.pages) { for (var i = 0; i < section.pages.length; i++) { if (section.pages[i].slug === currentPage) return true; } } if (section.groups) { for (var g = 0; g < section.groups.length; g++) { for (var j = 0; j < section.groups[g].pages.length; j++) { if (section.groups[g].pages[j].slug === currentPage) return true; } } } return false; } // Expose toggleSection globally for onclick window.toggleSection = function (header) { header.parentElement.classList.toggle("collapsed"); }; // --- Routing --- function handleRoute() { var hash = window.location.hash.slice(1) || flatPages[0]; if (!hash || hash === currentPage) return; loadPage(hash); } // --- Load & Render Page --- async function loadPage(slug) { currentPage = slug; // Update sidebar active state renderSidebar(); // Scroll content to top contentEl.scrollTop = 0; // Show loading article.innerHTML = '
加载中...
'; tocNav.innerHTML = ""; pageNav.innerHTML = ""; // Fetch markdown var md; try { var resp = await fetch("content/" + slug + ".md"); if (!resp.ok) throw new Error(resp.status); md = await resp.text(); } catch (e) { article.innerHTML = '
无法加载页面: ' + escHtml(slug) + "
"; return; } // Pre-process: transform cross-doc links md = md.replace(/\]\((\d+-[\w-]+)\)/g, function (match, linkSlug) { // Only transform if it matches a known page slug if (flatPages.indexOf(linkSlug) !== -1) { return "](#" + linkSlug + ")"; } return match; }); // Configure marked var renderer = new marked.Renderer(); // Custom paragraph handler for "Sources:" lines renderer.paragraph = function (obj) { var text = typeof obj === "object" ? obj.text : obj; if (text && (text.indexOf("Sources:") === 0 || text.indexOf("Sources: [") === 0)) { return '
' + text + "
"; } return "

" + text + "

"; }; // Add IDs to headings var headingIndex = {}; renderer.heading = function (text, depth) { // marked v15 passes heading data as object or text+depth var headingText = typeof text === "object" ? text.text : text; var headingDepth = typeof text === "object" ? text.depth : depth; var raw = headingText.replace(/<[^>]+>/g, ""); var baseId = raw.toLowerCase() .replace(/[^\w\u4e00-\u9fff]+/g, "-") .replace(/^-|-$/g, ""); if (!baseId) baseId = "heading"; if (headingIndex[baseId] !== undefined) { headingIndex[baseId]++; baseId = baseId + "-" + headingIndex[baseId]; } else { headingIndex[baseId] = 0; } return '' + headingText + ""; }; // Code block handler renderer.code = function (obj) { var code, lang; if (typeof obj === "object") { code = obj.text; lang = obj.lang || ""; } else { code = obj; lang = ""; } if (lang === "mermaid") { return '
' + escHtml(code) + "
"; } var highlighted = ""; try { if (lang && hljs.getLanguage(lang)) { highlighted = hljs.highlight(code, { language: lang }).value; } else { highlighted = hljs.highlightAuto(code).value; } } catch (e) { highlighted = escHtml(code); } var langLabel = lang ? '' + escHtml(lang) + "" : ""; return "
" + langLabel + '' + highlighted + "
"; }; marked.use({ renderer: renderer, gfm: true, breaks: false, }); // Render var html; try { html = marked.parse(md); } catch (e) { console.error("Markdown render error:", e); article.innerHTML = '
渲染出错: ' + escHtml(String(e)) + '
'; return; } article.innerHTML = html; // Add page title from nav data var pageInfo = findPageInfo(slug); if (pageInfo) { var titleDiv = document.createElement("div"); titleDiv.className = "page-title"; titleDiv.innerHTML = "

" + escHtml(pageInfo.title) + "

"; if (pageInfo.section) { titleDiv.innerHTML += '
' + escHtml(pageInfo.section); if (pageInfo.level) titleDiv.innerHTML += ' · ' + escHtml(pageInfo.level) + ""; titleDiv.innerHTML += "
"; } article.insertBefore(titleDiv, article.firstChild); } // Render mermaid diagrams await renderMermaid(); // Build TOC buildTOC(); // Prev/Next navigation renderPageNav(); // Update document title document.title = (pageInfo ? pageInfo.title + " — " : "") + "sunvpy 文档"; } // --- Mermaid --- async function renderMermaid() { var mermaidDivs = article.querySelectorAll(".mermaid"); if (mermaidDivs.length === 0) return; if (!mermaidInitialized) { mermaid.initialize({ startOnLoad: false, theme: isDark ? "dark" : "default", securityLevel: "loose", maxTextSize: 90000, }); mermaidInitialized = true; } try { await mermaid.run({ nodes: mermaidDivs, suppressErrors: true }); } catch (e) { console.warn("Mermaid render error:", e); } } async function rerenderMermaid() { var wrappers = article.querySelectorAll(".mermaid-wrapper"); wrappers.forEach(function (wrapper) { var div = wrapper.querySelector(".mermaid"); if (!div) return; var source = div.getAttribute("data-mermaid-source"); if (source) { div.removeAttribute("data-processed"); div.innerHTML = escHtml(source); } }); mermaid.initialize({ startOnLoad: false, theme: isDark ? "dark" : "default", securityLevel: "loose", maxTextSize: 90000, }); try { await mermaid.run({ nodes: article.querySelectorAll(".mermaid"), suppressErrors: true }); } catch (e) { console.warn("Mermaid re-render error:", e); } } // --- TOC --- function buildTOC() { var headings = article.querySelectorAll("h2, h3"); if (headings.length === 0) { tocNav.innerHTML = '
无目录
'; return; } var html = ""; tocNav.innerHTML = html; // Scroll spy setupScrollSpy(headings); } function setupScrollSpy(headings) { var links = tocNav.querySelectorAll("a"); contentEl.removeEventListener("scroll", scrollSpyHandler); contentEl.addEventListener("scroll", scrollSpyHandler); function scrollSpyHandler() { var scrollTop = contentEl.scrollTop; var activeIdx = 0; for (var i = headings.length - 1; i >= 0; i--) { if (headings[i].offsetTop - 100 <= scrollTop) { activeIdx = i; break; } } links.forEach(function (link, idx) { link.classList.toggle("active", idx === activeIdx); }); } } // --- Prev/Next --- function renderPageNav() { var idx = flatPages.indexOf(currentPage); if (idx === -1) { pageNav.innerHTML = ""; return; } var html = ""; if (idx > 0) { var prevInfo = findPageInfo(flatPages[idx - 1]); html += '"; } else { html += "
"; } if (idx < flatPages.length - 1) { var nextInfo = findPageInfo(flatPages[idx + 1]); html += '"; } pageNav.innerHTML = html; } // --- Helpers --- function findPageInfo(slug) { for (var s = 0; s < navData.sections.length; s++) { var section = navData.sections[s]; if (section.pages) { for (var i = 0; i < section.pages.length; i++) { if (section.pages[i].slug === slug) { return Object.assign({}, section.pages[i], { section: section.title }); } } } if (section.groups) { for (var g = 0; g < section.groups.length; g++) { for (var j = 0; j < section.groups[g].pages.length; j++) { if (section.groups[g].pages[j].slug === slug) { return Object.assign({}, section.groups[g].pages[j], { section: section.title }); } } } } } return null; } function escHtml(s) { if (!s) return ""; return String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } function escAttr(s) { return escHtml(s); } // Copy code to clipboard window.copyCode = function (btn) { var code = btn.parentElement.querySelector("code"); var text = code.textContent; navigator.clipboard.writeText(text).then(function () { btn.textContent = "已复制"; setTimeout(function () { btn.textContent = "复制"; }, 1500); }); }; // --- Mobile sidebar --- function toggleMobileSidebar() { document.getElementById("sidebar").classList.toggle("open"); document.getElementById("sidebarBackdrop").classList.toggle("open"); } function closeMobileSidebar() { document.getElementById("sidebar").classList.remove("open"); document.getElementById("sidebarBackdrop").classList.remove("open"); } // Close mobile sidebar on nav document.addEventListener("click", function (e) { if (e.target.classList.contains("nav-page")) { closeMobileSidebar(); } }); // --- Start --- init(); })();