mdc
@nuxtjs/mdc

MDC 强化了常规的 Markdown,可以编写与任何 Vue 组件深度交互的文档。

Nuxt MDC

Nuxt MDC

npm versionnpm downloadsLicenseNuxt

MDC 强化了常规的 Markdown,可以编写与任何 Vue 组件深度交互的文档。MDC 代表 MarkDown 组件。

功能

  • 将 Markdown 语法与 HTML 标签或 Vue 组件混合使用
  • 解包任何生成的内容(例如:每个 Markdown 段落添加的 <p>
  • 使用带命名插槽的 Vue 组件
  • 支持内联组件
  • 支持嵌套组件的异步渲染
  • 向内联 HTML 标签添加属性和类

https://content.nuxtjs.org.cn/docs/files/markdown 上了解更多关于 MDC 语法的信息

!注意 您可以在 Nuxt 项目(标准配置)或任何 Vue 项目中使用此包。

有关更多信息,请参阅下面的 在 Vue 项目中渲染

安装

npx nuxi@latest module add mdc

然后,将 @nuxtjs/mdc 添加到您的 nuxt.config.ts 的 modules 部分

nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/mdc']
})

就是这样!您可以在 Nuxt 项目中开始编写和渲染 markdown 文件了 ✨

渲染

@nuxtjs/mdc 暴露了三个组件来渲染 markdown 文件。

<MDC>

使用 <MDC>,您可以在组件/页面内部直接解析和渲染 markdown 内容。此组件接收原始 markdown,使用 parseMarkdown 函数解析它,然后使用 <MDCRenderer> 渲染它。

<script setup lang="ts">
const md = `
::alert
Hello MDC
::
`
</script>

<template>
  <MDC :value="md" tag="article" />
</template>

请注意,::alert 将使用 components/mdc/Alert.vue 组件。

<MDCRenderer>

此组件将接收 parseMarkdown 函数的结果并渲染内容。例如,这是 Browser 部分 中示例代码的扩展版本,它使用 MDCRenderer 来渲染解析后的 markdown。

mdc-test.vue
<script setup lang="ts">
import { parseMarkdown } from '@nuxtjs/mdc/runtime'

const { data: ast } = await useAsyncData('markdown', () => parseMarkdown('::alert\nMissing markdown input\n::'))
</script>

<template>
  <MDCRenderer :body="ast.body" :data="ast.data" />
</template>

<MDCSlot>

此组件是 Vue 的 <slot/> 组件的替代品,专为 MDC 设计。使用此组件,您可以渲染组件的子元素,同时移除一个或多个包装元素。在下面的示例中,Alert 组件接收文本及其默认插槽(子元素)。但是,如果组件使用正常的 <slot/> 渲染此插槽,它将在文本周围渲染一个 <p> 元素。

markdown.md
::alert
This is an Alert
::
Alert.vue
<template>
  <div class="alert">
    <!-- Slot will render <p> tag around the text -->
    <slot />
  </div>
</template>

这是 markdown 的默认行为,将每个文本都包装在一个段落中。MDC 并非旨在打破 markdown 行为;相反,MDC 的目标是使 markdown 更强大。在此示例和所有类似情况下,您可以使用 <MDCSlot /> 来移除不需要的包装器。

Alert.vue
<template>
  <div class="alert">
    <!-- MDCSlot will only render the actual text without the wrapping <p> -->
    <MDCSlot unwrap="p" />
  </div>
</template>

散文组件

散文组件是取代常规 HTML 标签的组件列表。例如,@nuxtjs/mdc 不会渲染 <p> 标签,而是渲染 <ProseP> 组件。当您想为 Markdown 文件添加额外功能时,这非常有用。例如,您可以为代码块添加一个 复制 按钮。

您可以通过在 nuxt.config.ts 中将 prose 选项设置为 false 来禁用散文组件。或者扩展散文组件的映射以添加您自己的组件。

nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/mdc'],
  mdc: {
    components: {
      prose: false, // Disable predefined prose components
      map: {
        p: 'MyCustomPComponent'
      }
    }
  }
})

为了自定义这些组件,您只需创建一个与您要控制的散文组件同名的组件即可。请务必将这些散文组件放在它们自己的散文文件夹中,并告知 nuxt 全局注册它们,以便 MDC 能够正确访问。

export default defineNuxtConfig({
  modules: ['@nuxtjs/mdc'],
  mdc: {
    components: {
      prose: true
    }
  },
  components: {
    global: true,
    path: './components/prose'
  }
})

以下是可用散文组件的列表

标签组件源文件描述
p<ProseP>ProseP.vue段落
h1<ProseH1>ProseH1.vue一级标题
h2<ProseH2>ProseH2.vue二级标题
h3<ProseH3>ProseH3.vue三级标题
h4<ProseH4>ProseH4.vue四级标题
h5<ProseH5>ProseH5.vue五级标题
h6<ProseH6>ProseH6.vue六级标题
ul<ProseUl>ProseUl.vue无序列表
ol<ProseOl>ProseOl.vue有序列表
li<ProseLi>ProseLi.vue列表项
blockquote<ProseBlockquote>ProseBlockquote.vue引用块
hr<ProseHr>ProseHr.vue水平线
pre<ProsePre>ProsePre.vue预格式化文本
code<ProseCode>ProseCode.vue代码块
table<ProseTable>ProseTable.vue表格
thead<ProseThead>ProseThead.vue表头
tbody<ProseTbody>ProseTbody.vue表体
tr<ProseTr>ProseTr.vue表行
th<ProseTh>ProseTh.vue表头单元格
td<ProseTd>ProseTd.vue表数据单元格
a<ProseA>ProseA.vue锚点链接
img<ProseImg>ProseImg.vue图像
em<ProseEm>ProseEm.vue强调
strong<ProseStrong>ProseStrong.vue加粗

解析 Markdown

Nuxt MDC 暴露了一个方便的辅助函数来解析 MDC 文件。您可以从 @nuxtjs/mdc/runtime 导入 parseMarkdown 函数,并使用它来解析用 MDC 语法编写的 markdown 文件。

Node.js

// server/api/parse-mdc.ts
import { parseMarkdown } from '@nuxtjs/mdc/runtime'

export default eventHandler(async () => {
  const mdc = [
    '# Hello MDC',
    '',
    '::alert',
    'This is an Alert',
    '::'
  ].join('\n')

  const ast = await parseMarkdown(mdc)

  return ast
})

浏览器

parseMarkdown 函数是一个通用的辅助函数,您也可以在浏览器中使用它,例如在 Vue 组件内部。

<script setup lang="ts">
import { parseMarkdown } from '@nuxtjs/mdc/runtime'

const { data: ast } = await useAsyncData('markdown', () => parseMarkdown('::alert\nMissing markdown input\n::'))
</script>

<template>
  <MDCRenderer :body="ast.body" :data="ast.data" />
</template>

选项

parseMarkdown 辅助函数也接受选项作为第二个参数来控制解析器的行为。(查看 MDCParseOptions 接口↗︎)。

名称默认描述
remark.plugins{}注册/配置解析器的 remark 插件。
rehype.options{}配置 remark-rehype 选项。
rehype.plugins{}注册/配置解析器的 rehype 插件。
highlightfalse控制代码块是否应该高亮显示。您还可以提供自定义高亮器。
toc.depth2目录中包含的最大标题深度。
toc.searchDepth2搜索标题的嵌套标签的最大深度。

查看 MDCParseOptions 类型↗︎

配置

您可以通过在 nuxt.config.js 中提供 mdc 属性来配置模块;以下是默认选项

import { defineNuxtConfig } from 'nuxt/config'

export default defineNuxtConfig({
  modules: ['@nuxtjs/mdc'],
  mdc: {
    remarkPlugins: {
      // Register/Configure remark plugin to extend the parser, e.g.
      // 'remark-math': {
      //   src: 'remark-math',
      //   options: {
      //     singleDollarTextMath: true,
      //   },
      // },
    },
    rehypePlugins: {
      // Register/Configure rehype plugin to extend the parser, e.g.
      // 'rehype-mathjax': {
      //   src: 'rehype-mathjax',
      //   options: {
      //     tex: {
      //       inlineMath: [['$', '$'], ['\\(', '\\)']],
      //       displayMath: [['$$', '$$'], ['\\[', '\\]']],
      //     },
      //   },
      // },
    },
    headings: {
      anchorLinks: {
        // Enable/Disable heading anchor links. { h1: true, h2: false }
      }
    },
    highlight: false, // Control syntax highlighting
    components: {
      prose: false, // Add predefined map to render Prose Components instead of HTML tags, like p, ul, code
      map: {
        // This map will be used in `<MDCRenderer>` to control rendered components
      }
    }
  }
})

查看 ModuleOptions 类型↗︎


渲染嵌套异步组件

MDCRenderer 还支持渲染*嵌套*异步组件,通过等待其树中的任何子组件解析其顶层 async setup()

此行为允许渲染异步 MDC 块组件(例如通过 defineAsyncComponent)以及引入组件,这些组件本身内部利用 MDCRenderer 在父组件解析之前渲染 markdown。

为了让父 MDCRenderer 组件正确等待子异步组件解析

  1. 子组件中的所有功能**必须**在带有顶层 await 的异步 setup 函数中执行(如果子组件不需要异步/await 行为,例如没有数据获取,那么组件将正常解析)。
  2. 子组件的 template 内容**应该**用内置的 Suspense 组件 包裹,并将 suspensible prop 设置为 true
    <template>
      <Suspense suspensible>
        <pre>{{ data }}</pre>
      </Suspense>
    </template>
    
    <script setup>
    const { data } = await useAsyncData(..., {
      immediate: true, // This is the default, but is required for this functionality
    })
    </script>
    

    在 Nuxt 应用程序中,这意味着将任何 useAsyncDatauseFetch 调用的 immediate: false 设置将*阻止*父 MDCRenderer 等待,并且父组件可能会在子组件完成渲染之前解析,从而导致水合错误或内容丢失。

简单示例:异步组件

您的嵌套 MDC 块组件可以利用顶层 async setup() 作为其生命周期的一部分,例如在允许父组件解析之前等待数据获取。

请参阅 playground 中的代码 AsyncComponent 组件 作为示例,并查看实际行为,通过运行 pnpm dev 并导航到 /async-components 路由来查看 playground。

高级示例:MDC“片段”

为了演示这些嵌套异步块组件的强大功能,您可以允许用户在项目中定义一部分 Markdown 文档,这些文档将在父文档中用作可重用的“片段”。

您将在项目中创建一个自定义块组件,该组件处理从 API 获取片段 Markdown 内容,使用 parseMarkdown 获取 ast 节点,并在其自己的 MDCMDCRenderer 组件中渲染它。

请参阅 playground 中的代码 PageSnippet 组件 作为示例,并查看实际行为,通过运行 pnpm dev 并导航到 /async-components/advanced 路由来查看 playground。

处理递归

如果您的项目实现了“可重用片段”类型的方法,您可能希望防止使用递归片段,即嵌套的 MDCRenderer 试图在其组件树中的某个位置加载另一个具有相同内容的子组件(意味着导入自身),您的应用程序将被抛入无限循环。

解决此问题的一种方法是利用 Vue 的 provide/inject 来传递已渲染“片段”的历史记录,以便子组件可以正确判断它是否被递归调用,并停止链式调用。这可以与调用 parseMarkdown 函数后解析 ast 文档节点结合使用,以便在 DOM 中渲染内容之前从 ast 中剥离递归节点树。

有关如何使用此模式防止无限循环和递归的示例,请参阅 playground 的 PageSnippet 组件 中的代码。


在 Vue 项目中渲染

<MDCRenderer> 组件结合一些导出的包实用程序也可以在普通(非 Nuxt)Vue 项目中使用。

要在您的标准 Vue 项目中实现,请按照以下说明操作。

安装包

按照 上面的安装说明 进行操作,忽略将 Nuxt 模块添加到 nuxt.config.ts 文件的步骤。

模拟 Nuxt 模块导入

由于您没有使用 Nuxt,您需要在 Vue 项目的 Vite 配置文件中模拟模块的几个导入。这是为了避免模块尝试访问 Nuxt 特定导入时出现错误。

在 Vue 项目的根目录中创建一个新文件,例如 stub-mdc-imports.js,并添加以下内容

// stub-mdc-imports.js
export default {}

接下来,更新您的 Vue 项目的 Vite 配置文件(例如 vite.config.ts)以将模块的导入别名到 stub 文件

import { defineConfig } from 'vite'
import path from 'path'

export default defineConfig({
  resolve: {
    alias: {
      '#mdc-imports': path.resolve(__dirname, './stub-mdc-imports.js'),
      '#mdc-configs': path.resolve(__dirname, './stub-mdc-imports.js'),
    }
  }
})

使用

接下来,让我们创建一个新的 Vue composable 来处理 Markdown 内容的解析,以及使用 Shiki 为代码块添加语法高亮。

// composables/useMarkdownParser.ts
// Import package exports
import {
  createMarkdownParser,
  rehypeHighlight,
  createShikiHighlighter,
} from '@nuxtjs/mdc/runtime'
// Import desired Shiki themes and languages
import MaterialThemePalenight from '@shikijs/themes/material-theme-palenight'
import HtmlLang from '@shikijs/langs/html'
import MdcLang from '@shikijs/langs/mdc'
import TsLang from '@shikijs/langs/typescript'
import VueLang from '@shikijs/langs/vue'
import ScssLang from '@shikijs/langs/scss'
import YamlLang from '@shikijs/langs/yaml'

export default function useMarkdownParser() {
  let parser: Awaited<ReturnType<typeof createMarkdownParser>>

  const parse = async (markdown: string) => {
    if (!parser) {
      parser = await createMarkdownParser({
        rehype: {
          plugins: {
            highlight: {
              instance: rehypeHighlight,
              options: {
                // Pass in your desired theme(s)
                theme: 'material-theme-palenight',
                // Create the Shiki highlighter
                highlighter: createShikiHighlighter({
                  bundledThemes: {
                    'material-theme-palenight': MaterialThemePalenight,
                  },
                  // Configure the bundled languages
                  bundledLangs: {
                    html: HtmlLang,
                    mdc: MdcLang,
                    vue: VueLang,
                    yml: YamlLang,
                    scss: ScssLang,
                    ts: TsLang,
                    typescript: TsLang,
                  },
                }),
              },
            },
          },
        },
      })
    }
    return parser(markdown)
  }

  return parse
}

现在将我们刚刚创建的 useMarkdownParser composable 以及导出的类型接口导入到您的宿主项目的 Vue 组件中,并利用它们来处理原始 Markdown 并初始化 <MDCRenderer> 组件。

<script setup lang="ts">
import { onBeforeMount, ref, watch } from 'vue'
// Import package exports
import MDCRenderer from '@nuxtjs/mdc/runtime/components/MDCRenderer.vue'
import type { MDCParserResult } from '@nuxtjs/mdc'
import useMarkdownParser from './composables/useMarkdownParser';

const md = ref(`
# Just a Vue app

This is markdown content rendered via the \`<MDCRenderer>\` component, including MDC below.

::alert
Hello MDC
::

\`\`\`ts
const a = 1;
\`\`\`
`);

const ast = ref<MDCParserResult | null>(null)
const parse = useMarkdownParser()

onBeforeMount(async () => {
  ast.value = await parse(md.value)
})
</script>

<template>
  <Suspense>
    <MDCRenderer v-if="ast?.body" :body="ast.body" :data="ast.data" />
  </Suspense>
</template>

贡献

您可以使用 StackBlitz 在线深入研究此模块

Edit @nuxtjs/mdc

或本地

  1. 克隆此仓库
  2. 使用 pnpm install 安装依赖
  3. 使用 pnpm dev 启动开发服务器

许可证

麻省理工学院许可证

版权所有 (c) NuxtLabs