mirror of https://github.com/vuejs/vitepress
141 lines
3.5 KiB
141 lines
3.5 KiB
import fs from 'fs'
|
|
import path from 'path'
|
|
import glob from 'fast-glob'
|
|
import type { SiteConfig } from './config'
|
|
import matter from 'gray-matter'
|
|
import { normalizePath } from 'vite'
|
|
import { createMarkdownRenderer, type MarkdownRenderer } from './markdown'
|
|
|
|
export interface ContentOptions<T = ContentData[]> {
|
|
/**
|
|
* Include src?
|
|
* default: false
|
|
*/
|
|
includeSrc?: boolean
|
|
/**
|
|
* Render src to HTML and include in data?
|
|
* default: false
|
|
*/
|
|
render?: boolean
|
|
/**
|
|
* Whether to parse and include excerpt (rendered as HTML)
|
|
* default: false
|
|
*/
|
|
excerpt?: boolean
|
|
/**
|
|
* Transform the data. Note the data will be inlined as JSON in the client
|
|
* bundle if imported from components or markdown files.
|
|
*/
|
|
transform?: (data: ContentData[]) => T | Promise<T>
|
|
}
|
|
|
|
export interface ContentData {
|
|
url: string
|
|
src: string | undefined
|
|
html: string | undefined
|
|
frontmatter: Record<string, any>
|
|
excerpt: string | undefined
|
|
}
|
|
|
|
/**
|
|
* Create a loader object that can be directly used as the default export
|
|
* of a data loader file.
|
|
*/
|
|
export function createContentLoader<T = ContentData[]>(
|
|
/**
|
|
* files to glob / watch - relative to <project root>
|
|
*/
|
|
pattern: string | string[],
|
|
{
|
|
includeSrc,
|
|
render,
|
|
excerpt: renderExcerpt,
|
|
transform
|
|
}: ContentOptions<T> = {}
|
|
): {
|
|
watch: string | string[]
|
|
load: () => Promise<T>
|
|
} {
|
|
const config: SiteConfig = (global as any).VITEPRESS_CONFIG
|
|
if (!config) {
|
|
throw new Error(
|
|
'content loader invoked without an active vitepress process, ' +
|
|
'or before vitepress config is resolved.'
|
|
)
|
|
}
|
|
|
|
if (typeof pattern === 'string') pattern = [pattern]
|
|
pattern = pattern.map((p) => normalizePath(path.join(config.root, p)))
|
|
|
|
let md: MarkdownRenderer
|
|
|
|
const cache = new Map<
|
|
string,
|
|
{
|
|
data: any
|
|
timestamp: number
|
|
}
|
|
>()
|
|
|
|
return {
|
|
watch: pattern,
|
|
async load(files?: string[]) {
|
|
if (!files) {
|
|
// the loader is being called directly, do a fresh glob
|
|
files = (
|
|
await glob(pattern, {
|
|
ignore: ['**/node_modules/**', '**/dist/**']
|
|
})
|
|
).sort()
|
|
}
|
|
|
|
md =
|
|
md ||
|
|
(await createMarkdownRenderer(
|
|
config.srcDir,
|
|
config.markdown,
|
|
config.site.base,
|
|
config.logger
|
|
))
|
|
|
|
const raw: ContentData[] = []
|
|
|
|
for (const file of files) {
|
|
if (!file.endsWith('.md')) {
|
|
continue
|
|
}
|
|
const timestamp = fs.statSync(file).mtimeMs
|
|
const cached = cache.get(file)
|
|
if (cached && timestamp === cached.timestamp) {
|
|
raw.push(cached.data)
|
|
} else {
|
|
const src = fs.readFileSync(file, 'utf-8')
|
|
const { data: frontmatter, excerpt } = matter(src, {
|
|
excerpt: true
|
|
})
|
|
const url =
|
|
'/' +
|
|
normalizePath(path.relative(config.root, file)).replace(
|
|
/\.md$/,
|
|
config.cleanUrls ? '' : '.html'
|
|
)
|
|
const html = render ? md.render(src) : undefined
|
|
const renderedExcerpt = renderExcerpt
|
|
? excerpt && md.render(excerpt)
|
|
: undefined
|
|
const data: ContentData = {
|
|
src: includeSrc ? src : undefined,
|
|
html,
|
|
frontmatter,
|
|
excerpt: renderedExcerpt,
|
|
url
|
|
}
|
|
cache.set(file, { data, timestamp })
|
|
raw.push(data)
|
|
}
|
|
}
|
|
return (transform ? transform(raw) : raw) as any
|
|
}
|
|
}
|
|
}
|