Vuetify+TypeScriptでちょっとリッチなmarkdownを

## 前書き
この記事は JSL (日本システム技研) Advent Calendar 2020 - Qiita 22日目の記事です。
Vueでマークダウン書きたいよ、という要望に応えるために色々やってみた備忘録になります。
なんか、あまりこの組み合わせのやってみた記事を見かけないけどなんで・・・？？？ :thinking: 

## 環境
- `MacOS: Mojave v10.14.5`
- `Node: v10.19.0`
- `TypeScript: v3.9.7`
- `Vue.js: v2.6.12`
- `Vuetify: v2.3.21`

## 楽をするために我々はnpmの奥地へと向かった...

### vuetify-markdown-editor
なんかそれっぽいのが見つかりました。 [vuetify-markdown-editor](https://www.npmjs.com/package/vuetify-markdown-editor) ですね。覗いてみましょうか・・・

おおぉ・・・、もう今回の要件ほとんど満たしているじゃありませんか。  
Vuetifyコンポーネントをベースにしているし、リッチですし、TypeScriptにも対応しているしで至れりつくせりです。  
もうこれでいいじゃん...と思いましたが、今後でてくるであろう要望をどこまで吸収できるのかが不安でした（使用例があんまり見つからない）

とはいえ、サクッと用意したいならこれを使わない理由はないのではないでしょうか？

`vuetify-markdown-editor` 以外にVuetifyをベースにしたものは見つかりませんでした。
今後の自由度考えて独自に実装しちゃおう、ということで本編です。~~車輪の再開発?~~

## 本編
### markdown-itを導入してみる
色々調べてみて、 [markdown-it](https://www.npmjs.com/package/markdown-it) が一番一般的っぽかったのでこれにしました。マークダウン形式で書かれたテキストをパースしてhtmlにしてくれるパッケージです。

導入に当たっては、[VuejsでMarkdownを使うときの最強な組み合わせ](https://watchcontents.com/best-combination-vuejs-markdown/) を参考にさせていただきました。 `markdown-it` にはプラグインがたくさんあって、結構カスタムできるみたいです。

で、以下のプラグインを導入することにしました。（タイトルは「リッチな」とうたっていますし豪華に行きましょう）

- `highlight.js` : コードのハイライトしてくれるやつ（重要）
- `markdown-it-container` : ↓こういうのを出せるやつ
![SS 2020-12-22 23.30.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252036/3d324677-8505-eb88-7022-2ae550a37316.png)

- `markdown-it-emoji` : 名前の通り :rolling_eyes: 出すためのやつ
- `markdown-it-imsize` : 画像サイズを調整できるようにするやつ
- `markdown-it-ins` : 文字に下線ひけるやつ
- `markdown-it-sanitizer` : XSS対策でサニタイズできるやつ
- `markdown-it-sub` : ↓のような表示をできるようにするやつ
![SS 2020-12-22 23.38.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252036/73b65cb6-a639-a6ee-8c96-f7da21d8666e.png)

- `markdown-it-task-checkbox` : チェックボックス出せるようにするやつ

基本は参考記事の通りで問題ありませんが、ここでTypeScript特有の問題が発生します。
そう、型定義です。

`@types/hoge` パッケージが配布されていればいいのですが、以下のパッケージにはありませんでした。
`declare module hoge` でanyにしてもいいのですが、一応ちゃんとやってみることにしました。~~（えらいぞ自分）~~

まぁやることは簡単で、ちゃんと `.d.ts` ファイルに型定義を書いてあげればよいです。（たぶん）

```markdown-it-plugins.d.ts
declare module "markdown-it-task-checkbox" {
  import { PluginSimple, PluginWithOptions } from "markdown-it";
  declare const checkbox: PluginSimple | PluginWithOptions;
  export = checkbox;
}
```

この要領で他のやつの分も追記していきます。

![SS 2020-12-22 23.49.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252036/d598a478-cb5b-50a3-8e9c-59b1ff70102e.png)

エディタもちゃんと分かってくれました（型を）

### とりあえず試してみる

参考資料を元に実装してみました。動かしてみましょう。

![SS 2020-12-22 23.58.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252036/8aa4ed20-7bf5-44aa-ccdb-af7bf23ee82a.png)

コードブロックすらまともに出ない・・・だと？？？？ :thinking: 
悪さをしているのはVuetifyのCSS、　`<code>` タグになんかあたってました。
ならばこちらもCSS書いて上書きしてやるまでです。

```Markdown.vue
// 省略

<style lang="scss" scoped>
// 参考記事のCSSインポートはここでやることに
@import "https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.11.1/katex.min.css";
@import "../../node_modules/highlight.js/styles/monokai.css";

.v-application .md ::v-deep {
  code {
    font-weight: initial;
    background-color: #2f2f2f;
    color: rgba(255, 255, 255, 0.9);
    font-size: 95%;
    display: inline-block;
    padding: 1px 0.5em 1px 0.5em;
    margin: 2px;
    margin-bottom: 14px;
  }

  pre code {
    display: block;
    padding: 0.5em 0.8em 0.5em 0.8em;
  }

  code:before,
  code:after {
    content: initial;
  }

  p {
    margin-bottom: 8px;
  }

  table {
    margin: 16px 0;
    color: #303030;
    width: auto;
    display: block;
    overflow-x: auto;
    border-collapse: collapse;

    th,
    td {
      padding: 10px;
      border: 1px solid #dbdbdb;
    }

    th {
      background-color: #f0f0f0;
      border-bottom: solid 2px #bfbfbf;
    }
  }
}
```

※ `::v-deep` は子コンポーネントにstyleを指定するためのセレクタです

ちょっと `scss` 書くの楽しくて他もいじっちゃいましたが許してください :bow:  
これでどうでしょうか。

![SS 2020-12-23 0.06.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252036/a18c536e-88a2-ec52-b846-34a0c891ea8f.png)

やりました。~~調教完了です~~

### markdown-it-containerをVuetify標準にしよう

何もしないとこんなです

![SS 2020-12-23 0.17.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252036/1db6448b-d679-df93-e850-85d3f772df95.png)

あれ、リッチとは？？？？ :thinking: 

その時思いました、Vuetifyのクラスをつけてあげれば勝手にVuetifyのデザインになるのでは、と。

早速やってみます。プラグインのオプションで変換処理を上書きして実現しようという魂胆です。Vuetifyの `v-alart` コンポーネントのクラスを拝借して・・・

```markdownRender.ts

// 省略

type AlertType = "success" | "info" | "warning" | "error";

const createContainerOptions = (a: AlertType) => {
  const pattern = new RegExp(`^${a}$`);
  return [
    a,
    {
      render: (tokens: Token[], idx: number) => {
        const m = tokens[idx].info.trim().match(pattern);
        if (m && tokens[idx].nesting === 1) {
          return `<div class="v-alert v-alert--dense v-alert--text ${a}--text">\n`;
        }
        return `</div>\n`;
      }
    }
  ];
};

const md = markdownIt({
  highlight: function(code, lang) {
    return hljs.highlightAuto(code, [lang]).value;
  },
  html: true,
  linkify: true,
  breaks: true,
  typographer: true
})
  .use(container, ...createContainerOptions("success"))
  .use(container, ...createContainerOptions("info"))
  .use(container, ...createContainerOptions("warning"))
  .use(container, ...createContainerOptions("error"))

// 省略

```

こんなんでどうよ・・・

![SS 2020-12-23 0.26.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252036/9d9a5a27-b227-d576-e182-c9cc8df06b60.png)

完☆璧

### 完成品
![SS 2020-12-23 0.33.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252036/c591c0fe-37d9-1109-619c-34a85ee07837.png)

こんな感じで、ちょっとリッチな表示ができているのではないでしょうか。
styleとプラグインで自由に拡張できるのは楽しいですね。
Vuetifyくんに邪魔されることもありますが、仲良くできればいくらでも活かすことができそうです！~~夢が膨らみます~~

## あとがき
エディタのツールバーとかはハリボテです・・・ごめんなさい・・・