import fs from 'node:fs' import path from 'node:path' import { getDefaultTitle, getFileLastModifyTime, getTextSummary, getVitePressPages, grayMatter, normalizePath, renderDynamicMarkdown } from '@sugarat/theme-shared' import type { SiteConfig } from 'vitepress' import type { Theme } from '../../composables/config/index' import { formatDate, shuffleArray } from '../client' import { getFirstImagURLFromMD } from './index' export function patchDefaultThemeSideBar(cfg?: Partial) { return cfg?.blog !== false && cfg?.recommend !== false ? { sidebar: [ { text: '', items: [] } ] } : undefined } export function getPageRoute(filepath: string, srcDir: string) { const route = normalizePath(path.relative(srcDir, filepath)) .replace(/\.md$/, '') return `/${route}` } const defaultTimeZoneOffset = new Date().getTimezoneOffset() / -60 export async function getArticleMeta(filepath: string, route: string, timeZone = defaultTimeZoneOffset, ops?: { baseContent?: string cacheDir?: string }) { const fileContent = ops?.baseContent || await fs.promises.readFile(filepath, 'utf-8') const cacheDir = ops?.cacheDir const { data: frontmatter, excerpt, content } = grayMatter(fileContent, { excerpt: true, }) const meta: Partial = { ...frontmatter } if (!meta.title) { meta.title = getDefaultTitle(content) } const utcValue = timeZone >= 0 ? `+${timeZone}` : `${timeZone}` const date = await ( (meta.date && new Date(`${new Date(meta.date).toUTCString()}${utcValue}`)) || getFileLastModifyTime(filepath, cacheDir) ) // 无法获取时兜底当前时间 meta.date = formatDate(date || new Date(), 'yyyy/MM/dd hh:mm:ss') // 处理tags和categories,兼容历史文章 meta.categories = typeof meta.categories === 'string' ? [meta.categories] : meta.categories meta.tags = typeof meta.tags === 'string' ? [meta.tags] : meta.tags meta.tag = [meta.tag || []] .flat() .concat([ ...new Set([...(meta.categories || []), ...(meta.tags || [])]) ]) // 获取摘要信息 // TODO:摘要生成优化,支持AI? // 先去除 Markdown 中的标题行(# 开头),避免 description 中重复出现标题文字 const contentWithoutHeadings = content.replace(/^#+\s+.*$/gm, '') meta.description = meta.description || getTextSummary(contentWithoutHeadings, 100) || excerpt // 获取封面图 // TODO: 耦合信息优化 meta.cover = meta.cover ?? (getFirstImagURLFromMD(fileContent, route)) // 是否发布 默认发布 if (meta.publish === false) { meta.hidden = true meta.recommend = false } return meta as Theme.PageMeta } export async function getArticles(cfg: Partial, vpConfig: SiteConfig) { const pages = getVitePressPages(vpConfig) const metaResults = pages.reduce((prev, value) => { const { page, route, originRoute, filepath, isDynamic, dynamicRoute } = value const metaPromise = (isDynamic && dynamicRoute) ? getArticleMeta(filepath, originRoute, cfg?.timeZone, { baseContent: renderDynamicMarkdown(filepath, dynamicRoute.params, dynamicRoute.content), cacheDir: vpConfig.cacheDir }) : getArticleMeta(filepath, originRoute, cfg?.timeZone, { cacheDir: vpConfig.cacheDir }) // 提前获取,有缓存取缓存 prev[page] = { route, metaPromise } return prev }, {} as Record }>) const pageData: Theme.PageData[] = [] for (const page of pages) { const { route, metaPromise } = metaResults[page.page] const meta = await metaPromise if (meta.layout === 'home') { continue } pageData.push({ route, meta }) } return pageData } export function patchVPConfig(vpConfig: any, cfg?: Partial) { vpConfig.head = vpConfig.head || [] // // Artalk 资源地址 // if (cfg?.comment && 'type' in cfg.comment && cfg?.comment?.type === 'artalk') { // const server = cfg.comment?.options?.server // if (server) { // vpConfig.head.push(['link', { href: `${server}/dist/Artalk.css`, rel: 'stylesheet' }]) // vpConfig.head.push(['script', { src: `${server}/dist/Artalk.js`, id: 'artalk-script' }]) // } // } } export function patchVPThemeConfig( cfg?: Partial, vpThemeConfig: any = {} ) { // 用于自定义sidebar卡片slot vpThemeConfig.sidebar = patchDefaultThemeSideBar(cfg)?.sidebar return vpThemeConfig } export function checkConfig(cfg?: Partial) { const friendConfig = cfg?.friend if (!Array.isArray(friendConfig) && friendConfig?.random) { friendConfig.list = shuffleArray(friendConfig.list) } }