UNPKG

@nolebase/vitepress-plugin-page-properties

Version:

A VitePress plugin that renders frontmatter as page properties, and makes them editable.

241 lines (228 loc) 8.36 kB
import { relative, extname } from 'node:path'; import { env } from 'node:process'; import GrayMatter from 'gray-matter'; import { normalizePath } from 'vite'; import { existsSync, lstatSync, readFileSync } from 'node:fs'; function pathEquals(path, equals) { return normalizePath(path) === normalizePath(equals); } function pathStartsWith(path, startsWith) { return normalizePath(path).startsWith(normalizePath(startsWith)); } function pathEndsWith(path, startsWith) { return normalizePath(path).endsWith(normalizePath(startsWith)); } function PagePropertiesMarkdownSection(options) { const { excludes = ["index.md"], exclude = () => false } = options ?? {}; let root = ""; return { name: "@nolebase/vitepress-plugin-page-properties-markdown-section", // May set to 'pre' since end user may use vitepress wrapped vite plugin to // specify the plugins, which may cause this plugin to be executed after // vitepress or the other markdown processing plugins. enforce: "pre", configResolved(config) { root = config.root ?? ""; }, transform(code, id) { function idEndsWith(endsWith) { return pathEndsWith(relative(root, id), endsWith); } function idEquals(equals) { return pathEquals(relative(root, id), equals); } function idStartsWith(startsWith) { return pathStartsWith(relative(root, id), startsWith); } const context = { helpers: { pathEndsWith, pathEquals, pathStartsWith, idEndsWith, idEquals, idStartsWith } }; if (!id.endsWith(".md")) return null; if (excludes.includes(relative(root, id))) return null; if (exclude(id, context)) return null; const targetComponent = env.NODE_ENV === "development" ? TemplatePagePropertiesEditor() : TemplatePageProperties(); const parsedMarkdownContent = GrayMatter(code); if ("nolebase" in parsedMarkdownContent.data && "pageProperties" in parsedMarkdownContent.data.nolebase && !parsedMarkdownContent.data.nolebase.pageProperties) return null; if ("pageProperties" in parsedMarkdownContent.data && !parsedMarkdownContent.data.pageProperties) return null; const hasFrontmatter = Object.keys(parsedMarkdownContent.data).length > 0; const headingMatch = parsedMarkdownContent.content.match(/^# .*/m); if (!headingMatch || !headingMatch[0] || headingMatch.index === void 0) { if (!hasFrontmatter) return `${targetComponent} ${code}`; return `${GrayMatter.stringify(`${targetComponent} ${parsedMarkdownContent.content}`, parsedMarkdownContent.data)}`; } const headingPart = parsedMarkdownContent.content.slice(0, headingMatch.index + headingMatch[0].length); const contentPart = parsedMarkdownContent.content.slice(headingMatch.index + headingMatch[0].length); if (!hasFrontmatter) return `${headingPart} ${targetComponent} ${contentPart}`; return `${GrayMatter.stringify(`${headingPart} ${targetComponent} ${contentPart}`, parsedMarkdownContent.data)}`; } }; } function TemplatePagePropertiesEditor() { return ` <NolebasePagePropertiesEditor /> `; } function TemplatePageProperties() { return ` <NolebasePageProperties /> `; } const languageHandlers = { japanese: { regex: /\p{Script=Hiragana}|\p{Script=Katakana}/gu, // Match Japanese characters wordsPerMinute: 400 // Hypothetical average reading speed for Japanese }, chinese: { regex: /\p{Script=Han}/gu, // Match Chinese characters wordsPerMinute: 300 // Average reading speed for Chinese }, latinCyrillic: { regex: /[\p{Script=Latin}\p{Script=Cyrillic}\p{Mark}\p{Punctuation}\p{Number}]+/gu, // Match Latin and Cyrillic characters wordsPerMinute: 160 // Average reading speed for English and similar languages } }; function countWordsByLanguage(content) { return Object.keys(languageHandlers).reduce((accumulator, language) => { const match = content.match(languageHandlers[language].regex); accumulator[language] = match ? match.length : 0; return accumulator; }, {}); } function calculateWordsCountAndReadingTime(content) { const wordsCounts = countWordsByLanguage(content); const totalWords = Object.values(wordsCounts).reduce((sum, count) => sum + count, 0); const totalMinutes = Object.entries(wordsCounts).reduce((sum, [language, count]) => { return sum + count / languageHandlers[language].wordsPerMinute; }, 0); return { readingTime: Math.ceil(totalMinutes), wordsCount: totalWords }; } const VirtualModuleID = "virtual:nolebase-page-properties"; const ResolvedVirtualModuleId = `\0${VirtualModuleID}`; function normalizeWithRelative(from, path) { return normalizePath(relative(from, path)).toLowerCase(); } function PageProperties() { let _config; let srcDir = ""; const calculatedPagePropertiesActualData = {}; const knownMarkdownFiles = /* @__PURE__ */ new Set(); return { name: "@nolebase/vitepress-plugin-page-properties", // May set to 'pre' since end user may use vitepress wrapped vite plugin to // specify the plugins, which may cause this plugin to be executed after // vitepress or the other markdown processing plugins. enforce: "pre", config: () => ({ optimizeDeps: { exclude: [ "@nolebase/vitepress-plugin-page-properties/client" ] }, ssr: { noExternal: [ "@nolebase/vitepress-plugin-page-properties" ] } }), configResolved(config) { _config = config; srcDir = _config.vitepress.srcDir; }, resolveId(id) { if (id === VirtualModuleID) return ResolvedVirtualModuleId; }, load(id) { if (id !== ResolvedVirtualModuleId) return null; return `export default ${JSON.stringify(calculatedPagePropertiesActualData)}`; }, transform(code, id) { if (!id.endsWith(".md")) return null; const parsedContent = GrayMatter(code); calculatedPagePropertiesActualData[normalizeWithRelative(srcDir, id)] = calculateWordsCountAndReadingTime(parsedContent.content); }, configureServer(server) { compatibleConfigureServer(server, (_, env) => { env.hot.on("nolebase-page-properties:client-mounted", async (data) => { if (!data || typeof data !== "object") return; if (!("page" in data && "filePath" in data.page)) return; const toMarkdownFilePath = data.page.filePath; if (extname(data.page.filePath) !== ".md") return; if (!knownMarkdownFiles.has(toMarkdownFilePath.toLowerCase())) { try { const exists = await existsSync(toMarkdownFilePath); if (!exists) return; const stat = await lstatSync(toMarkdownFilePath); if (!stat.isFile()) return; knownMarkdownFiles.add(toMarkdownFilePath.toLowerCase()); } catch { return; } } if (!knownMarkdownFiles.has(toMarkdownFilePath.toLowerCase())) return; const content = await readFileSync(toMarkdownFilePath, "utf-8"); const parsedContent = GrayMatter(content); calculatedPagePropertiesActualData[toMarkdownFilePath] = calculateWordsCountAndReadingTime(parsedContent.content); const virtualModule = env.moduleGraph.getModuleById(ResolvedVirtualModuleId); if (!virtualModule) return; env.moduleGraph.invalidateModule(virtualModule); env.hot.send({ type: "custom", event: "nolebase-page-properties:updated", data: calculatedPagePropertiesActualData }); }); }); } }; } function compatibleConfigureServer(server, registerHandler) { if ("environments" in server && typeof server.environments === "object" && server.environments != null) { Object.entries(server.environments).forEach(([name, env]) => registerHandler(name, env)); } else { registerHandler("server", server); } } export { PageProperties, PagePropertiesMarkdownSection }; //# sourceMappingURL=index.mjs.map