UNPKG

@nolebase/vitepress-plugin-sidebar

Version:
286 lines (283 loc) 8.99 kB
import { existsSync, readFileSync, statSync } from 'node:fs'; import { basename, extname, join } from 'node:path'; import { cwd } from 'node:process'; import GrayMatter from 'gray-matter'; import { globSync } from 'tinyglobby'; function isDirectory(path) { try { const res = statSync(path); return res.isDirectory(); } catch { return false; } } function directoryTitleFromPath(path) { const possibleTitleFromFiles = [ "index.md", "_page.md" ]; let title = ""; for (const file of possibleTitleFromFiles) { if (!existsSync(join(path, file))) continue; const fileContent = readFileSync(join(path, file), "utf-8"); const { content, data } = GrayMatter(fileContent); if (!title && data?.sidebarTitle) { title = data.sidebarTitle; } if (!title && data?.title) { title = data.title; } const matchRes = content.match(/^#\s+(.*)$/m)?.[1]; if (!title && matchRes) { title = matchRes.trim(); } } if (!title) return basename(path); return title; } function directoryCollapsedFromPath(path) { const possibleTitleFromFiles = [ "index.md", "_page.md" ]; let collapsed = true; for (const file of possibleTitleFromFiles) { if (!existsSync(join(path, file))) continue; const fileContent = readFileSync(join(path, file), "utf-8"); const { data } = GrayMatter(fileContent); if (data?.sidebarCollapsed != null) { collapsed = Boolean(data.sidebarCollapsed); } } return collapsed; } function shouldHideFromSidebar(path) { try { if (existsSync(path)) { const fileContent = readFileSync(path, "utf-8"); const { data } = GrayMatter(fileContent); return data?.sidebarHide === true; } return false; } catch (e) { console.error(`Error reading or parsing file: ${path}`, e); return false; } } function titleFromPage(path) { const titleFromFilename = basename(path, extname(path)); let title = ""; try { if (existsSync(join(cwd(), path))) { const fileContent = readFileSync(join(cwd(), path), "utf-8"); const { content, data } = GrayMatter(fileContent); if (!title && data?.sidebarTitle) { title = data.sidebarTitle; } if (!title && data?.title) { title = data.title; } const matchRes = content.match(/^#\s+(.*)$/m)?.[1]; if (!title && matchRes) { title = matchRes.trim(); } } if (!title) { title = titleFromFilename; } return title; } catch (e) { console.error(`Error reading or parsing file: ${path}`, e); return basename(path, extname(path)); } } function listPages(options) { const { targets = [], ignore = [] } = options; const files = globSync(`**/*.md`, { cwd: cwd(), ignore: [ "_*", "**/_page.md", "dist", "node_modules", ...ignore ], onlyFiles: true }); files.sort(); return files.filter((file) => { return targets.some((target) => file.startsWith(folderNameFromTargetConfig(target))); }); } function folderNameFromTargetConfig(target) { return typeof target === "string" ? target : target.folderName; } function separateFromTargetConfig(target) { return typeof target === "string" ? false : target.separate; } function addRouteItem(indexes, path, base) { if (shouldHideFromSidebar(path)) { return indexes; } const title = titleFromPage(path); const suffixIndex = path.lastIndexOf("."); const item = { index: title, text: title, link: `/${path.slice(0, suffixIndex)}` }; let linkItems = item.link?.split("/") ?? []; linkItems = linkItems.slice(1); if (linkItems.length === 1) return; if (base) { const baseItems = base.split("/").filter(Boolean); linkItems = linkItems.slice(baseItems.length); } indexes = addRouteItemRecursion(indexes, item, linkItems, [], item.link); } function addRouteItemRecursion(indexes, item, path, parentPath, fullLink, upgradeIndex = false) { if (path.length === 1) { indexes.push(item); return indexes; } else { const onePath = path.shift(); if (!onePath) return indexes; parentPath.push(onePath); const currentPath = join(cwd(), ...parentPath); let obj = indexes.find((obj2) => obj2.index === onePath); if (!obj) { let collapsed = true; if (isDirectory(currentPath)) { collapsed = directoryCollapsedFromPath(currentPath) ?? true; } obj = { index: onePath, text: onePath, collapsed, items: [] }; indexes.push(obj); } else if (!obj.items) { let collapsed = true; if (isDirectory(currentPath)) { collapsed = directoryCollapsedFromPath(currentPath) ?? true; } obj.collapsed = collapsed; obj.items = []; } if (path.length === 1 && path[0] === "index") { obj.link = item.link; if (parentPath.includes(onePath) && isDirectory(join(cwd(), ...parentPath))) { const title = directoryTitleFromPath(join(cwd(), ...parentPath)); obj.text = title; } } else { if (parentPath.includes(onePath) && isDirectory(join(cwd(), ...parentPath))) { const title = directoryTitleFromPath(join(cwd(), ...parentPath)); obj.text = title; } obj.items = addRouteItemRecursion(obj.items ?? [], item, path, parentPath, fullLink, upgradeIndex); } return indexes; } } function processSidebar(docs, base) { const sidebar = []; docs.map(async (docPath) => { addRouteItem(sidebar, docPath, base); }); return sidebar; } function articleTreeSort(articleTree) { articleTree.sort((itemA, itemB) => { return itemA.text.localeCompare(itemB.text); }); return articleTree; } function sidebarSort(sidebar, folderTop = true) { let _sideBar; if (folderTop) { const files = articleTreeSort(sidebar.filter((item) => { return !item.items || item.items.length === 0; })); const folders = articleTreeSort(sidebar.filter((item) => { return item.items && item.items.length > 0; })); _sideBar = [...folders, ...files]; } else { _sideBar = articleTreeSort(sidebar); } for (const articleTree of _sideBar) { if (articleTree.items && articleTree.items.length > 0) articleTree.items = sidebarSort(articleTree.items, folderTop); } return _sideBar; } function skipSidebarLevels(sidebar, levels) { let currentSidebar = sidebar; let skippedCount = 0; const levelsToSkip = Math.max(0, Math.floor(levels)); while (skippedCount < levelsToSkip) { if (currentSidebar.length === 1 && currentSidebar[0].items && Array.isArray(currentSidebar[0].items)) { currentSidebar = currentSidebar[0].items; skippedCount++; } else { break; } } return currentSidebar; } function mergeSidebar(targets, docs, base, skipLevelsConfig) { let sidebar = processSidebar(docs, base); sidebar = sidebarSort(sidebar, true); let isSingleOptimized = false; let singleOptimizedSidebar; if (sidebar.length === 1 && targets.some((item) => { const folderName = folderNameFromTargetConfig(item); return base ? folderName.endsWith(`/${sidebar[0].index}`) : folderName === sidebar[0].index; })) { singleOptimizedSidebar = sidebar[0].items ?? []; isSingleOptimized = true; } const basePrefix = base ? `/${base}` : ""; const sidebarMultiple = { [`${basePrefix}/`]: isSingleOptimized ? singleOptimizedSidebar : sidebar }; if (!isSingleOptimized) { const rootSidebar = [...sidebar]; for (const target of targets) { const folderName = folderNameFromTargetConfig(target); if (separateFromTargetConfig(target)) { const targetIndex = base ? folderName.split("/").pop() : folderName; const folderIdx = rootSidebar.findIndex((item) => item.index === targetIndex && item.items); if (folderIdx !== -1) { const folderItem = rootSidebar[folderIdx]; sidebarMultiple[`${basePrefix}/${folderItem.index}/`] = folderItem.items || []; rootSidebar.splice(folderIdx, 1); } else { sidebarMultiple[`${basePrefix}/${targetIndex}/`] = []; } } } sidebarMultiple[`${basePrefix}/`] = rootSidebar; } if (skipLevelsConfig) { for (const key in sidebarMultiple) { if (Object.prototype.hasOwnProperty.call(sidebarMultiple, key) && Object.prototype.hasOwnProperty.call(skipLevelsConfig, key)) { const levelsToSkip = skipLevelsConfig[key]; if (levelsToSkip > 0) { sidebarMultiple[key] = skipSidebarLevels(sidebarMultiple[key], levelsToSkip); } } } } if (Object.keys(sidebarMultiple).length === 1 && sidebarMultiple[`${basePrefix}/`]) { return sidebarMultiple[`${basePrefix}/`]; } return sidebarMultiple; } function calculateSidebar(targets = ["\u7B14\u8BB0"], base, skipLevelsConfig) { const docs = listPages({ targets }); return mergeSidebar(targets, docs, base, skipLevelsConfig); } export { calculateSidebar };