Emacs + markdownで快適なリアルタイムプレビューを!(コードハイライト編)


日々、Emacsで作業しているみなさまは当然、markdownもEmacsでガリガリ書いておられることと思います笑。いろんな人がいろんなやり方でmarkdown執筆環境を作っていると思いますが、コードのハイライトをリアルタイムプレビュー環境でも実現したいなぁ、というのが今回のモチベーションです。

TL; DR

markdown-preview-modeの中に入ってるpreview.htmlhighlight.jsを当てましょう。これだけっ!

環境

emacs使ってて、markdown-preview-modeが使えることが前提です。この記事が参考になると思います。ありがとうございます。

コードハイライト

このままだと、リアルタイムプレビューはできるが、コードハイライトがされない、、、という状態になります。こんな感じです。
これは辛い。なんとかしてコードハイライトさせたいですよね。ネットで調べると色々出てくるのですが、リアルタイムプレビューな環境でコードハイライト、というのがなかなかヒットしません。所詮HTMLなので、なんとかしてハイライトさせるとしたら、HTMLにhighlight.js当てるしかないな、、と思いました。

markdown-preview-modeの仕組み

markdown-preview-modeを見るとわかるのですが、markdownからHTMLを生成する際にはpreview.htmlをテンプレートにしているようです。ローカル環境だとどこに格納されているかは人それぞれですが、私は今はCaskを使っているので、私の環境では以下に格納されていました。

なので、このpreview.htmlをいじれば、コードハイライトもされるだろう、、という発想です。

preview.htmlを編集しよう

HTMLを見てみるとWebSocket()でデータをとって、<p>Markdown preview</p>をリアルタイム更新していることがわかります。

preview.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, minimal-ui">
    <title>Markdown preview</title>
    ${MD_STYLE}
    <script src="http://code.jquery.com/jquery-1.12.4.min.js"></script>
    ${MD_JS}
    <script>
     (function($, undefined) {
         var socket = new WebSocket("ws://${WS_HOST}:${WS_PORT}");
         socket.onopen = function() {
             console.log("Connection established.");
         socket.send("MDPM-Register-UUID: ${MD_UUID}");
         };
         socket.onclose = function(event) {
             if (event.wasClean) {
                 console.log('Connection closed gracefully.');
             } else {
                 console.log('Connection terminated.');
             }
             console.log('Code: ' + event.code + ' reason: ' + event.reason);
         };
         socket.onmessage = function(event) {
             $("#markdown-body").html($(event.data).find("#content").html()).trigger('mdContentChange');
             var scroll = $(document).height() * ($(event.data).find("#position-percentage").html() / 100);
             $("html, body").animate({ scrollTop: scroll }, 600);
         };
         socket.onerror = function(error) {
             console.log("Error: " + error.message);
         };
     })(jQuery);
    </script>
  </head>
  <body>
    <article id="markdown-body" class="markdown-body">
      <p>Markdown preview</p>
    </article>
  </body>
</html>

なので、このHTMLにhighlight.jsを当てて、WebSocketからデータ取得して書き込むたびにハイライトさせれば、リアルタイムにハイライトがされるはず、、!というわけで、早速ですが修正後はこちら。

preview.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, minimal-ui">
    <title>Markdown preview</title>
    ${MD_STYLE}
    <script src="http://code.jquery.com/jquery-1.12.4.min.js"></script>
    ${MD_JS}

    <!-- ↓highlight.jsとCSSを当てましょう。CSSはいろんな種類があります。
         私はtomorrow.cssというものに落ち着きました。詳しくはhightlightのドキュメント見てください -->
    <link rel="stylesheet"
      href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.0.1/styles/tomorrow.min.css">
    <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.0.1/highlight.min.js"></script>
    <!-- ネットでよく見る<script>hljs.initHighlightingOnLoad();</script>は今回は不要です。
         WebSocketでリアルタイムにレンダリングするので、OnLoadでhighlightしても意味がないですよね -->

    <script>
     (function($, undefined) {
         var socket = new WebSocket("ws://${WS_HOST}:${WS_PORT}");
         socket.onopen = function() {
             console.log("Connection established.");
         socket.send("MDPM-Register-UUID: ${MD_UUID}");
         };
         socket.onclose = function(event) {
             if (event.wasClean) {
                 console.log('Connection closed gracefully.');
             } else {
                 console.log('Connection terminated.');
             }
             console.log('Code: ' + event.code + ' reason: ' + event.reason);
         };
         socket.onmessage = function(event) {
             $("#markdown-body").html($(event.data).find("#content").html()).trigger('mdContentChange');
             var scroll = $(document).height() * ($(event.data).find("#position-percentage").html() / 100);
             $("html, body").animate({ scrollTop: scroll }, 600);

         // ここですね。pre codeタグを全て拾って、highlight処理をかけます。
         document.querySelectorAll('pre code').forEach((block) => {
         hljs.highlightBlock(block);
         });
     };
         socket.onerror = function(error) {
             console.log("Error: " + error.message);
         };
     })(jQuery);
    </script>
  </head>
  <body>
    <article id="markdown-body" class="markdown-body">
      <p>Markdown preview</p>
    </article>
  </body>
</html>

たったこれだけです。これで改めてコードハイライトが効いているか確認してみます。
いいですね!リアルタイムプレビュー環境下でも、ちゃんとコードがハイライトされるようになりました。

まとめ

今回はmarkdown-preview-mode環境下でコードハイライトを実現するための方法の1つをご提示しました。みなさま、他にもっと良いやり方があるよ!とか、知見があれば、是非おしえてください!!