前書き
この記事は JSL (日本システム技研) Advent Calendar 2020 - Qiita 22日目の記事です。
Vueでマークダウン書きたいよ、という要望に応えるために色々やってみた備忘録になります。
なんか、あまりこの組み合わせのやってみた記事を見かけないけどなんで・・・???
環境
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 ですね。覗いてみましょうか・・・
おおぉ・・・、もう今回の要件ほとんど満たしているじゃありませんか。
Vuetifyコンポーネントをベースにしているし、リッチですし、TypeScriptにも対応しているしで至れりつくせりです。
もうこれでいいじゃん...と思いましたが、今後でてくるであろう要望をどこまで吸収できるのかが不安でした(使用例があんまり見つからない)
とはいえ、サクッと用意したいならこれを使わない理由はないのではないでしょうか?
vuetify-markdown-editor
以外にVuetifyをベースにしたものは見つかりませんでした。
今後の自由度考えて独自に実装しちゃおう、ということで本編です。車輪の再開発?
本編
markdown-itを導入してみる
色々調べてみて、 markdown-it が一番一般的っぽかったのでこれにしました。マークダウン形式で書かれたテキストをパースしてhtmlにしてくれるパッケージです。
導入に当たっては、VuejsでMarkdownを使うときの最強な組み合わせ を参考にさせていただきました。 markdown-it
にはプラグインがたくさんあって、結構カスタムできるみたいです。
で、以下のプラグインを導入することにしました。(タイトルは「リッチな」とうたっていますし豪華に行きましょう)
-
highlight.js
: コードのハイライトしてくれるやつ(重要) -
markdown-it-emoji
: 名前の通り 出すためのやつ -
markdown-it-imsize
: 画像サイズを調整できるようにするやつ -
markdown-it-ins
: 文字に下線ひけるやつ -
markdown-it-sanitizer
: XSS対策でサニタイズできるやつ -
markdown-it-task-checkbox
: チェックボックス出せるようにするやつ
基本は参考記事の通りで問題ありませんが、ここでTypeScript特有の問題が発生します。
そう、型定義です。
@types/hoge
パッケージが配布されていればいいのですが、以下のパッケージにはありませんでした。
declare module hoge
でanyにしてもいいのですが、一応ちゃんとやってみることにしました。(えらいぞ自分)
まぁやることは簡単で、ちゃんと .d.ts
ファイルに型定義を書いてあげればよいです。(たぶん)
declare module "markdown-it-task-checkbox" {
import { PluginSimple, PluginWithOptions } from "markdown-it";
declare const checkbox: PluginSimple | PluginWithOptions;
export = checkbox;
}
この要領で他のやつの分も追記していきます。
エディタもちゃんと分かってくれました(型を)
とりあえず試してみる
参考資料を元に実装してみました。動かしてみましょう。
コードブロックすらまともに出ない・・・だと????
悪さをしているのはVuetifyのCSS、 <code>
タグになんかあたってました。
ならばこちらもCSS書いて上書きしてやるまでです。
// 省略
<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
書くの楽しくて他もいじっちゃいましたが許してください
これでどうでしょうか。
やりました。調教完了です
markdown-it-containerをVuetify標準にしよう
何もしないとこんなです
あれ、リッチとは????
その時思いました、Vuetifyのクラスをつけてあげれば勝手にVuetifyのデザインになるのでは、と。
早速やってみます。プラグインのオプションで変換処理を上書きして実現しようという魂胆です。Vuetifyの v-alart
コンポーネントのクラスを拝借して・・・
// 省略
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"))
// 省略
こんなんでどうよ・・・
完☆璧
完成品
こんな感じで、ちょっとリッチな表示ができているのではないでしょうか。
styleとプラグインで自由に拡張できるのは楽しいですね。
Vuetifyくんに邪魔されることもありますが、仲良くできればいくらでも活かすことができそうです!夢が膨らみます
あとがき
エディタのツールバーとかはハリボテです・・・ごめんなさい・・・