@nolebase/vitepress-plugin-sidebar
Version:
A vitepress plugin to generate sidebar for your site.
286 lines (283 loc) • 8.99 kB
JavaScript
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 };