mirror of https://github.com/vuejs/vitepress
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
179 lines
4.9 KiB
179 lines
4.9 KiB
import matter from 'gray-matter'
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import pMap from 'p-map'
|
|
import { normalizePath } from 'vite'
|
|
import type { SiteConfig } from './config'
|
|
import {
|
|
createMarkdownRenderer,
|
|
mergeMarkdownLocales
|
|
} from './markdown/markdown'
|
|
import type { Awaitable, MarkdownEnv } from './shared'
|
|
import { glob, normalizeGlob, type GlobOptions } from './utils/glob'
|
|
|
|
export interface ContentOptions<T = ContentData[]> {
|
|
/**
|
|
* Include src?
|
|
* @default false
|
|
*/
|
|
includeSrc?: boolean
|
|
|
|
/**
|
|
* Render src to HTML and include in data?
|
|
* @default false
|
|
*/
|
|
render?: boolean
|
|
|
|
/**
|
|
* If `boolean`, whether to parse and include excerpt? (rendered as HTML)
|
|
*
|
|
* If `function`, control how the excerpt is extracted from the content.
|
|
*
|
|
* If `string`, define a custom separator to be used for extracting the
|
|
* excerpt. Default separator is `---` if `excerpt` is `true`.
|
|
*
|
|
* @see https://github.com/jonschlinkert/gray-matter#optionsexcerpt
|
|
* @see https://github.com/jonschlinkert/gray-matter#optionsexcerpt_separator
|
|
*
|
|
* @default false
|
|
*/
|
|
excerpt?:
|
|
| boolean
|
|
| ((
|
|
file: {
|
|
data: { [key: string]: any }
|
|
content: string
|
|
excerpt?: string
|
|
},
|
|
options?: any
|
|
) => void)
|
|
| string
|
|
|
|
/**
|
|
* 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[]) => Awaitable<T>
|
|
|
|
/**
|
|
* Options to pass to `tinyglobby` and `picomatch` for globbing.
|
|
*/
|
|
globOptions?: GlobOptions
|
|
}
|
|
|
|
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 srcDir
|
|
*/
|
|
watch: string | string[],
|
|
options: ContentOptions<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.'
|
|
)
|
|
}
|
|
|
|
const cache = new Map<string, { data: any; timestamp: number }>()
|
|
|
|
watch = normalizeGlob(watch, config.srcDir)
|
|
|
|
return {
|
|
watch,
|
|
options: { globOptions: options.globOptions },
|
|
|
|
async load(files?: string[]) {
|
|
// the loader is being called directly, do a fresh glob
|
|
files ??= await glob(watch, {
|
|
absolute: true,
|
|
...options.globOptions
|
|
})
|
|
|
|
const md = await createMarkdownRenderer(
|
|
config.srcDir,
|
|
mergeMarkdownLocales(config.markdown, config.site.locales),
|
|
config.site.base,
|
|
config.logger,
|
|
config.publicDir
|
|
)
|
|
|
|
const raw = await pMap(
|
|
files,
|
|
async (file) => {
|
|
if (!file.endsWith('.md')) return null
|
|
|
|
const timestamp = fs.statSync(file).mtimeMs
|
|
const cached = cache.get(file)
|
|
|
|
if (cached && timestamp === cached.timestamp) return cached.data
|
|
|
|
const src = fs.readFileSync(file, 'utf8')
|
|
|
|
const renderExcerpt = options.excerpt
|
|
const { data: frontmatter, excerpt } = matter(
|
|
src,
|
|
typeof renderExcerpt === 'string'
|
|
? { excerpt_separator: renderExcerpt }
|
|
: { excerpt: renderExcerpt as any } // gray-matter types are wrong
|
|
)
|
|
|
|
const relFile = normalizePath(path.relative(config.srcDir, file))
|
|
const relativePath = config.rewrites.map[relFile] || relFile
|
|
|
|
const url =
|
|
'/' +
|
|
relativePath
|
|
.replace(/(^|\/)index\.md$/, '$1')
|
|
.replace(/\.md$/, config.cleanUrls ? '' : '.html')
|
|
|
|
// pass a markdown env so plugins (e.g. the internal link plugin)
|
|
// resolve links, `cleanUrls` and paths the same way as during a
|
|
// normal page render instead of falling back to defaults
|
|
const env: MarkdownEnv = {
|
|
path: file,
|
|
relativePath,
|
|
cleanUrls: !!config.cleanUrls,
|
|
realPath: file
|
|
}
|
|
|
|
const html = options.render
|
|
? await md.renderAsync(src, env)
|
|
: undefined
|
|
const renderedExcerpt = renderExcerpt
|
|
? excerpt && (await md.renderAsync(excerpt, env))
|
|
: undefined
|
|
|
|
const data: ContentData = {
|
|
src: options.includeSrc ? src : undefined,
|
|
html,
|
|
frontmatter,
|
|
excerpt: renderedExcerpt,
|
|
url
|
|
}
|
|
|
|
cache.set(file, { data, timestamp })
|
|
return data
|
|
},
|
|
{ concurrency: config.buildConcurrency }
|
|
)
|
|
|
|
const filtered = raw.filter((i) => i !== null)
|
|
return options.transform?.(filtered) ?? (filtered as T)
|
|
}
|
|
}
|
|
}
|