うさぎのメモ帳

Nuxtでマークダウンを使う (markdown-it x Prism)

2019年09月21日 - 2019年09月22日

あらすじ

このメモ帳は Nuxt と Strapi で作られています。
Strapi のリッチテキスト形式のフィールドで記事を書いているのですが、このフィールドはマークダウンで入力できるようになっています。そのマークダウンをHTMLに変換して表示させる必要がありました。

入力画面

markdown-it は何を使おう

Nuxtコミュニティの開発する @nuxtjs/markdownit を使うのが楽なのですが、外部リンクの場合は自動的に target="_blank" を付ける方法がわからなかったので、本家の markdown-it をNuxtのプラグインにして使うことにしました。
直接使った方がカスタマイズがしやすいということはよくあります。

どうやって使うの?

markdown-itで外部へのリンクのみtarget="_blank"する方法 - Izm Log を参考にして target="_blank" を付与したのと、それに rel="noopener noreferrer" を追加しました。
なぜ追加したかは リンクのへの rel=noopener 付与による Tabnabbing 対策 | blog.jxck.io より。

まずはマークダウンを使うために maridown-it を持ってきて、

yarn markdown-it

Nuxt のプラグインとして読み込ませます。

plugins/markdown-it.js
※最初は .ts で読み込ませようとしていましたが、types が解決できなかったのでJS読み込みにしました

const md = require('markdown-it')({
  html: true,
  linkify: true,
  breaks: true,
  typography: true
})

export default (_context, inject) => {
  const defaultRender = md.renderer.rules.link_open || function (tokens, idx, options, _env, self) {
    return self.renderToken(tokens, idx, options)
  }

  md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
    if (tokens[idx].attrs[0][1].match('http')) {
      const tIdx = tokens[idx].attrIndex('target')
      if (tIdx < 0) {
        tokens[idx].attrPush(['target', '_blank'])
      } else {
        tokens[idx].attrs[tIdx][1] = '_blank'
      }

      const rIdx = tokens[idx].attrIndex('rel')
      if (rIdx < 0) {
        tokens[idx].attrPush(['rel', 'noopener noreferrer'])
      } else {
        tokens[idx].attrs[rIdx][1] = 'noopener noreferrer'
      }
    }
    return defaultRender(tokens, idx, options, env, self)
  }

  inject('md', md)
}

nuxt.config.js

export default {
  ...
  plugins: [
    ...
+    '~/plugins/markdown-it'
  ],
  ...

これをしておけば、あとは .vue 側で v-html を使って、

<div class="u-format" v-html="$md.render(memo.content)" />

みたいな感じで使うことができるようになります。

便利な機能を追加 (markdown-it-attrs, markdown-it-div)

マークダウンの文中にHTMLタグやID/クラスを付与したいときに、便利なパッケージを2つ追加しようと思います。
この2つのパッケージは modules/packages/markdownit at master · nuxt-community/modules で紹介されていたので導入してみました。

まずはパッケージをGETします。

yarn markdown-it-attrs markdown-it-div

そして先程自作したプラグインを拡張します。

plugins/markdown-it.js

const md = require('markdown-it')({
  ...
  typography: true
})
+  .use(require('markdown-it-div'))
+  .use(require('markdown-it-attrs'))

.use で指定する、これだけ。use(使う)。わかりやすい。

コードの表記に色を付けたい(シンタックスハイライト - Prism)

何のプラグインを使おうかと調べて highlight.jsPrism が候補に上がりましたが、ハイライトが必要な言語だけを選んで使えて、容量が軽そうなので Prism にしました。

シンタックスハイライトは、大体JSとCSSの2つのファイルを読み込みます。
JSの役割は『テキストを解析して、クラスのついた span タグを付与する』ことになり、もう一方のCSSでそのHTMLにスタイルを適用するという流れになります。

[JSの仕事] コードを解析してクラスを付与する

まずはJSから進めていこうと思います。
必要なパッケージを読み込みます。

yarn add prismjs

そして例のごとく自作パッケージを拡張します。

plugins/markdown-it.js

+const Prism = require('prismjs')
+require('prismjs/components/prism-typescript')  // ※1
+require('prismjs/components/prism-diff')
+require('prismjs/components/prism-bash')
+require('prismjs/components/prism-json')
+require('prismjs/components/prism-graphql')

const md = require('markdown-it')({
  ...
  typography: true,
+  highlight (str, lang) { // ※2
+    if (lang && Prism.languages[lang]) {
+      try {
+        return `<pre class="language-${lang}"><code>${Prism.highlight(str, Prism.languages[lang], lang)}</code></pre>`
+      } catch (e) {}
+    }
+    return `<pre class="language-plain"><code>${md.utils.escapeHtml(str)}</code></pre>`
+  }
})
  • ※1
    supported-languages やローカルの /node_modules/prismjs/components/ 以下を見て、使いたい言語のJSを require() するようにします。
    すると Prism.languages[lang] が利用できるようになります。
    例えば require('/prismjs/components/prism-typescript') とすると、 Prism.languages.typescriptPrism.languages.ts が使えるようになるようなイメージです。

  • ※2
    markdown-it#syntax-highlighting を参考にしました。
    $md.render()にて渡したテキストの ``` で囲んだテキストが highlight(str, lang)strに渡されて実行されます。
    第2引数のlangには ```js と指定した場合の js 部分が渡されます。
    Prism.languages[lang]でコードが見つからない場合は、pre.language-plainとなるようにしています。

コード部分はこれでOK。

[CSSの仕事] 解析されたコードに色を付ける

あとは Download ▲ Prism にある右の円型のボタンで好きなテーマを選んでから、最下部の [DOWNLOAD CSS] でテーマをダウンロードします。

このファイルを assets/ に入れて、nuxt.config.js からCSSを読み込むように設定すれば完了です。私はダウンロードしたCSSを assets/css/prism.css としました。

nuxt.config.js

export default {
  ...
  css: [
    ...
+    '~/assets/css/prism.css'
  ],
  ...
}

これで完了です。
お疲れさまでした。