Asanaの本文をコードハイライトするChrome拡張を作りました

## はじめに

この記事はHamee Advent Calendar 2021の4日目の記事です。
Hamee開発部は長年タスク管理にRedmineを使ってきましたが、つい最近Asanaへ切り替えを行いました。
Asana本文でコードハイライトができないようで、Redmineと同じような使い方をしようとすると不都合があったためコードハイライトできるように拡張を作りました。

## デモ

拡張アイコンをクリックするとasanaの本文がコードハイライトされます。
もう1度押すと元に戻ります。
![asana_code_highlight.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67471/42f41d48-a57f-650c-404a-b7cfe2c9e2a6.gif)



## コード

拡張を公開するにはデベロッパー登録しないといけないため社内限定公開にしてあります。
ソースコードはgithubにアップしてありますので手元に持ってきて自身で拡張登録すればどなたでもお使いいただけます。
https://github.com/yamamoto-hiroya/asana_code_highlight

## 解説

軽くソースコードを解説します。
ブラウザ上部の拡張アイコンをクリックすると
background.jsが発火します。

```js:background.js
// backgroundで動くjs
// ブラウザ上部のアイコンをクリックすることで発火し、content.jsの方にメッセージを飛ばす（トリガー）
chrome.browserAction.onClicked.addListener(function(tab) {
  /**
   * 第一引数: タブのID
   * 第二引数: ポストするキーとバリュー
   * 第三引数: コールバック関数（レスポンスが戻ってきた時に実行される）
   */
  chrome.tabs.sendMessage(tab.id, { trigger: "on" },
    function(msg) {
      console.log("background側のconsole:", msg);
    });
});
```

発火した結果content.jsに処理が移る。

```js:asana_code_highlight.js
/**
 * 基本方針
 * 画面が動的変わるので適用が面倒くさい
 * 拡張アイコンをクリックしたタイミングで適用/解除をすることにする
 * 元の要素を直接置き換えてしまうとフロントの保存処理が効かなくなってしまうため
 * タスク本文欄はhide,showで切り替え、highlight欄はafter,removeで切り替えて
 * さも切り替わっているように見せることにした
 *
 * ハイライトは以下のライブラリを使用
 * @see https://github.com/highlightjs/highlight.js
 */
$(function(){
  /**
   * @param object message backgroundからポストされた値
   * オブジェクトになっていてmessage.trigger = 'on'のような形になっている
   * @param sender
   * @sendResponse
   * これらは2つとも宣言してないとsendResponseが最後に返せなかったので設定した
   *
   * backgroundからメッセージが送られてきた時（ブラウザボタンをクリックされた時）に発火する
   */
  chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
    // 意図したbackground.jsからの送信でない場合は弾く
    if(message.trigger !== 'on'){
      return false;
    }

    /** ----- main ----- **/
    // APPEND: タスク本文以外（コメント欄やメッセージ欄など）にも適用できると嬉しいかも
    // タスク説明文が表示状態で存在していればハイライトする
    var task_description = $(".TaskDescription");
    if(task_description.is(':visible')){
      console.log('タスク本文要素を発見したのでハイライト欄に切り替えます。');

      var task_description_body = task_description.find(".ProsemirrorEditor").html();
      var highlight_block = "<div id='highlight'></div>";
      task_description.hide();
      task_description.after(highlight_block);

      /**
       * ```で囲まれている部分を置換しハイライトする
       */
      if(task_description_body.match(/```(.*?)```/)){
        task_description_body = task_description_body.replace(/```php(.*?)```/g, "<pre><code class='highlight php'>$1</code></pre>")
        task_description_body = task_description_body.replace(/```ruby(.*?)```/g, "<pre><code class='highlight ruby'>$1</code></pre>")
        task_description_body = task_description_body.replace(/```sql(.*?)```/g, "<pre><code class='highlight sql'>$1</code></pre>")
        task_description_body = task_description_body.replace(/```js(.*?)```/g, "<pre><code class='highlight js'>$1</code></pre>")
        task_description_body = task_description_body.replace(/```sh(.*?)```/g, "<pre><code class='highlight sh'>$1</code></pre>")
        task_description_body = task_description_body.replace(/```html(.*?)```/g, "<pre><code class='highlight html'>$1</code></pre>")
        task_description_body = task_description_body.replace(/```css(.*?)```/g, "<pre><code class='highlight css'>$1</code></pre>")
        task_description_body = task_description_body.replace(/```go(.*?)```/g, "<pre><code class='highlight go'>$1</code></pre>")
        task_description_body = task_description_body.replace(/```py(.*?)```/g, "<pre><code class='highlight py'>$1</code></pre>")
        task_description_body = task_description_body.replace(/```(.*?)```/g, "<pre><code class='highlight'>$1</code></pre>")
      }

      // textで入れると改行されないのでhtmlごと入れる
      $('#highlight').html(task_description_body);
      // ハイライト処理
      $('.highlight').each(function(i, block) {
        hljs.highlightBlock(block);
      });

    // ハイライト欄があれば元に戻す
    } else if($("#highlight").length !== 0) {
      console.log('ハイライト欄を発見したので元の要素に切り替えます。');
      $('#highlight').remove();
      task_description.show();

    // 対象の要素がなければ何もしない
    } else {
      console.log("対象の要素がなかったため何もしません。");
    }

    sendResponse('Done');
  });
});
```

`chrome.runtime.onMessage.addListener`
でbackgroud.jsからのメッセージを取得
意図したメッセージなら処理するようにしている。
ハイライトのメイン処理は
https://github.com/highlightjs/highlight.js
こちらを利用させていただきました。
`hljs.highlightBlock(block);`
を呼ぶだけでいい感じにハイライトしてくれるからめっちゃ便利でした。
後は基本的にコメントに書いている通り、コード読めば大体理解できるかと思います。

## 苦労したところ

asanaの画面が動的にDOM要素が変わるため、ボタンを仕込むとか画面が切り替わる度に再適用するとか、そういったことは面倒くさそうでした。
なので拡張アイコンのクリックによって切り替える方針にしました。
はじめは`.text()`を取得しようとしていたんですが、asanaのタスク本文が普通のinput要素ではなくpタグとcodeタグで囲まれたhtmlだったため、`.html()`で取得するようにしました。
また、切り替え方に関してもタスク本文を`.empty()`して要素を追加、戻すときは元のhtmlを入れて戻す、みたいな方針で実装していたんですが、元のcloneしておいたhtmlを入れても自動保存のjsが効かなくなってしまいました。
ハイライト処理すると保存できなくなるのは困るので試行錯誤した結果、`hide,show`で切り替わってるように見える実装方法に変更しました。

また、最初はタスク全文をハイライトしていましたが意図しないハイライトがかかったりして逆に見づらかったので
\`\`\`のようなマークダウン記法で囲まれた部分のみを置換するように方針変更しました。
チームに共有して使ってもらったところ「拡張入れてない人からしたら\`\`\`が謎に入ってかえって見づらいかも」との意見をもらって、うーん一長一短かーと思っているところです。
今後アップデートするかも、しないかも。
何か良い案があればコメントかissueかプルリクかください。

## おわりに

これでasana上でもいい感じにコードが見れるので見間違いなどのリスクが減らせるといいなー。
タスク本文以外の部分にも適用した方がいいか悩み中。
まぁそれは今後需要があればということで。
実装時間約4時間にしてはやりたいこともできてるし我ながら上出来かな。

それではこの後もHameeのアドカレの記事をお楽しみに！