mdc
@nuxtjs/mdc

MDCは通常のMarkdownを強化し、Vueコンポーネントと深く連携するドキュメントを作成できます。

Nuxt MDC

Nuxt MDC

npm versionnpm downloadsLicenseNuxt

MDCは通常のMarkdownを強化し、Vueコンポーネントと深く連携するドキュメントを作成できます。MDCはMarkDown Componentsの略です。

機能

  • Markdown構文とHTMLタグまたはVueコンポーネントを組み合わせる
  • 生成されたコンテンツ(例:Markdownの各段落によって追加された<p>)をラップ解除する
  • 名前付きスロットを持つVueコンポーネントを使用する
  • インラインコンポーネントをサポート
  • ネストされたコンポーネントの非同期レンダリングをサポート
  • インラインHTMLタグに属性とクラスを追加する

MDC構文の詳細については、https://content.nuxt.com/docs/files/markdown を参照してください。

!注意 このパッケージは、Nuxtプロジェクト(標準設定)または任意のVueプロジェクト内で利用できます。

詳細については、以下のVueプロジェクトでのレンダリングを参照してください。

インストール

npx nuxi@latest module add mdc

次に、@nuxtjs/mdcnuxt.config.tsのmodulesセクションに追加します。

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

これで準備完了です!Nuxtプロジェクトでマークダウンファイルの作成とレンダリングを開始できます✨

レンダリング

@nuxtjs/mdcは、マークダウンファイルをレンダリングするための3つのコンポーネントを公開しています。

<MDC>

<MDC>を使用すると、コンポーネント/ページ内でマークダウンコンテンツを直接解析してレンダリングできます。このコンポーネントは生のマークダウンを受け取り、parseMarkdown関数を使用して解析し、<MDCRenderer>でレンダリングします。

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

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

なお、::alertcomponents/mdc/Alert.vueコンポーネントを使用します。

<MDCRenderer>

このコンポーネントは、parseMarkdown関数の結果を受け取り、コンテンツをレンダリングします。例えば、これはBrowserセクションのサンプルコードを拡張したもので、MDCRendererを使用して解析されたマークダウンをレンダリングします。

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>

このコンポーネントは、MDC専用に設計されたVueの<slot/>コンポーネントの代替です。このコンポーネントを使用すると、1つまたは複数のラッパー要素を削除しながら、コンポーネントの子をレンダリングできます。以下の例では、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>

マークダウンのデフォルトの動作では、すべてのテキストを段落で囲みます。MDCはマークダウンの動作を壊すために来たのではなく、MDCの目標はマークダウンを強力にすることです。この例や類似のすべての状況で、<MDCSlot />を使用して不要なラッパーを削除できます。

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

プロズコンポーネント

プロズコンポーネントは、通常のHTMLタグの代わりにレンダリングされるコンポーネントのリストです。たとえば、<p>タグをレンダリングする代わりに、@nuxtjs/mdc<ProseP>コンポーネントをレンダリングします。これは、マークダウンファイルに extraな機能を追加したい場合に便利です。たとえば、コードブロックにコピーボタンを追加できます。

nuxt.config.tsproseオプションをfalseに設定することで、プロズコンポーネントを無効にできます。または、プロズコンポーネントのマップを拡張して独自のコンポーネントを追加することもできます。

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

これらのコンポーネントを自分でカスタマイズするには、制御したいプロズコンポーネントと同じ名前のコンポーネントを作成するだけです。これらのプロズコンポーネントを独自のプロズフォルダーに入れ、MDCが適切にアクセスできるようにNuxtにグローバルに登録するようにしてください。

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

利用可能なプロズコンポーネントのリストです。

タグコンポーネントソース説明
p<ProseP>ProseP.vue段落
h1<ProseH1>ProseH1.vue見出し1
h2<ProseH2>ProseH2.vue見出し2
h3<ProseH3>ProseH3.vue見出し3
h4<ProseH4>ProseH4.vue見出し4
h5<ProseH5>ProseH5.vue見出し5
h6<ProseH6>ProseH6.vue見出し6
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ファイルを解析するための便利なヘルパーを公開しています。parseMarkdown関数を@nuxtjs/mdc/runtimeからインポートし、MDC構文で書かれたマークダウンファイルを解析するために使用できます。

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ヘルパーは、パーサーの動作を制御するためのオプションを2番目の引数として受け入れます。( MDCParseOptionsインターフェース↗︎ を確認してください)。

名前デフォルト説明
remark.plugins{}パーサーのremarkプラグインを登録/設定します。
rehype.options{}remark-rehypeオプションを設定します。
rehype.plugins{}パーサーのrehypeプラグインを登録/設定します。
highlightfalseコードブロックをハイライト表示するかどうかを制御します。カスタムハイライターを提供することもできます。
toc.depth2目次に含める見出しの最大深度。
toc.searchDepth2見出しを検索するネストされたタグの最大深度。

MDCParseOptionsタイプ↗︎ を確認してください。

設定

nuxt.config.jsmdcプロパティを指定することでモジュールを設定できます。以下はデフォルトオプションです。

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を利用してマークダウンをレンダリングするコンポーネントの導入が可能になります。

親のMDCRendererコンポーネントが子非同期コンポーネントの解決を適切に待つためには

  1. 子コンポーネントのすべての機能は、トップレベルのawaitを持つ非同期セットアップ関数内で実行されなければなりません(子に非同期/await動作が必要ない場合、例えばデータフェッチがない場合は、コンポーネントは通常通り解決されます)。
  2. 子コンポーネントのtemplateコンテンツは、組み込みのSuspenseコンポーネントでラップし、suspensible proptrueに設定する必要があります。
    <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アプリケーションでは、useAsyncDataまたはuseFetch呼び出しでimmediate: falseを設定すると、親のMDCRendererが待機するのを妨げ、子コンポーネントがレンダリングを終了する前に親が解決される可能性があり、ハイドレーションエラーやコンテンツの欠落を引き起こします。

簡単な例:非同期コンポーネント

ネストされたMDCブロックコンポーネントは、親コンポーネントが解決する前にデータフェッチを待つなど、ライフサイクルの一部としてトップレベルのasync setup()を利用できます。

例として、プレイグラウンドのAsyncComponentコンポーネントのコードを参照し、動作を確認するには、pnpm devを実行して/async-componentsルートに移動してプレイグラウンドを確認してください。

応用例:MDC「スニペット」

これらのネストされた非同期ブロックコンポーネントがいかに強力であるかを示すために、ユーザーがプロジェクト内でマークダウンドキュメントのサブセットを定義し、それを親ドキュメントで再利用可能な「スニペット」として利用できるようにすることができます。

プロジェクト内にカスタムブロックコンポーネントを作成し、APIからスニペットのマークダウンコンテンツをフェッチし、parseMarkdownを使用してastノードを取得し、独自のMDCまたはMDCRendererコンポーネントでレンダリングします。

例として、プレイグラウンドのPageSnippetコンポーネントのコードを参照し、動作を確認するには、pnpm devを実行して/async-components/advancedルートに移動してプレイグラウンドを確認してください。

再帰の処理

プロジェクトで「再利用可能なスニペット」のようなアプローチを実装している場合、ネストされたMDCRendererがコンポーネントツリー内のどこかで同じコンテンツを持つ別の子をロードしようとする(つまり、自身をインポートする)再帰的なスニペットの使用を防ぐ必要があるでしょう。この場合、アプリケーションは無限ループに陥ります。

これを回避する1つの方法は、Vueのprovide/injectを利用して、レンダリングされた「スニペット」の履歴を渡すことです。これにより、子が再帰的に呼び出されているかどうかを適切に判断し、連鎖を停止できます。これは、parseMarkdown関数を呼び出した後、astドキュメントノードを解析し、DOMにコンテンツをレンダリングする前にastから再帰的なノードツリーを削除する方法と組み合わせて使用できます。

このパターンで無限ループと再帰を防ぐ方法の例については、プレイグラウンドの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)を更新し、モジュールのインポートをスタブファイルにエイリアスします。

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'),
    }
  }
})

使用方法

次に、マークダウンコンテンツの解析と、Shikiを使ったコードブロックの構文ハイライト処理を行うための新しいVueコンポーザブルを作成しましょう。

// 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コンポーザブルと、エクスポートされた型インターフェースをホストプロジェクトのVueコンポーネントにインポートし、それらを利用して生のマークダウンを処理し、<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を使用して開発サーバーを起動します。

ライセンス

MITライセンス

Copyright (c) NuxtLabs