vitepress-plugin-catalogue
Version:
201 lines (194 loc) • 7.6 kB
JavaScript
import { readdirSync, statSync, readFileSync, existsSync } from 'node:fs';
import { basename, resolve, extname, join } from 'node:path';
import matter from 'gray-matter';
import { createLogger } from 'vite';
import picocolors from 'picocolors';
const getTitleFromMd = (mdContent) => {
const lines = mdContent.trimStart().split(/\r?\n/);
for (const line of lines) {
if (line.startsWith("# ")) {
return line.substring(2).trim();
}
}
return void 0;
};
const isIllegalIndex = (index) => {
return isNaN(index) || index < 0;
};
const isMdFile = (filePath) => {
return filePath.includes("md") || filePath.includes("MD");
};
const isSome = (arr, name) => {
return arr.some((item) => item === name || item instanceof RegExp && item.test(name));
};
const DEFAULT_IGNORE_DIR = ["node_modules", "dist", ".vitepress", "public"];
const catalogueInfo = [];
const createCatalogues = (option = {}) => {
const { path = "", ignoreList = [] } = option;
if (!path) return [];
const dirPaths = readDirPaths(path, ignoreList);
dirPaths.forEach((dirPath) => scannerMdFile(dirPath, option, basename(dirPath)));
return catalogueInfo;
};
const readDirPaths = (sourceDir, ignoreList = []) => {
const ignoreListAll = [...DEFAULT_IGNORE_DIR, ...ignoreList];
const dirPaths = [];
const dirOrFilenames = readdirSync(sourceDir);
dirOrFilenames.forEach((dirOrFilename) => {
const secondDirPath = resolve(sourceDir, dirOrFilename);
if (!isSome(ignoreListAll, dirOrFilename) && statSync(secondDirPath).isDirectory()) {
dirPaths.push(secondDirPath);
}
});
return dirPaths;
};
const scannerMdFile = (root, option, prefix = "") => {
const { path: srcDir = "", ignoreList = [] } = option;
const ignoreListAll = [...DEFAULT_IGNORE_DIR, ...ignoreList];
const dirOrFilenames = readdirSync(root);
dirOrFilenames.forEach((dirOrFilename) => {
if (isSome(ignoreListAll, dirOrFilename)) return [];
const filePath = resolve(root, dirOrFilename);
if (statSync(filePath).isDirectory()) {
scannerMdFile(filePath, option, `${prefix}/${dirOrFilename}`);
} else {
if (!isMdFile(dirOrFilename)) return;
const content = readFileSync(filePath, "utf-8");
const { data: { catalogue, path = "" } = {} } = matter(content, {});
if (catalogue && path) {
const filename = basename(dirOrFilename, extname(dirOrFilename));
catalogueInfo.push({
filePath: `${prefix}/${filename}`,
path,
catalogues: createCatalogueList(join(srcDir, path), option, `/${path}/`)
});
}
}
});
};
const createCatalogueList = (root, option, prefix = "/") => {
if (!existsSync(root)) {
console.warn(`'${root}' \u8DEF\u5F84\u4E0D\u5B58\u5728\uFF0C\u5C06\u5FFD\u7565\u8BE5\u76EE\u5F55\u9875\u7684\u751F\u6210`);
return [];
}
const { ignoreIndexMd = false, titleFormMd = false } = option;
const catalogueItemList = [];
const catalogueItemListNoIndex = [];
const dirOrFilenames = readdirSync(root);
dirOrFilenames.forEach((dirOrFilename) => {
const filePath = resolve(root, dirOrFilename);
const { index: indexStr, title, name } = resolveFileName(dirOrFilename, filePath);
const index = parseInt(indexStr, 10);
if (statSync(filePath).isDirectory()) {
const mdTitle = titleFormMd ? tryGetMdTitle(root, dirOrFilename) : "";
const catalogueItem = {
title: mdTitle || title,
children: createCatalogueList(filePath, option, `${prefix}${dirOrFilename}/`)
};
if (isIllegalIndex(index)) catalogueItemListNoIndex.push(catalogueItem);
else catalogueItemList[index] = catalogueItem;
} else {
if (!isMdFile(dirOrFilename)) return [];
if (ignoreIndexMd && ["index.md", "index.MD"].includes(dirOrFilename)) return [];
const content = readFileSync(filePath, "utf-8");
const { data: frontmatter = {}, content: mdContent } = matter(content, {});
const { title: frontmatterTitle, catalogue, inCatalogue = true } = frontmatter;
if (catalogue || !inCatalogue) return [];
const mdTitle = titleFormMd ? getTitleFromMd(mdContent) : "";
const finalTitle = frontmatterTitle || mdTitle || title;
const catalogueItem = {
title: finalTitle,
link: prefix + name,
frontmatter
};
if (isIllegalIndex(index)) catalogueItemListNoIndex.push(catalogueItem);
else catalogueItemList[index] = catalogueItem;
}
});
return [...catalogueItemList, ...catalogueItemListNoIndex].filter(Boolean);
};
const resolveFileName = (filename, filePath) => {
const stat = statSync(filePath);
let index = "";
let title = "";
let type = "";
let name = "";
const fileNameArr = filename.split(".");
if (fileNameArr.length === 2) {
index = fileNameArr[0] === "index" ? "0" : fileNameArr[0];
title = stat.isDirectory() ? fileNameArr[1] : fileNameArr[0];
type = fileNameArr[1];
name = fileNameArr[0];
} else {
const firstDotIndex = filename.indexOf(".");
const lastDotIndex = filename.lastIndexOf(".");
index = filename.substring(0, firstDotIndex);
type = filename.substring(lastDotIndex + 1);
name = filename.substring(0, lastDotIndex);
if (stat.isDirectory()) title = filename.substring(firstDotIndex + 1);
else title = filename.substring(firstDotIndex + 1, lastDotIndex);
}
return { index, title, type, name };
};
const tryGetMdTitle = (root, dirOrFilename) => {
const filePaths = [
join(root, dirOrFilename, "index.md"),
join(root, dirOrFilename, "index.MD"),
join(root, dirOrFilename, dirOrFilename + ".md")
];
for (const filePath of filePaths) {
if (!existsSync(filePath)) continue;
const content = readFileSync(filePath, "utf-8");
const { content: mdContent } = matter(content, {});
const t = getTitleFromMd(mdContent);
if (t) return t;
}
return "";
};
const version = "1.0.15";
const logger = createLogger("info", {
prefix: `[vitepress-plugin-catalogue v${version}]`
});
const info = (message, level = "green", option = { timestamp: true }) => {
logger.info(picocolors[level](message), option);
};
const warn = (message, level = "yellow", option = { timestamp: true }) => {
logger.warn(picocolors[level](message), option);
};
const warnOnce = (message, level = "yellow", option = { timestamp: true }) => {
logger.info(picocolors[level](message), option);
};
const error = (message, level = "red", option = { timestamp: true }) => {
logger.error(picocolors[level](message), option);
};
const logger$1 = {
info,
warn,
warnOnce,
error
};
function VitePluginVitePressCatalogue(option = {}) {
let isExecute = false;
return {
name: "vite-plugin-vitepress-catalogue",
config(config) {
if (isExecute) return;
isExecute = true;
const {
site: { themeConfig },
srcDir
} = config.vitepress;
const baseDir = option.path ? join(srcDir, option.path) : srcDir;
const catalogues = createCatalogues({ ...option, path: baseDir });
const finalCatalogues = { arr: catalogues, map: {}, inv: {} };
catalogues.forEach((item) => {
const { filePath, path, catalogues: catalogues2 = [] } = item;
finalCatalogues.map[filePath] = { path, catalogues: catalogues2 };
finalCatalogues.inv[path] = { filePath, catalogues: catalogues2 };
});
themeConfig.catalogues = finalCatalogues;
logger$1.info("Injected Catalogues Data Successfully. \u6CE8\u5165\u76EE\u5F55\u9875\u6570\u636E\u6210\u529F!");
}
};
}
export { VitePluginVitePressCatalogue as default };