RailsでMarkdownをリアルタイムでプレビューする(marked.js + highlight.js)

Rails初心者です。至らぬ点・誤り等ありましたら、やんわりとご指摘いただきたく存じます。

以前、Markdown記法に対応したビューを作る、というミッションで`redcarpet`と`rouge`というgemを使った[こちら](https://qiita.com/turtlekazu/items/a22935423b0c15e10a18)の記事を書かせていただきました。ですが、どうやらこのやり方だとリアルタイムでのプレビューができないぞ、ということが判明し改めて方法を調査した形になります。(`redcarpet`はrubyファイルのため、jsとのデータ授受に難儀が...)
自分はVue.jsやReactには不慣れなゆえ、純JavaScriptを使って書きたいという願望がありました。苦戦した部分を記事にさせていただきます。

# 結論
Markdown形式のリアルタイムプレビューがしたい場合、
Markdown表示には「`marked.js`」を
シンタックスハイライトには「`highlight.js`」を使う。
両方とも、CDN（コンテンツデリバリーネットワーク）を使う方法とライブラリをダウンロードする方法の2種類がある(今回はCDNを使用)。
動的に中身が変わる場合には、jsファイルで`hljs.highlightBlock`を呼び出してシンタックスハイライトを更新してやる必要があるのがポイント。
`rouge`と`highlight.js`は役割が被るので、`rouge`は無効化。

# 流れ
1. marked.jsをCDNで追加する
2. highlight.jsをCDNで追加する
3. ビューファイルを記述
4. jsファイルを記述
5. 完成！

# 1. marked.jsをCDNで追加する
application.html.erbの`<head>`内に以下を追加する。(バージョンは執筆時点で最新の2.0.1を指定)

```html:app/views/layouts/application.html.erb
<head>
... 
  <script src='https://cdnjs.cloudflare.com/ajax/libs/marked/2.0.1/marked.js'></script>
...
</head>
```

# 2. highlight.jsをCDNで追加する
application.html.erbの`<head>`内に以下を追加する。(バージョンは執筆時点で最新の10.6.0を指定)

```html:app/views/layouts/application.html.erb
<head>
... 
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.6.0/styles/monokai-sublime.min.css">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.6.0/highlight.min.js"></script>
  <script>hljs.initHighlightingOnLoad();</script>
...
</head>
```

# 3. ビューファイルを記述
自分の場合はform_withのヘルパーメソッドを使ってtextareaのフォームに適用したかったので、以下のように書きました。

```HTML:app/views/exam/new.html.erb(任意)
...
<%= form_with model: @exam, local: true do |f| %>
...
  <div class="code-block">
    <div>あなたの解答</div>
    <%= f.text_area :user_answer_code, id: 'user_answer_code', placeholder: "input your code" %>
  </div>
  <div class="code-block">
    <div>あなたの解答(Markdown)</div>
    <div id="code_markdown"></div>   <%# ここにMarkdownに変換された入力内容が入る %>
  </div>
...
<% end %>
...
```

# 4. jsファイルを記述
自分の場合、codeblock.jsというファイルを作成し、その中に記述しました。

```js:app/javascript/codeblock.js
function markdownCode() {
  const codeInput = document.getElementById('user_answer_code');  // idが'user_answer_code'のもの(textarea)
  const codeMarkdown = document.getElementById('code_markdown');  // idが'code_markdown'のもの(空のdiv)
  if (codeInput == null || codeMarkdown == null) {
    return;  // 該当ページでない場合はアーリーリターン
  }

  codeInput.addEventListener('input', () => {
    const HTML = `${codeInput.value}`;
    codeMarkdown.innerHTML = marked(HTML);
    const pre_code_nodes = document.querySelectorAll("pre code");  // ここがポイント
    for(let i = 0; i < pre_code_nodes.length; ++i){                // ここがポイント
      hljs.highlightBlock(pre_code_nodes[i]);                      // ここがポイント
    }
  });
};

window.addEventListener("load", markdownCode);
```

ポイントは、上記で「ここがポイント」と書いた以下の部分

```js
const pre_code_nodes = document.querySelectorAll("pre code");
for(let i = 0; i < pre_code_nodes.length; ++i){
  hljs.highlightBlock(pre_code_nodes[i]);
}
```
で、`pre`と`code`のつく要素全部を取得して、`hljs.highlightBlock()`に入れてあげるところです。`hljs.initHighlighting()`や`hljs.highlightAuto()`、そして`hljs.highlightBlock(codeMarkdown)`ではうまくいかず、一番悩んだ部分です。[こちら](https://bkbkb.net/articles/use_highlightjs/)の記事の著者の方には感謝をしてもしきれません(荒技ってことになってますが...)。

なお、当然かもしれませんが、このjsファイルを読み込むために、application.jsに以下を追記するのをお忘れなく(名前は任意です)。

```js:app/javascript/packs/application.js
...
require("../codeblock")
...
```

# 完成！
textareaへの入力が無事に以下のGIFのように変換されれば、成功です！
![markdoen_realtime](https://gyazo.com/ac70072de59ce9937b5fec32f7ded837.gif)

# まとめ
リアルタイムでMarkdownをプレビューする機能をつけるには、`marked.js`と`highlight.js`を使うとできる。
このとき、`pre`と`code`のつく要素全部を取得して、`hljs.highlightBlock()`に入れてあげるのが、シンタックスハイライトをきちんと反映させるためのポイント。

# 参考記事
公式
https://cdnjs.com/libraries/marked
https://highlightjs.org/download/

その他記事
https://bkbkb.net/articles/use_highlightjs/
https://tr.you84815.space/hljs/api/highlight-block.html
https://madogiwa0124.hatenablog.com/entry/2019/01/03/203334
https://qiita.com/kimromi/items/6b2293be6fd84184dabf
https://qiita.com/samuraibrass/items/d40d54aa0754692d5439