[Nuxt Content] v2のMarkdownでMermaid記法を使えるようにする
2024/08/08
masyus
このサイトには技術系記事が多く、
- DBスキーマ
- フローチャート
- シーケンス図
- etc.
で解説したいケースが多々ありました。Mermaidはこうした情報をJSで簡単にビジュアライズすることができて便利ですが、Nuxt Content v2へ導入するにあたり克服すべき課題がありました。今回はその課題と解消方法について解説します。
バージョン情報
- Nuxt: 3.11.1
- Nuxt Content: 2.12.1
- TypeScript: 5.4.3
- Mermaid: 10.9.1
Nuxt Content v2にMermaidを導入する方法
課題を解説する前に、先に最終的な導入方法を解説します。まだ試行錯誤している最中ではありますが、私はMermaidをBlock Componentsに切り出して導入する方式を取りました。
::mermaid
sequenceDiagram
participant Alice
participant Bob
Alice->>John: Hello John, how are you?
loop HealthCheck
John->>John: Fight against hypochondria
end
Note right of John: Rational thoughts <br/>prevail!
John-->>Alice: Great!
John->>Bob: How about you?
Bob-->>John: Jolly good!
::
ルートディレクトリ配下のcontent/articles/hoge.mdに当記事冒頭のシーケンス図を、上記のようにMermaid記法で書き出します。
<script setup lang="ts">
import mermaid from 'mermaid'
const show: Ref<boolean> = ref(false)
const content = ref<HTMLDivElement | null>(null)
onMounted(async() => {
show.value = true
await nextTick()
if (content.value !== null) {
content.value.innerHTML = content.value.innerHTML.replace(/<span>/g, '[').replace(/<\/span>/g, ']')
}
mermaid.initialize({startOnLoad: true })
mermaid.init()
})
</script>
<template>
<div class="w-full my-4">
<div v-if="show" class="mermaid flex justify-center items-center" ref="content">
<ContentSlot :use="$slots.default" unwrap="p" />
</div>
<div v-else>
<Spinner />
</div>
</div>
</template>
次にMermaid.vueの中身です。Block Components且つ記事ページでのみ使うため、ルートディレクトリ配下のcomponents/content/Mermaid.vueに上記を書き出しました。CSSのクラスはTailwind CSSを使っていますが、状況に応じて適宜書き換えてください。
<script setup lang="ts">
</script>
<template>
<div class="flex justify-center items-center h-[200px]">
<div class="animate-spin rounded-full w-20 h-20 border-8 border-gray-300 border-t-gray-800"></div>
</div>
</template>
components/Spinner.vueはいろいろデザイン方法あると思いますが、私はTailwind CSSで上記のように実装しました。CLSをなるべく抑えたかったので適当にh-[200px]
を入れていますが、より良い方法を模索中です。
Mermaid導入のための調査
Googleにて「Nuxt Content v2 Mermaid」で検索した結果の最上位にヒットしたのが、nuxt/contentのGitHubのIssuesに挙がっていた Mermaid Support #1866 でした。こちらのIssueを読むに、Mermaidの導入は
- Block Componentsを使う
- remarkPluginsを使う
のいずれかで自作できそうなことがわかりました。Issueにおける最終的な解決案は1.でしたので、私もBlock Componentsを使う方式を採用した次第です。
ただ、Issueに記載の「動いた!」という事例を手元で再現してみたところ、どうも中途半端でバグっていることが分かりました。以下で解説します。
Mermaid導入で参考にしたコード
参考にしたコードはIssueに記載されていた
import mermaid from "mermaid/dist/mermaid"
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.provide('mermaid', () => mermaid)
})
<template>
<div class="mermaid" v-if="show">
<slot></slot>
</div>
</template>
<script setup>
let show = ref(false);
const { $mermaid } = useNuxtApp()
onMounted( async() => {
show.value = true
$mermaid().initialize({startOnLoad: true })
await nextTick()
$mermaid().init();
})
</script>
です。処理の流れを時系列で読み解くと、下記のようになります。
- Mermaidをimportしつつプラグイン化して、アプリケーション内のどこからでも使えるようにする
- Markdownで記述されたMermaid記法のテキストがslotにレンダリングされる
- 2.のレンダリングが完了するまでの間はslotの中身を表示しない(レンダリング完了前に
mermaid()
を呼び出すと、slotの中身をパースできないため) - マウントが完了し、show.valueがtrueに切り替わりDOMの更新も完了した上で
mermaid()
を実行したいので、その手前でnextTick()
を呼び出す mermaid().init()
を実行し、class="mermaid"が適用されているDOMのinnerHTMLをパースしてダイアグラムを書き出す
この流れ自体は特に問題ありませんでした。強いて挙げるならこのサイトでMermaidが必要となるのは記事ページのみでしたため、pluginにせずBlock Components内でimportするスタイルを取ったくらいでしょうか。書き換えると下記のようになりました。
<script setup lang="ts">
import mermaid from 'mermaid'
const show: Ref<boolean> = ref(false)
onMounted(async() => {
show.value = true
await nextTick()
mermaid.initialize({startOnLoad: true })
mermaid.init()
})
</script>
<template>
<div class="w-full my-4">
<div v-if="show" class="mermaid flex justify-center items-center">
<slot />
</div>
<div v-else>
<Spinner />
</div>
</div>
</template>
ただ、これだとmermaid().init()
時にMermaidでSyntax error in textが発生しました。以下で解説します。
課題1: Prose ComponentのProsePでslotがラッピングされてしまう
Block Componentsを利用している為に、デフォルトでは<p></p>
で囲われた状態のMarkdownテキストが<slot />
に渡ってしまいます。これがSyntax errorになる原因でした。
解決策: unwrapを使い、pタグでslotがラッピングされないようにする
幸いなことにNuxt Content v2ではspecial slotとして<ContentSlot />
が提供されており、ContentSlotの1機能であるunwrapを使うことで解消できました。
<ContentSlot :use="$slots.default" unwrap="p" />
このように記述することで、pタグで囲われないように指定することができます。
課題2: []が<span></span>タグに変換されてしまう
Mermaid記法はフローチャートやシーケンス図を表すための基本構成要素として
- nodes: 幾何学的形状(例: 四角や三角など)
- edges: 矢印もしくは線(例: →など)
があります。今回課題となったのは
nodesのA node with textと、Nuxt Content v2のSpan Textの食い合わせが悪かったこと
です。A node with textはnodeに対し[]
を使うことでテキストを補足するためのものですが、Nuxt Content v2のMarkdownではSpan Text機能により[]
が<span></span>
に変換されてしまいます。そうなると、slotの中身にspanタグが混入し、mermaid().init()
時にSyntax error in textが発生してしまいます。これをどのように回避するかで苦戦しました。
解決策: <span></span>タグを[]に変換し直す
unwrap="p,span"
と指定することも考えましたが、unwrapでspanを指定すると[]
自体が無くなってしまうため、Mermaid記法でA node with textの表現が使えなくなってしまいます。
そのため別の方策を検討した結果、一度slotにレンダリングされたMarkdownコンテンツの中に混入した<span></span>
タグを[]
に変換し直す処理を加えることにしました。改良したコードは下記です。
<script setup lang="ts">
import mermaid from 'mermaid'
const show: Ref<boolean> = ref(false)
const content = ref<HTMLDivElement | null>(null)
onMounted(async() => {
show.value = true
await nextTick()
if (content.value !== null) {
content.value.innerHTML = content.value.innerHTML.replace(/<span>/g, '[').replace(/<\/span>/g, ']')
}
mermaid.initialize({startOnLoad: true })
mermaid.init()
})
</script>
<template>
<div class="w-full my-4">
<div v-if="show" class="mermaid flex justify-center items-center" ref="content">
<ContentSlot :use="$slots.default" unwrap="p" />
</div>
<div v-else>
<Spinner />
</div>
</div>
</template>
<ContentSlot />
でレンダリングされる内容を、親のdivに仕込んだref="content"
にてJSでリアクティブに取り扱えるようにします。あとはinnerHTMLの中身を煮るなり焼くなりです。
あとがき
こうして要点まとめて書き出してみると簡単ですが、起きてる事象を把握して原理を知り、原因を特定するまではそこそこ時間を使いました。個人的にNuxt Contentは慣れ親しんでおり且つ好きなので、これを機にNuxt Contentを使う方が増えたら嬉しい限りです。