UNPKG

vitepress-plugin-pagefind

Version:

vitepress plugin, Offline full-text search based on pagefind implementation.

170 lines (151 loc) 5.74 kB
import { Buffer } from 'node:buffer' import path from 'node:path' import { fileURLToPath } from 'node:url' import fs from 'node:fs' import process from 'node:process' import type { PluginOption } from 'vite' import type { HeadConfig, SiteConfig } from 'vitepress' import { stringify } from 'javascript-stringify' import { getFileLastModifyTime, grayMatter, joinPath } from '@sugarat/theme-shared' import { buildEnd, getPagefindHead } from './node' import type { PagefindOption, SearchConfig } from './type' function isESM() { return typeof __filename === 'undefined' || typeof __dirname === 'undefined' } function getDirname() { return isESM() ? path.dirname(fileURLToPath(import.meta.url)) : __dirname } const aliasSearchVueFile = `${getDirname()}/../src/Search.vue` export function meta2string(frontmatter: Record<string, any>) { return `base64:${Buffer.from(encodeURIComponent(JSON.stringify(frontmatter))).toString('base64')}` } export function pagefindPlugin( searchConfig: SearchConfig & PagefindOption = {} ): any { const virtualModuleId = 'virtual:pagefind' const resolvedVirtualModuleId = `\0${virtualModuleId}` let resolveConfig: any let vitepressConfig: SiteConfig let dynamicRoutes: SiteConfig['dynamicRoutes'] const pluginOps: PluginOption = { name: 'vitepress-plugin-pagefind', config: () => { return { resolve: { alias: { './VPNavBarSearch.vue': aliasSearchVueFile } } } }, configResolved(config: any) { if (searchConfig.manual) { return } if (resolveConfig) { return } resolveConfig = config vitepressConfig = config.vitepress dynamicRoutes = vitepressConfig.dynamicRoutes if (!vitepressConfig) { return } // 添加生成索引的方法 const selfBuildEnd = vitepressConfig.buildEnd vitepressConfig.buildEnd = async (siteConfig: any) => { // 调用自己的 await selfBuildEnd?.(siteConfig) await buildEnd(searchConfig, siteConfig) const okMark = '\x1B[32m✓\x1B[0m' console.log(`${okMark} generating pagefind Indexing...`) } // 通过 head 添加额外的脚本注入 const selfTransformHead = vitepressConfig.transformHead vitepressConfig.transformHead = async (ctx) => { const selfHead = (await Promise.resolve(selfTransformHead?.(ctx))) || [] return selfHead.concat(getPagefindHead(ctx.siteData.base) as HeadConfig[]) } }, resolveId(id: string) { if (id === virtualModuleId) { return resolvedVirtualModuleId } }, load(this, id) { if (id !== resolvedVirtualModuleId) return // 动态模块处理 return ` import { ref } from 'vue' export const searchConfig = ${stringify(searchConfig)} ` }, // 添加检索的内容标识 async transform(code, id, options) { if (!id.includes('.md')) { return code } let { searchParams, pathname, protocol } = new URL(id, 'file:') pathname = decodeURIComponent(pathname) if (!pathname.endsWith('.md')) { return code } // 兼容 动态 路由 const isWindows = process.platform === 'win32' const fullPath = isWindows ? `${protocol}${pathname}` : pathname const _dynamicRoutes = Array.isArray(dynamicRoutes) ? dynamicRoutes : (dynamicRoutes?.routes || []) const dynamicRoute = _dynamicRoutes.find(route => fullPath.toLowerCase() === route.fullPath.toLowerCase()) const isDynamicRoute = !!dynamicRoute const filepath = isDynamicRoute ? joinPath(vitepressConfig.srcDir, `/${dynamicRoute.route}`) : pathname const isExist = fs.existsSync(filepath) if (!isExist) { this.warn(`${id}: not parse ${filepath} please contact the author for assistance`) return code } // 兼容 setup lang="ts" if (!searchParams.size || searchParams.has('lang.ts')) { const fileContent = await fs.promises.readFile(filepath, 'utf-8') const { data: frontmatter, content } = grayMatter(fileContent, { excerpt: true }) // 不检索空内容页 if (!content.trim()) { return code } // 指定标志的不检索 if (frontmatter['pagefind-indexed'] === false) { return code } // 添加检索标志 const attrs: Record<string, any> = { 'data-pagefind-body': true } if (!searchConfig.manual) { // 添加 frontmatter 元数据 frontmatter.date = +new Date(frontmatter.date || await getFileLastModifyTime(id)) // 没有filter则不插入额外的 meta if (typeof searchConfig.filter === 'function') { attrs['data-pagefind-meta'] = meta2string(frontmatter) } } if (!code.includes(options?.ssr ? '_push(`' : '_createElementBlock("div", null')) { // 兼容 setup lang="ts" if (!code.includes(`${id}?vue&type=script&setup=true&lang.ts`)) { this.warn(`${options?.ssr ? 'SSR' : 'Client'} ${id} may not be a valid file, will not be indexed, please contact the author for assistance`) } return code } if (options?.ssr) { return code.replace('_push(`', `Object.assign(_attrs, ${stringify(attrs)});_push(\``) } return code.replace('_createElementBlock("div", null', `_createElementBlock("div", ${stringify(attrs)}`) } return code } } return pluginOps } export * from './type' export { chineseSearchOptimize } from './node' export { formatShowDate } from './utils/index'