当前位置: 首页 > news >正文

vitePress实现原理(三)

(二)createVitePressPlugin函数

源码位置:src/node/plugins.ts

import path from 'path' // 导入路径模块
import c from 'picocolors' // 导入颜色模块用于控制台输出着色
import {
  mergeConfig, // 合并配置函数
  searchForWorkspaceRoot, // 查找工作区根目录函数
  type ModuleNode, // Vite模块节点类型
  type Plugin, // Vite插件类型
  type ResolvedConfig, // 解析后的Vite配置类型
  type Rollup, // Rollup打包工具类型
  type UserConfig // 用户自定义Vite配置类型
} from 'vite'
import {
  APP_PATH, // 应用程序路径常量
  DIST_CLIENT_PATH, // 客户端分发路径常量
  SITE_DATA_REQUEST_PATH, // 站点数据请求路径常量
  resolveAliases // 解析别名函数
} from './alias'
import { resolvePages, resolveUserConfig, type SiteConfig } from './config' // 解析页面和用户配置函数及站点配置类型
import { disposeMdItInstance } from './markdown/markdown' // 清理Markdown实例函数
import {
  clearCache, // 清除缓存函数
  createMarkdownToVueRenderFn, // 创建Markdown转Vue渲染函数
  type MarkdownCompileResult // Markdown编译结果类型
} from './markdownToVue'
import { dynamicRoutesPlugin } from './plugins/dynamicRoutesPlugin' // 动态路由插件
import { localSearchPlugin } from './plugins/localSearchPlugin' // 本地搜索插件
import { rewritesPlugin } from './plugins/rewritesPlugin' // 重写插件
import { staticDataPlugin } from './plugins/staticDataPlugin' // 静态数据插件
import { webFontsPlugin } from './plugins/webFontsPlugin' // Web字体插件
import { slash, type PageDataPayload } from './shared' // 斜杠处理函数及页面数据负载类型
import { deserializeFunctions, serializeFunctions } from './utils/fnSerialize' // 函数序列化和反序列化工具

declare module 'vite' {
  interface UserConfig {
    vitepress?: SiteConfig // 扩展Vite用户配置接口,添加vitepress字段
  }
}

const themeRE = /\/\.vitepress\/theme\/index\.(m|c)?(j|t)s$/ // 主题文件正则表达式
const hashRE = /\.([-\w]+)\.js$/ // 哈希值正则表达式
const staticInjectMarkerRE =
  /\b(const _hoisted_\d+ = \/\*(?:#|@)__PURE__\*\/\s*createStaticVNode)\("(.*)", (\d+)\)/g // 静态注入标记正则表达式
const staticStripRE = /['"`]__VP_STATIC_START__[^]*?__VP_STATIC_END__['"`]/g // 静态剥离正则表达式
const staticRestoreRE = /__VP_STATIC_(START|END)__/g // 静态恢复正则表达式

// 匹配MPA模式下的客户端JavaScript块。
const scriptClientRE = /<script\b[^>]*client\b[^>]*>([^]*?)<\/script>/

const isPageChunk = (
  chunk: Rollup.OutputAsset | Rollup.OutputChunk
): chunk is Rollup.OutputChunk & { facadeModuleId: string } =>
  !!(
    chunk.type === 'chunk' && // 检查是否为chunk类型
    chunk.isEntry && // 检查是否为主入口
    chunk.facadeModuleId && // 检查是否有facadeModuleId
    chunk.facadeModuleId.endsWith('.md') // 检查facadeModuleId是否以.md结尾
  )

const cleanUrl = (url: string): string =>
  url.replace(/#.*$/s, '').replace(/\?.*$/s, '') // 移除URL中的哈希和查询参数

export async function createVitePressPlugin(
  siteConfig: SiteConfig,
  ssr = false,
  pageToHashMap?: Record<string, string>,
  clientJSMap?: Record<string, string>,
  recreateServer?: () => Promise<void>
) {
  const {
    srcDir,
    configPath,
    configDeps,
    markdown,
    site,
    vue: userVuePluginOptions,
    vite: userViteConfig,
    pages,
    lastUpdated,
    cleanUrls
  } = siteConfig

  let markdownToVue: Awaited<ReturnType<typeof createMarkdownToVueRenderFn>> // Markdown转Vue渲染函数的结果
  const userCustomElementChecker =
    userVuePluginOptions?.template?.compilerOptions?.isCustomElement // 用户自定义元素检查器
  let isCustomElement = userCustomElementChecker // 初始化自定义元素检查器

  if (markdown?.math) {
    isCustomElement = (tag) => {
      if (tag.startsWith('mjx-')) {
        return true
      }
      return userCustomElementChecker?.(tag) ?? false // 使用用户自定义元素检查器或默认返回false
    }
  }

  // 懒加载导入plugin-vue以尊重NODE_ENV在@vue/compiler-x中的设置
  const vuePlugin = await import('@vitejs/plugin-vue').then((r) =>
    r.default({
      include: [/\.vue$/, /\.md$/], // 包含.vue和.md文件
      ...userVuePluginOptions, // 合并用户Vue插件选项
      template: {
        ...userVuePluginOptions?.template, // 合并模板选项
        compilerOptions: {
          ...userVuePluginOptions?.template?.compilerOptions, // 合并编译器选项
          isCustomElement // 设置自定义元素检查器
        }
      }
    })
  )

  const processClientJS = (code: string, id: string) => {
    return scriptClientRE.test(code)
      ? code.replace(scriptClientRE, (_, content) => {
        if (ssr && clientJSMap) clientJSMap[id] = content // 如果是SSR并且有clientJSMap,则存储内容
        return `\n`.repeat(_.split('\n').length - 1) // 替换为相同数量的新行
      })
      : code // 如果不匹配则返回原始代码
  }

  let siteData = site // 初始化站点数据
  let allDeadLinks: MarkdownCompileResult['deadLinks'] = [] // 初始化所有死链数组
  let config: ResolvedConfig // 解析后的Vite配置
  let importerMap: Record<string, Set<string> | undefined> = {} // 导入映射

  const vitePressPlugin: Plugin = {
    name: 'vitepress', // 插件名称

    async configResolved(resolvedConfig) {
      // 触发时机:当Vite完成配置解析后调用
      config = resolvedConfig // 设置解析后的配置
      markdownToVue = await createMarkdownToVueRenderFn(
        srcDir,
        markdown,
        pages,
        config.command === 'build',
        config.base,
        lastUpdated,
        cleanUrls,
        siteConfig
      ) // 创建Markdown转Vue渲染函数
    },

    config() {
      // 触发时机:在Vite读取配置文件时调用
      const baseConfig: UserConfig = {
        resolve: {
          alias: resolveAliases(siteConfig, ssr) // 解析别名
        },
        define: {
          __VP_LOCAL_SEARCH__: site.themeConfig?.search?.provider === 'local', // 是否启用本地搜索
          __ALGOLIA__:
            site.themeConfig?.search?.provider === 'algolia' ||
            !!site.themeConfig?.algolia, // 是否启用Algolia搜索
          __CARBON__: !!site.themeConfig?.carbonAds, // 是否启用Carbon广告
          __ASSETS_DIR__: JSON.stringify(siteConfig.assetsDir), // 资源目录
          __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: !!process.env.DEBUG // Vue生产水合调试信息
        },
        optimizeDeps: {
          // 强制包含vue以避免链接和优化时出现重复副本
          include: [
            'vue',
            'vitepress > @vue/devtools-api',
            'vitepress > @vueuse/core'
          ],
          exclude: ['@docsearch/js', 'vitepress'] // 排除某些依赖
        },
        server: {
          fs: {
            allow: [
              DIST_CLIENT_PATH,
              srcDir,
              searchForWorkspaceRoot(process.cwd())
            ] // 允许访问的文件系统路径
          }
        },
        vitepress: siteConfig // VitePress配置
      }
      return userViteConfig
        ? mergeConfig(baseConfig, userViteConfig)
        : baseConfig // 合并用户配置和基础配置
    },

    resolveId(id) {
      // 触发时机:当Vite需要解析一个模块ID时调用
      if (id === SITE_DATA_REQUEST_PATH) {
        return SITE_DATA_REQUEST_PATH // 返回站点数据请求路径
      }
    },

    load(id) {
      // 触发时机:当Vite需要加载一个模块的内容时调用
      if (id === SITE_DATA_REQUEST_PATH) {
        let data = siteData
        // 生产构建中客户端不需要头部信息
        if (config.command === 'build') {
          data = { ...siteData, head: [] } // 移除头部信息
          // 在生产客户端构建中,数据内联到每个页面以避免配置更改使每个块失效
          if (!ssr) {
            return `export default window.__VP_SITE_DATA__` // 导出全局站点数据
          }
        }
        data = serializeFunctions(data) // 序列化数据
        return `${deserializeFunctions};export default deserializeFunctions(JSON.parse(${JSON.stringify(
          JSON.stringify(data)
        )}))` // 反序列化数据并导出
      }
    },

    async transform(code, id) {
      // 触发时机:当Vite需要转换一个模块的内容时调用
      if (id.endsWith('.vue')) {
        return processClientJS(code, id) // 处理客户端JavaScript
      } else if (id.endsWith('.md')) {
        // 将.md文件转换为vueSrc以便plugin-vue处理
        const { vueSrc, deadLinks, includes } = await markdownToVue(
          code,
          id,
          config.publicDir
        )
        allDeadLinks.push(...deadLinks) // 添加死链
        if (includes.length) {
          includes.forEach((i) => {
            ; (importerMap[slash(i)] ??= new Set()).add(id) // 更新导入映射
            this.addWatchFile(i) // 添加监视文件
          })
        }
        return processClientJS(vueSrc, id) // 处理客户端JavaScript
      }
    },

    renderStart() {
      // 触发时机:当Vite开始渲染页面时调用
      if (allDeadLinks.length > 0) {
        allDeadLinks.forEach(({ url, file }, i) => {
          siteConfig.logger.warn(
            c.yellow(
              `${i === 0 ? '\n\n' : ''}(!) 找到死链 ${c.cyan(
                url
              )} 在文件 ${c.white(c.dim(file))}`
            )
          ) // 输出警告信息
        })
        siteConfig.logger.info(
          c.cyan(
            '\n如果这是预期行为,您可以通过配置禁用此检查。参考:https://vitepress.dev/reference/site-config#ignoredeadlinks\n'
          )
        ) // 提示如何禁用死链检查
        throw new Error(`${allDeadLinks.length} 死链(s) 被找到。`) // 抛出错误
      }
    },

    configureServer(server) {
      // 触发时机:当Vite启动开发服务器时调用
      // 监听文件变化 configPath 项目目录默认doc
      if (configPath) {
        server.watcher.add(configPath) // 添加配置路径监听
        configDeps.forEach((file) => server.watcher.add(file)) // 添加配置依赖监听
      }

      const onFileAddDelete = async (added: boolean, _file: string) => {
        const file = slash(_file)
        // 当主题文件被创建或删除时重启服务器
        if (themeRE.test(file)) {
          siteConfig.logger.info(
            c.green(
              `${path.relative(process.cwd(), _file)} ${added ? '已创建' : '已删除'
              },正在重启服务器...\n`
            ),
            { clear: true, timestamp: true }
          ) // 输出重启服务器信息

          await recreateServer?.() // 重启服务器
        }
        // 当Markdown文件被创建或删除时更新页面、动态路由和重写规则
        if (file.endsWith('.md')) {
          Object.assign(
            siteConfig,
            await resolvePages(
              siteConfig.srcDir,
              siteConfig.userConfig,
              siteConfig.logger
            )
          ) // 更新页面配置
        }

        if (!added && importerMap[file]) {
          delete importerMap[file] // 删除导入映射
        }
      }
      server.watcher
        .on('add', onFileAddDelete.bind(null, true))
        .on('unlink', onFileAddDelete.bind(null, false)) // 绑定文件添加和删除事件
      // 为服务器中间件添加自定义HTML响应
      return () => {
        server.middlewares.use(async (req, res, next) => {
          const url = req.url && cleanUrl(req.url)
          if (url?.endsWith('.html')) {
            res.statusCode = 200
            res.setHeader('Content-Type', 'text/html')
            let html = `<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <meta name="description" content="">
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/@fs/${APP_PATH}/index.js"></script>
  </body>
</html>` // 构建基本HTML结构
            html = await server.transformIndexHtml(url, html, req.originalUrl) // 转换HTML
            res.end(html) // 发送响应
            return
          }
          next() // 调用下一个中间件
        })
      }
    },

    renderChunk(code, chunk) {
      // 触发时机:当Vite渲染一个代码块时调用
      if (!ssr && isPageChunk(chunk as Rollup.OutputChunk)) {
        // 对于每个页面块,
        // 注入标记以标识静态字符串的开始和结束。
        // 我们在这里这样做是因为在generateBundle阶段,
        // 块会被压缩,我们无法安全地定位字符串。
        // 使用正则表达式依赖于Vue编译器核心的特定输出,
        // 这是一个合理的权衡,考虑到相对于完整AST解析的巨大性能提升。
        code = code.replace(
          staticInjectMarkerRE,
          '$1("__VP_STATIC_START__$2__VP_STATIC_END__", $3)'
        )
        return code
      }
      return null
    },

    generateBundle(_options, bundle) {
      // 触发时机:当Vite生成最终的构建包时调用
      if (ssr) {
        this.emitFile({
          type: 'asset',
          fileName: 'package.json',
          source: '{ "private": true, "type": "module" }'
        })
      } else {
        // 客户端构建:
        // 对于每个.md入口块,调整其名称为其正确的路径。
        for (const name in bundle) {
          const chunk = bundle[name]
          if (isPageChunk(chunk)) {
            // 记录页面 -> 哈希关系
            const hash = chunk.fileName.match(hashRE)![1]
            pageToHashMap![chunk.name.toLowerCase()] = hash

            // 注入另一个块,其中内容已被剥离
            bundle[name + '-lean'] = {
              ...chunk,
              fileName: chunk.fileName.replace(/\.js$/, '.lean.js'),
              preliminaryFileName: chunk.preliminaryFileName.replace(
                /\.js$/,
                '.lean.js'
              ),
              code: chunk.code.replace(staticStripRE, `""`)
            }

            // 从原始代码中移除静态标记
            chunk.code = chunk.code.replace(staticRestoreRE, '')
          }
        }
      }
    },

    async handleHotUpdate(ctx) {
      // 触发时机:当Vite检测到文件热更新时调用
      const { file, read, server } = ctx
      if (file === configPath || configDeps.includes(file)) {
        siteConfig.logger.info(
          c.green(
            `${path.relative(
              process.cwd(),
              file
            )} changed, restarting server...\n`
          ),
          { clear: true, timestamp: true }
        )

        try {
          await resolveUserConfig(siteConfig.root, 'serve', 'development')
        } catch (err: any) {
          siteConfig.logger.error(err)
          return
        }

        disposeMdItInstance()
        clearCache()
        await recreateServer?.()
        return
      }

      // 热重载 .md 文件为 .vue 文件
      if (file.endsWith('.md')) {
        const content = await read()
        const { pageData, vueSrc } = await markdownToVue(
          content,
          file,
          config.publicDir
        )

        const relativePath = slash(path.relative(srcDir, file))
        const payload: PageDataPayload = {
          path: `/${siteConfig.rewrites.map[relativePath] || relativePath}`,
          pageData
        }

        // 通知客户端更新页面数据
        server.ws.send({
          type: 'custom',
          event: 'vitepress:pageData',
          data: payload
        })

        // 覆盖src以便Vue插件可以处理HMR
        ctx.read = () => vueSrc
      }
    }
  }

  const hmrFix: Plugin = {
    name: 'vitepress:hmr-fix',
    async handleHotUpdate({ file, server, modules }) {
      // 触发时机:当Vite检测到文件热更新时调用
      const importers = [...(importerMap[slash(file)] || [])]
      if (importers.length > 0) {
        return [
          ...modules,
          ...importers.map((id) => {
            clearCache(slash(path.relative(srcDir, id)))
            return server.moduleGraph.getModuleById(id)
          })
        ].filter(Boolean) as ModuleNode[]
      }
    }
  }

  return [
    vitePressPlugin,
    rewritesPlugin(siteConfig),
    vuePlugin,
    hmrFix,
    webFontsPlugin(siteConfig.useWebFonts),
    ...(userViteConfig?.plugins || []),
    await localSearchPlugin(siteConfig),
    staticDataPlugin,
    await dynamicRoutesPlugin(siteConfig)
  ]
}
  1. 配置解析和合并
    • 解析用户自定义的VitePress配置。
    • 合并基础配置和用户自定义配置,生成最终的Vite配置。
  2. 主题文件处理
    • 监听主题文件的变化,并在发生变化时重启服务器以应用新的主题。
  3. Markdown文件处理
    • 将Markdown文件转换为Vue组件以便使用Vue插件进行进一步处理。
    • 处理Markdown文件中的死链,并在检测到死链时输出警告信息。
  4. 静态资源处理
    • 在构建过程中对静态资源进行优化,包括注入标记以标识静态字符串的开始和结束。
    • 生成精简版本的JavaScript文件(.lean.js),剥离不必要的静态内容。
  5. 热模块替换 (HMR)
    • 支持Markdown文件的热重载,当Markdown文件发生变化时,更新页面数据并通过WebSocket通知客户端重新加载页面。
    • 修复热更新问题,确保导入的模块能够正确地被清除和重新加载。
  6. 服务配置
    • 配置开发服务器,允许访问特定的文件系统路径。
    • 添加自定义中间件以处理HTML请求,并返回基本的HTML结构。
  7. 插件集成
    • 集成多个插件,包括动态路由插件、本地搜索插件、重写插件、静态数据插件、Web字体插件等,以增强VitePress的功能。
  8. 日志记录
    • 提供详细的日志记录,包括警告信息和操作步骤,便于调试和监控。
  9. 缓存管理
    • 清除缓存以确保每次构建都是最新的状态。

[!NOTE]

  • configResolved: 当Vite完成配置解析后调用,主要用于初始化和设置一些必要的变量和函数。
  • config: 在Vite读取配置文件时调用,用于合并基础配置和用户自定义配置。
  • resolveId: 当Vite需要解析一个模块ID时调用,主要用于处理特定路径的模块。
  • load: 当Vite需要加载一个模块的内容时调用,主要用于加载站点数据。
  • transform: 当Vite需要转换一个模块的内容时调用,主要用于处理Markdown文件并将其转换为Vue组件。
  • renderStart: 当Vite开始渲染页面时调用,主要用于检查和报告死链。
  • configureServer: 当Vite启动开发服务器时调用,主要用于配置开发服务器的行为和监听文件变化。
  • renderChunk: 当Vite渲染一个代码块时调用,主要用于在代码块中注入静态字符串的标记。
  • generateBundle: 当Vite生成最终的构建包时调用,主要用于生成精简版本的JavaScript文件。
  • handleHotUpdate: 当Vite检测到文件热更新时调用,主要用于处理Markdown文件的热重载。
1.createMarkdownToVueRenderFn函数

源码位置:src/node/markdownToVue.ts

import { resolveTitleFromToken } from '@mdit-vue/shared'
import _debug from 'debug'
import fs from 'fs-extra'
import { LRUCache } from 'lru-cache'
import path from 'path'
import type { SiteConfig } from './config'
import {
  createMarkdownRenderer,
  type MarkdownOptions,
  type MarkdownRenderer
} from './markdown/markdown'
import {
  EXTERNAL_URL_RE,
  getLocaleForPath,
  slash,
  treatAsHtml,
  type HeadConfig,
  type MarkdownEnv,
  type PageData
} from './shared'
import { getGitTimestamp } from './utils/getGitTimestamp'
import { processIncludes } from './utils/processIncludes'

const debug = _debug('vitepress:md')
const cache = new LRUCache<string, MarkdownCompileResult>({ max: 1024 })

export interface MarkdownCompileResult {
  vueSrc: string // 转换后的Vue源代码
  pageData: PageData // 页面数据
  deadLinks: { url: string; file: string }[] // 死链信息
  includes: string[] // 包含的文件路径
}

export function clearCache(file?: string) {
  if (!file) {
    cache.clear() // 清除所有缓存
    return
  }

  file = JSON.stringify({ file }).slice(1)
  cache.find((_, key) => key.endsWith(file!) && cache.delete(key)) // 清除特定文件的缓存
}

export async function createMarkdownToVueRenderFn(
  srcDir: string,
  options: MarkdownOptions = {},
  pages: string[],
  isBuild = false,
  base = '/',
  includeLastUpdatedData = false,
  cleanUrls = false,
  siteConfig: SiteConfig | null = null
) {
  const md = await createMarkdownRenderer(
    srcDir,
    options,
    base,
    siteConfig?.logger
  ) // 创建Markdown渲染器

  pages = pages.map((p) => slash(p.replace(/\.md$/, ''))) // 处理页面路径

  const dynamicRoutes = new Map(
    siteConfig?.dynamicRoutes?.routes.map((r) => [
      r.fullPath,
      slash(path.join(srcDir, r.route))
    ]) || []
  ) // 处理解析动态路由

  const rewrites = new Map(
    Object.entries(siteConfig?.rewrites.map || {}).map(([key, value]) => [
      slash(path.join(srcDir, key)),
      slash(path.join(srcDir, value!))
    ]) || []
  ) // 处理解析重写规则

  return async (
    src: string,
    file: string,
    publicDir: string
  ): Promise<MarkdownCompileResult> => {
    const fileOrig = dynamicRoutes.get(file) || file // 获取原始文件路径
    file = rewrites.get(file) || file // 获取重写后的文件路径
    const relativePath = slash(path.relative(srcDir, file)) // 获取相对路径

    const cacheKey = JSON.stringify({ src, file: relativePath })
    if (isBuild || options.cache !== false) {
      const cached = cache.get(cacheKey)
      if (cached) {
        debug(`[cache hit] ${relativePath}`) // 使用缓存
        return cached
      }
    }

    const start = Date.now()

    // resolve params for dynamic routes
    let params
    src = src.replace(
      /^__VP_PARAMS_START([^]+?)__VP_PARAMS_END__/,
      (_, paramsString) => {
        params = JSON.parse(paramsString) // 解析动态路由参数
        return ''
      }
    )

    // resolve includes
    let includes: string[] = []
    src = processIncludes(srcDir, src, fileOrig, includes) // 处理包含的文件

    const localeIndex = getLocaleForPath(siteConfig?.site, relativePath) // 获取语言索引

    // reset env before render
    const env: MarkdownEnv = {
      path: file,
      relativePath,
      cleanUrls,
      includes,
      realPath: fileOrig,
      localeIndex
    }
    const html = md.render(src, env) // 渲染Markdown为HTML
    const {
      frontmatter = {},
      headers = [],
      links = [],
      sfcBlocks,
      title = ''
    } = env

    // validate data.links
    const deadLinks: MarkdownCompileResult['deadLinks'] = []
    const recordDeadLink = (url: string) => {
      deadLinks.push({ url, file: path.relative(srcDir, fileOrig) }) // 记录死链
    }

    function shouldIgnoreDeadLink(url: string) {
      if (!siteConfig?.ignoreDeadLinks) {
        return false
      }
      if (siteConfig.ignoreDeadLinks === true) {
        return true
      }
      if (siteConfig.ignoreDeadLinks === 'localhostLinks') {
        return url.replace(EXTERNAL_URL_RE, '').startsWith('//localhost')
      }

      return siteConfig.ignoreDeadLinks.some((ignore) => {
        if (typeof ignore === 'string') {
          return url === ignore
        }
        if (ignore instanceof RegExp) {
          return ignore.test(url)
        }
        if (typeof ignore === 'function') {
          return ignore(url)
        }
        return false
      })
    }

    if (links) {
      const dir = path.dirname(file)
      for (let url of links) {
        const { pathname } = new URL(url, 'http://a.com')
        if (!treatAsHtml(pathname)) continue

        url = url.replace(/[?#].*$/, '').replace(/\.(html|md)$/, '')
        if (url.endsWith('/')) url += `index`
        let resolved = decodeURIComponent(
          slash(
            url.startsWith('/')
              ? url.slice(1)
              : path.relative(srcDir, path.resolve(dir, url))
          )
        )
        resolved =
          siteConfig?.rewrites.inv[resolved + '.md']?.slice(0, -3) || resolved
        if (
          !pages.includes(resolved) &&
          !fs.existsSync(path.resolve(dir, publicDir, `${resolved}.html`)) &&
          !shouldIgnoreDeadLink(url)
        ) {
          recordDeadLink(url) // 检查并记录死链
        }
      }
    }

    let pageData: PageData = {
      title: inferTitle(md, frontmatter, title), // 推断标题
      titleTemplate: frontmatter.titleTemplate as any,
      description: inferDescription(frontmatter), // 推断描述
      frontmatter,
      headers,
      params,
      relativePath,
      filePath: slash(path.relative(srcDir, fileOrig))
    }

    if (includeLastUpdatedData && frontmatter.lastUpdated !== false) {
      if (frontmatter.lastUpdated instanceof Date) {
        pageData.lastUpdated = +frontmatter.lastUpdated
      } else {
        pageData.lastUpdated = await getGitTimestamp(fileOrig) // 获取最后更新时间
      }
    }

    if (siteConfig?.transformPageData) {
      const dataToMerge = await siteConfig.transformPageData(pageData, {
        siteConfig
      })
      if (dataToMerge) {
        pageData = {
          ...pageData,
          ...dataToMerge
        }
      }
    }

    const vueSrc = [
      ...injectPageDataCode(
        sfcBlocks?.scripts.map((item) => item.content) ?? [],
        pageData
      ),
      `<template><div>${html}</div></template>`, // 生成Vue模板
      ...(sfcBlocks?.styles.map((item) => item.content) ?? []),
      ...(sfcBlocks?.customBlocks.map((item) => item.content) ?? [])
    ].join('\n')

    debug(`[render] ${file} in ${Date.now() - start}ms.`) // 记录渲染时间

    const result = {
      vueSrc,
      pageData,
      deadLinks,
      includes
    }
    if (isBuild || options.cache !== false) {
      cache.set(cacheKey, result) // 设置缓存
    }
    return result
  }
}

const scriptRE = /<\/script>/
const scriptLangTsRE = /<\s*script[^>]*\blang=['"]ts['"][^>]*/
const scriptSetupRE = /<\s*script[^>]*\bsetup\b[^>]*/
const scriptClientRE = /<\s*script[^>]*\bclient\b[^>]*/
const defaultExportRE = /((?:^|\n|;)\s*)export(\s*)default/
const namedDefaultExportRE = /((?:^|\n|;)\s*)export(.+)as(\s*)default/

function injectPageDataCode(tags: string[], data: PageData) {
  const code = `\nexport const __pageData = JSON.parse(${JSON.stringify(
    JSON.stringify(data)
  )})`

  const existingScriptIndex = tags.findIndex((tag) => {
    return (
      scriptRE.test(tag) &&
      !scriptSetupRE.test(tag) &&
      !scriptClientRE.test(tag)
    )
  })

  const isUsingTS = tags.findIndex((tag) => scriptLangTsRE.test(tag)) > -1

  if (existingScriptIndex > -1) {
    const tagSrc = tags[existingScriptIndex]
    // user has <script> tag inside markdown
    // if it doesn't have export default it will error out on build
    const hasDefaultExport =
      defaultExportRE.test(tagSrc) || namedDefaultExportRE.test(tagSrc)
    tags[existingScriptIndex] = tagSrc.replace(
      scriptRE,
      code +
        (hasDefaultExport
          ? ``
          : `\nexport default {name:${JSON.stringify(data.relativePath)}}`) +
        `</script>`
    )
  } else {
    tags.unshift(
      `<script ${
        isUsingTS ? 'lang="ts"' : ''
      }>${code}\nexport default {name:${JSON.stringify(
        data.relativePath
      )}}</script>`
    )
  }

  return tags
}

const inferTitle = (
  md: MarkdownRenderer,
  frontmatter: Record<string, any>,
  title: string
) => {
  if (typeof frontmatter.title === 'string') {
    const titleToken = md.parseInline(frontmatter.title, {})[0]
    if (titleToken) {
      return resolveTitleFromToken(titleToken, {
        shouldAllowHtml: false,
        shouldEscapeText: false
      })
    }
  }
  return title // 推断标题
}

const inferDescription = (frontmatter: Record<string, any>) => {
  const { description, head } = frontmatter

  if (description !== undefined) {
    return description
  }

  return (head && getHeadMetaContent(head, 'description')) || '' // 推断描述
}

const getHeadMetaContent = (
  head: HeadConfig[],
  name: string
): string | undefined => {
  if (!head || !head.length) {
    return undefined
  }

  const meta = head.find(([tag, attrs = {}]) => {
    return tag === 'meta' && attrs.name === name && attrs.content
  })

  return meta && meta[1].content // 获取<head>中的<meta>内容
}

1.1createMarkdownRenderer函数

源码位置:src/node/markdown/markdown.ts

import {
  componentPlugin,
  type ComponentPluginOptions
} from '@mdit-vue/plugin-component'
import {
  frontmatterPlugin,
  type FrontmatterPluginOptions
} from '@mdit-vue/plugin-frontmatter'
import {
  headersPlugin,
  type HeadersPluginOptions
} from '@mdit-vue/plugin-headers'
import { sfcPlugin, type SfcPluginOptions } from '@mdit-vue/plugin-sfc'
import { titlePlugin } from '@mdit-vue/plugin-title'
import { tocPlugin, type TocPluginOptions } from '@mdit-vue/plugin-toc'
import { slugify } from '@mdit-vue/shared'
import type { Options } from 'markdown-it'
import MarkdownIt from 'markdown-it'
import anchorPlugin from 'markdown-it-anchor'
import attrsPlugin from 'markdown-it-attrs'
import { full as emojiPlugin } from 'markdown-it-emoji'
import type { BuiltinTheme, Highlighter } from 'shiki'
import type {
  LanguageInput,
  ShikiTransformer,
  ThemeRegistrationAny
} from '@shikijs/types'
import type { Logger } from 'vite'
import { containerPlugin, type ContainerOptions } from './plugins/containers'
import { gitHubAlertsPlugin } from './plugins/githubAlerts'
import { highlight as createHighlighter } from './plugins/highlight'
import { highlightLinePlugin } from './plugins/highlightLines'
import { imagePlugin, type Options as ImageOptions } from './plugins/image'
import { lineNumberPlugin } from './plugins/lineNumbers'
import { linkPlugin } from './plugins/link'
import { preWrapperPlugin } from './plugins/preWrapper'
import { restoreEntities } from './plugins/restoreEntities'
import { snippetPlugin } from './plugins/snippet'

export type { Header } from '../shared'

export type ThemeOptions =
  | ThemeRegistrationAny
  | BuiltinTheme
  | {
    light: ThemeRegistrationAny | BuiltinTheme
    dark: ThemeRegistrationAny | BuiltinTheme
  }

export interface MarkdownOptions extends Options {
  /* ==================== General Options ==================== */

  /**
   * Setup markdown-it instance before applying plugins
   */
  preConfig?: (md: MarkdownIt) => void
  /**
   * Setup markdown-it instance
   */
  config?: (md: MarkdownIt) => void
  /**
   * Disable cache (experimental)
   */
  cache?: boolean
  externalLinks?: Record<string, string>

  /* ==================== Syntax Highlighting ==================== */

  /**
   * Custom theme for syntax highlighting.
   *
   * You can also pass an object with `light` and `dark` themes to support dual themes.
   *
   * @example { theme: 'github-dark' }
   * @example { theme: { light: 'github-light', dark: 'github-dark' } }
   *
   * You can use an existing theme.
   * @see https://shiki.style/themes
   * Or add your own theme.
   * @see https://shiki.style/guide/load-theme
   */
  theme?: ThemeOptions
  /**
   * Languages for syntax highlighting.
   * @see https://shiki.style/languages
   */
  languages?: LanguageInput[]
  /**
   * Custom language aliases.
   *
   * @example { 'my-lang': 'js' }
   * @see https://shiki.style/guide/load-lang#custom-language-aliases
   */
  languageAlias?: Record<string, string>
  /**
   * Show line numbers in code blocks
   * @default false
   */
  lineNumbers?: boolean
  /**
   * Fallback language when the specified language is not available.
   */
  defaultHighlightLang?: string
  /**
   * Transformers applied to code blocks
   * @see https://shiki.style/guide/transformers
   */
  codeTransformers?: ShikiTransformer[]
  /**
   * Setup Shiki instance
   */
  shikiSetup?: (shiki: Highlighter) => void | Promise<void>
  /**
   * The tooltip text for the copy button in code blocks
   * @default 'Copy Code'
   */
  codeCopyButtonTitle?: string

  /* ==================== Markdown It Plugins ==================== */

  /**
   * Options for `markdown-it-anchor`
   * @see https://github.com/valeriangalliat/markdown-it-anchor
   */
  anchor?: anchorPlugin.AnchorOptions
  /**
   * Options for `markdown-it-attrs`
   * @see https://github.com/arve0/markdown-it-attrs
   */
  attrs?: {
    leftDelimiter?: string
    rightDelimiter?: string
    allowedAttributes?: Array<string | RegExp>
    disable?: boolean
  }
  /**
   * Options for `markdown-it-emoji`
   * @see https://github.com/markdown-it/markdown-it-emoji
   */
  emoji?: {
    defs?: Record<string, string>
    enabled?: string[]
    shortcuts?: Record<string, string | string[]>
  }
  /**
   * Options for `@mdit-vue/plugin-frontmatter`
   * @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-frontmatter
   */
  frontmatter?: FrontmatterPluginOptions
  /**
   * Options for `@mdit-vue/plugin-headers`
   * @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-headers
   */
  headers?: HeadersPluginOptions | boolean
  /**
   * Options for `@mdit-vue/plugin-sfc`
   * @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-sfc
   */
  sfc?: SfcPluginOptions
  /**
   * Options for `@mdit-vue/plugin-toc`
   * @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-toc
   */
  toc?: TocPluginOptions
  /**
   * Options for `@mdit-vue/plugin-component`
   * @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-component
   */
  component?: ComponentPluginOptions
  /**
   * Options for `markdown-it-container`
   * @see https://github.com/markdown-it/markdown-it-container
   */
  container?: ContainerOptions
  /**
   * Math support
   *
   * You need to install `markdown-it-mathjax3` and set `math` to `true` to enable it.
   * You can also pass options to `markdown-it-mathjax3` here.
   * @default false
   * @see https://vitepress.dev/guide/markdown#math-equations
   */
  math?: boolean | any
  image?: ImageOptions
  /**
   * Allows disabling the github alerts plugin
   * @default true
   * @see https://vitepress.dev/guide/markdown#github-flavored-alerts
   */
  gfmAlerts?: boolean
}

export type MarkdownRenderer = MarkdownIt

let md: MarkdownRenderer | undefined
let _disposeHighlighter: (() => void) | undefined

export function disposeMdItInstance() {
  if (md) {
    md = undefined
    _disposeHighlighter?.()
  }
}

/**
 * @experimental
 */
export async function createMarkdownRenderer(
  srcDir: string,
  options: MarkdownOptions = {},
  base = '/',
  logger: Pick<Logger, 'warn'> = console
): Promise<MarkdownRenderer> {
  if (md) return md // 如果已经存在Markdown实例,则直接返回

  const theme = options.theme ?? { light: 'github-light', dark: 'github-dark' } // 设置主题,默认为github-light和github-dark
  const codeCopyButtonTitle = options.codeCopyButtonTitle || 'Copy Code' // 设置代码复制按钮的提示文本
  const hasSingleTheme = typeof theme === 'string' || 'name' in theme // 判断是否有单一主题

  let [highlight, dispose] = options.highlight
    ? [options.highlight, () => { }]
    : await createHighlighter(theme, options, logger) // 创建高亮器

  _disposeHighlighter = dispose // 设置高亮器的清理函数

  md = MarkdownIt({ html: true, linkify: true, highlight, ...options }) // 创建Markdown解析器实例

  md.linkify.set({ fuzzyLink: false }) // 禁用模糊链接识别
  md.use(restoreEntities) // 恢复实体字符

  if (options.preConfig) {
    options.preConfig(md) // 在应用插件之前调用用户配置函数
  }

  // 自定义插件
  md.use(componentPlugin, { ...options.component }) // 使用组件插件
    .use(highlightLinePlugin) // 使用高亮行插件
    .use(preWrapperPlugin, { codeCopyButtonTitle, hasSingleTheme }) // 使用预处理器包装插件
    .use(snippetPlugin, srcDir) // 使用代码片段插件
    .use(containerPlugin, { hasSingleTheme }, options.container) // 使用容器插件
    .use(imagePlugin, options.image) // 使用图像插件
    .use(linkPlugin, { target: '_blank', rel: 'noreferrer', ...options.externalLinks }, base) // 使用链接插件
    .use(lineNumberPlugin, options.lineNumbers) // 使用行号插件

  md.renderer.rules.table_open = function (tokens, idx, options, env, self) {
    return '<table tabindex="0">\n' // 为表格添加tabindex属性
  }

  if (options.gfmAlerts !== false) {
    md.use(gitHubAlertsPlugin) // 使用GitHub风格警告插件
  }

  // 第三方插件
  if (!options.attrs?.disable) {
    md.use(attrsPlugin, options.attrs) // 使用属性插件
  }
  md.use(emojiPlugin, { ...options.emoji }) // 使用表情符号插件

  // mdit-vue 插件
  md.use(anchorPlugin, {
    slugify,
    permalink: anchorPlugin.permalink.linkInsideHeader({
      symbol: '&ZeroWidthSpace;',
      renderAttrs: (slug, state) => {
        // 找到与slug匹配的heading_open令牌
        const idx = state.tokens.findIndex((token) => {
          const attrs = token.attrs
          const id = attrs?.find((attr) => attr[0] === 'id')
          return id && slug === id[1]
        })
        // 获取实际的标题内容
        const title = state.tokens[idx + 1].content
        return {
          'aria-label': `Permalink to "${title}"`
        }
      }
    }),
    ...options.anchor
  } as anchorPlugin.AnchorOptions).use(frontmatterPlugin, {
    ...options.frontmatter
  } as FrontmatterPluginOptions)

  if (options.headers) {
    md.use(headersPlugin, {
      level: [2, 3, 4, 5, 6],
      slugify,
      ...(typeof options.headers === 'boolean' ? undefined : options.headers)
    } as HeadersPluginOptions)
  }

  md.use(sfcPlugin, {
    ...options.sfc
  } as SfcPluginOptions)
    .use(titlePlugin)
    .use(tocPlugin, {
      ...options.toc
    } as TocPluginOptions)

  if (options.math) {
    try {
      const mathPlugin = await import('markdown-it-mathjax3')
      md.use(mathPlugin.default ?? mathPlugin, {
        ...(typeof options.math === 'boolean' ? {} : options.math)
      })
      const orig = md.renderer.rules.math_block!
      md.renderer.rules.math_block = (tokens, idx, options, env, self) => {
        return orig(tokens, idx, options, env, self).replace(
          /^<mjx-container /,
          '<mjx-container tabindex="0" '
        )
      }
    } catch (error) {
      throw new Error(
        'You need to install `markdown-it-mathjax3` to use math support.'
      )
    }
  }

  // 应用用户配置
  if (options.config) {
    options.config(md)
  }

  return md
}
  • Markdown 解析器创建:
    • 创建并配置一个 MarkdownIt 实例,用于解析 Markdown 文档。
    • 支持多种自定义选项,包括语法高亮、插件配置等。
  • 插件集成:
    • 集成了多个内置插件,如 componentPlugin, frontmatterPlugin, headersPlugin, sfcPlugin, tocPlugin, anchorPlugin, attrsPlugin, emojiPlugin, containerPlugin, imagePlugin, linkPlugin, preWrapperPlugin, snippetPlugin, highlightLinePlugin, lineNumberPlugin, 和 gitHubAlertsPlugin
    • 支持第三方插件,如 markdown-it-attrsmarkdown-it-emoji
  • 语法高亮:
    • 使用 Shiki 进行语法高亮,支持多种主题和语言。
    • 提供代码行号显示和代码复制按钮功能。
  • 扩展性:
    • 允许用户通过 preConfigconfig 选项自定义 Markdown 解析器。
    • 支持数学公式渲染(需要安装 markdown-it-mathjax3)。
  • 缓存管理:
    • 提供 disposeMdItInstance 函数来释放资源,确保每次构建都是最新的状态。
  • 辅助功能:
    • 提供多种辅助函数来处理 Markdown 内容,如 restoreEntitieshighlightLinePlugin
2.resolveAliases函数

下节揭晓

相关文章:

  • 使用 Logback 的最佳实践:`logback.xml` 与 `logback-spring.xml` 的区别与用法
  • DIN:引入注意力机制的深度学习推荐系统,
  • Golang官方编程指南
  • 从安装软件到flask框架搭建可视化大屏(一)——创建一个flask页面,零基础也可以学会
  • Linux从0到1——线程池【利用日志Debug】
  • Elasticsearch:将 Ollama 与推理 API 结合使用
  • 【第11章:生成式AI与创意应用—11.3 AI艺术创作的实现与案例分析:DeepArt、GANBreeder等】
  • Leetcode 2466. Count Ways To Build Good Strings
  • 【Day41 LeetCode】单调栈问题
  • 什么是中间件中间件有哪些
  • 可解释性:走向透明与可信的人工智能
  • 浅谈线程安全问题的原因和解决方案
  • langchain学习笔记之消息存储在内存中的实现方法
  • Day3 25/2/16 SUN
  • Linux:用 clang 编译带 sched_ext 功能内核
  • 与传统光伏相比 城电科技的光伏太阳花有什么优势?
  • 最新智能优化算法: 阿尔法进化(Alpha Evolution,AE)算法求解23个经典函数测试集,MATLAB代码
  • 利用亚马逊AI代码助手生成、构建和编译一个游戏应用(下)
  • auto关键字的作用
  • Deepseek高效使用指南
  • 解放日报:抢占科技制高点,赋能新质生产力
  • 剑指3000亿产业规模,机器人“武林大会”背后的无锡“野望”
  • 证监会副主席王建军被查
  • 周劼已任中国航天科技集团有限公司董事、总经理、党组副书记
  • 强制性国家标准《危险化学品企业安全生产标准化通用规范》发布
  • 上海74岁老人宜春旅游时救起落水儿童,“小孩在挣扎容不得多想”