Markdown-itのプラグインの作り方
先日、VSCode ではてなブログの記事を書きやすくするための拡張機能を作りました。
開発時のあれこれは別の記事にも書いています。
さて、VSCode 拡張機能の Markdown プレビューはextendMarkdownItという関数を定義するだけで拡張することが出来ます。
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
  return {
    extendMarkdownIt(md: any) {
      return md.use(require('markdown-it-emoji'));
    }
  };
}
"markdown-it-emoji"のようなプラグインを作って差し込むだけで、VSCode の Markdown を拡張できます。
これを使って拡張機能を開発したいと思い、「markdown-it plugin tutorial」とかで検索してみたのですが、全然よい情報がありません。
検索結果の一番上に出てきた「How to write plugins?」という Markdown-it の Issue には以下のように書かれています。
Interested in a how-to tutorial. Awesome. We are not! So you can either write it yourself, or hire somebody.
https://github.com/markdown-it/markdown-it/issues/10#issuecomment-68269384
いっそ気持ちがいい感じですね。OSS でのユーザー要求への対応は何かと問題になりますが、むしろこれくらいの姿勢が良いのかもしれません。
とはいえ情報が少ないのも困りものなので、記事を書くことで今後作る方の参考になればと思います。
公式ドキュメント
公式が提供しているドキュメントのうち、以下がおそらく一番重要です。
既存の Markdown-it 拡張のコードを読みながら、architecture.md を読み返すのを繰り返すことで何となく分かってくるはずです。
プラグインの作り方
基本的な作り方からです。
プラグインの基礎
まず、第一引数にMarkdownItのインスタンスを受け取る関数を定義します。
import type { PluginSimple } from "markdown-it";
const markdownItPlugin: PluginSimple = (md) => {
  // ここに処理を書く
};
export = markdownItPlugin;
これをmd.use(require("./plugin"))のようにすると Markdown-it に組み込むことができます。
Renderer をカスタマイズする
次に処理を書きます。最も簡単なのは、Renderer をカスタマイズすることです。
Renderer は入力となる Markdown の内容を解析した後、解析結果をどのように HTML の文字列として出力するかを規定するものです。
Renderer はmd.renderer.rules[name]に登録されています。name は解析結果のトークンが持つtypeの値のようで、textやimageなどです。
例えば、textに対応する Renderer は以下のように実装されています。
default_rules.text = function (tokens, idx /*, options, env */) {
  return escapeHtml(tokens[idx].content);
};
tokens[idx]の内容をもとにした文字列を返すのが Renderer の役割であるとわかります。
なお、公式ドキュメントにはlink_openの Renderer をカスタマイズする例が載っています。
// Remember old renderer, if overridden, or proxy to default renderer
var 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 you are sure other plugins can't add `target` - drop check below
  var aIndex = tokens[idx].attrIndex("target");
  if (aIndex < 0) {
    tokens[idx].attrPush(["target", "_blank"]); // add new attribute
  } else {
    tokens[idx].attrs[aIndex][1] = "_blank"; // replace value of existing attr
  }
  // pass token to default renderer.
  return defaultRender(tokens, idx, options, env, self);
};
Markdown の解析結果を見てみる
Renderer の雰囲気がわかったところで、次にmd.renderer.rules[name]のnameにどんなものがあるのかを見ていきます。
Markdown-it はブラウザ上にデモがあり、Markdown の解析結果を見ることが出来ます。
右カラムのhtml、source、debugをdebugにすると解析結果のトークン一覧が見れます。
例として以下のような Markdown を変換してみます。
# これは h1
解析結果は以下のようになります。
[
  {
    "type": "heading_open",
    "tag": "h1"
    // 省略
  },
  {
    "type": "inline",
    "tag": "",
    // 省略
    "children": [
      {
        "type": "text",
        "tag": "",
        // 省略
        "children": null,
        "content": "これはh1"
        // 省略
      }
    ],
    "content": "これはh1"
    // 省略
  },
  {
    "type": "heading_close",
    "tag": "h1"
    // 省略
  }
]
inlineの子要素にtextがあります。このあたりの値をいじれば、特定のテキストを置換するなども出来そうです。
Ruler を操作する
このあたりは理解があいまいです。
Markdown の解析ルールは Ruler クラスのインスタンスが管理しているようです。インスタンスはmd.core.ruler、md.inline.ruler、md.block.rulerなどがあります。
ルールを追加するにはruler.pushなどの関数を呼び出します。
僕が作成したプラグインでは、各行で特定の条件にマッチしたトークンのtypeをtext_with_hatena_linkに上書きし、md.renderer.rules.text_with_hatena_linkを実装してみました。
参考になるリポジトリ
既存の実装を読むのも理解の助けになります。参考になりそうなリポジトリを 2 つ紹介します。
markdown-it-emoji
md.core.rulerにルールを追加し、emojiの Renderer を追加しているようです。
zenn-cli
Zenn の Markdown は Markdown-it で変換されており、独自の変換ルールはプラグインとして実装されています。
おわりに
Markdown-it のプラグインの作り方を紹介しました。
僕自身もあまり理解出来ていないので入門的な内容に留まってしまいましたが、これから作る方への参考になれば嬉しいです。



Discussion