【Ruby on Rails】ActionTextとMathJax 3.2で数式を表示できる簡易エディターを実装する

# はじめに
個人開発にて、Action TextとMathJax(バージョン 3.2) で、簡易的ではありますが数式を表示できるエディターを作成しました。
- [Rails ガイドーAction Text](https://railsguides.jp/action_text_overview.html)
- MathJax
  - [GitHub](https://github.com/mathjax/MathJax)
  - [ドキュメント](https://docs.mathjax.org/en/latest/index.html)

左の画像のように数式を入力してプレビューボタンを押すと、右のように数式が表示されます。

|![スクリーンショット 2022-08-01 16.32.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2519173/2eba4e53-b590-0b26-9c20-75047f21d775.png)|![スクリーンショット 2022-08-01 1.17.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2519173/00266d0b-4f23-d848-de7a-102bc9c99d3b.png)|
|:---:|:----:|

この記事では、実装した際につまづいた点をMathJaxを中心に説明したいと思います。

# 準備
## Action Text
テーブル作成など、サーバー側の準備は　[Rails ガイドーAction Text](https://railsguides.jp/action_text_overview.html) を、
エディターの見た目やJSのイベントなど、フロント側の設定は [trix の GitHub](https://github.com/basecamp/trix)をご覧ください。
私は、フロント側に関しては以下の2点をデフォルトの設定から変更しました。
1. ツールバー
エディター上部のツールバーの見た目は、https://github.com/basecamp/trix/blob/main/assets/trix/stylesheets/toolbar.scss で定義されています。これを参考に以下のようにすることで、ツールバーのいくつかのアイテムを削除しました。
app/javascript/stylesheets/action_text.scss
    ```
    trix-toolbar{
      background-color: white;
    }
    
    .trix-button--icon-heading-1,
    .trix-button--icon-italic,
    .trix-button--icon-quote,
    .trix-button--icon-strike,
    .trix-button--icon-attach,
    .trix-button--icon-code {
      display: none;
    }
    
    trix-toolbar .trix-button-group--file-tools {
      border: none;
    }
    ```
2. ファイル添付を無効化
Action Textはデフォルトでエディターにファイルを添付することができます。今回、この機能は必要ないため、[こちら](https://github.com/basecamp/trix/issues/604#issuecomment-1114265604)を参考にして無効化しました。

    app/javascript/src/ActionText.js
    ```
    require("trix");
    require("@rails/actiontext");
    
    
    (function() {
      addEventListener("trix-initialize", function(e) {
        const file_tools = document.querySelector(".trix-button-group--file-tools");
        file_tools.remove();
      })
      // action text のファイル添付機能を無効化
      addEventListener("trix-file-accept", function(e) {
        e.preventDefault();
      })
    })();
    
    ```

## MathJaxの初期設定
基本的な設定についてはここでは説明しません。[MathJaxのドキュメント]((https://docs.mathjax.org/en/latest/index.html)
)がとてもわかりやすいので、読めば大丈夫だと思います。

私の初期設定は以下のようになりました。
app/javascript/src/MathJax.js
```
MathJax = {
  tex: {
    inlineMath: [ ['$','$'], ['\\(','\\)'] ],
    processEscapes: true,
    tags: 'ams',
  },
  startup: {
    elements: [".mathjax-initialize-typeset"]
  }
};
```
[`startup`セクション](https://docs.mathjax.org/en/latest/options/startup/startup.html#startup-options-1)には、MathJaxのスタートアップ時の設定を記述します。
参考： [MathJax-Performing Actions During Startup](https://docs.mathjax.org/en/latest/web/configuration.html#startup-action)

`elements`オプションは、画面読み込み時にMathJaxによってタイプセットされるべきDOM要素を指定します（デフォルトの値は `document body` です）。
参考: [MathJax-Startup Options](https://docs.mathjax.org/en/latest/options/startup/startup.html#the-configuration-block)

今回であれば、例えばユーザーが入力するエディターはタイプセットされたくありません。そこで、画面読み込み時にタイプセットしてほしい要素に `mathjax-initialize-typeset` というクラス名を付与し、これを`elements`オプションの値に指定しておきます。

タイプセットすべき要素の設定は、Optionsセクションの `skipHtmlTags`などでも指定できるようです。
参考: [Document Options](https://docs.mathjax.org/en/latest/options/document.html#document-options-1)

## ビューファイル
CSSに関するものなど、重要でないものは省略しています。
（注）[BootStrap5](https://github.com/twbs/bootstrap)、gem [slim](https://github.com/slim-template/slim)、[simple_form](https://github.com/heartcombo/simple_form) を利用しています。
```
/ タブ
ul.nav.nav-tabs role="tablist"
  li.nav-item
    a.nav-link.active data-bs-toggle="tab" href="#code"
      | 入力
  li.nav-item
    a.nav-link.mathjax-typeset-button data-bs-toggle="tab" href="#preview"
      | プレビュー
/ タブの内容
.tab-content
  / 入力フォーム
  .tab-pane.fade.active.show.mathjax-code role="tabpanel" id="code"
    = f.input :document_with_equation, as: :rich_text_area
  / プレビュー表示
  .tab-pane.fade.trix-content.mathjax-typeset-result role="tabpanel" id="preview"
```

`= f.input :document_with_equation, as: :rich_text_area`によって、`trix-editor`要素が生成し、この中にユーザーの入力した内容のHTMLが作られていきます。

## エディターの入力内容をタイプセットして、数式をプレビューに表示するJS
app/javascript/src/MathJax.js
```
$(function(){
  // タブのプレビューを押したら
  $(document).on("click", ".mathjax-typeset-button", function(e){
    // 数式番号をリセット
    MathJax.texReset([0]);
    // タブを押されたエディター要素を取得
    var tabContent = $(e.target).parents("ul.nav-tabs").next(".tab-content");
    // エディターの入力内容を取得
    var Code = tabContent.find("trix-editor").html();
    // 入力内容をプレビューに表示後、それをタイプセットする
    MathJax.typeset(tabContent.find(".mathjax-typeset-result").html(Code));
  });
});
```
`MathJax.typeset()`メソッドを用いて、HTMLをタイプセットします。
参考: [ドキュメント-MathJax in Dynamic Content](https://docs.mathjax.org/en/latest/advanced/typeset.html#mathjax-in-dynamic-content)

ただし、タイプセットが実行されるたび自動的に数式番号が増えていくので、`MathJax.texReset([n])`で数式番号をリセットします。
参考: [ドキュメント-Resetting Automatic Equation Numbering](https://docs.mathjax.org/en/latest/web/typeset.html#resetting-automatic-equation-numbering)

# 問題点
以上が基本的な構造ですが、このままでは以下の問題が発生します。

### 問題点1
ディスプレー数式の直後に改行が入ってしまい、見栄えが悪い。

|![スクリーンショット 2022-08-01 20.37.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2519173/a9a8b842-b315-97a5-6318-248db86e5e70.png)|![スクリーンショット 2022-08-01 20.50.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2519173/79256e28-7ca5-33e3-ea9a-073dddc95505.png)|
|:--:|:--:|

プレビューのHTMLは概略次のようになります。
```
<mjx-container display="true">
（ディスプレー数式）
<mjx-container>
<br>
"が成り立つ。"
```

このように、ディスプレー数式の直後に `br` タグができてしまうことが原因です。

### 問題点2
複数のユーザーが作成した文書を同一画面に表示すると、数式番号が続き番号になってしまいます。
ざっくりとした例ですが、ビューファイルを
```
== user_b.document_with_equation
== user_a.document_with_equation
```
とすると、以下のようになります。
![スクリーンショット 2022-08-01 20.57.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2519173/27d7a641-9c7c-9dc7-7941-20fe20f42fd0.png)

# 問題点を解決する
## 問題点1について
`<mjx-container display="true">` に続く `br` 要素を削除するJS(JQuery)の`removeBrNextToDisplayMath`メソッドを作成します。
app/javascript/src/MathJax.js
```
// MathJaxによる数式表示時、ディスプレー数式の後のbrは削除する
const removeBrNextToDisplayMath = function(){
  $('mjx-container[display="true"]').next().each(function(){
    if($(this).is("br")){
      $(this).remove();
    }
  });
}
```
そして、MathJaxのタイプセット後にこのメソッドを実行します。

プレビューボタンをクリックした際の実行は簡単で、次のようにイベントリスナーの最後にメソッドを追加するだけです。
app/javascript/src/MathJax.js
```
$(function(){
  // タブのプレビューを押したら
  $(document).on("click", ".mathjax-typeset-button", function(e){
    // 数式番号をリセット
    MathJax.texReset([0]);
    // タブを押されたエディター要素を取得
    var tabContent = $(e.target).parents("ul.nav-tabs").next(".tab-content");
    // エディターの入力内容を取得
    var Code = tabContent.find("trix-editor").html();
    // 入力内容をプレビューに表示後、それをタイプセットする
    MathJax.typeset(tabContent.find(".mathjax-typeset-result").html(Code));
    // ディスプレー数式直後のbrタグを削除
    removeBrNextToDisplayMath(); // これを追加
  });
});
```
画面読み込み時の初期タイプセット後に `removeBrNextToDisplayMath()` を実行するには、MathJaxの `startup` セクションで `MathJax.startup.promise` を利用して`ready()` 関数を上書きします。
参考： [ドキュメント-Performing Actions After Typesetting](https://docs.mathjax.org/en/latest/web/configuration.html?highlight=Queue#performing-actions-after-typesetting)
app/javascript/src/MathJax.js
```
MathJax = {
  tex: {
    inlineMath: [ ['$','$'], ['\\(','\\)'] ],
    processEscapes: true,
    tags: 'ams',
  },
  startup: {
    elements: [".mathjax-initialize-typeset"]
    // 以下を追加
    ready() {
      MathJax.startup.promise.then(() => {
        // 初期タイプセットが終了したら、ディスプレー数式直後のbrタグを削除
        removeBrNextToDisplayMath();
      });
    }
  }
};
```

## 問題点2について
デフォルトでは、MathJaxが同一画面内の数式をタイプセットすると、それらの数式番号はひと続きになります。そのため、文章ごとに数式番号をリセットしたい場合は設定を追加する必要があります。今回、[ドキュメント-Example: Section Numbering](https://docs.mathjax.org/en/latest/input/tex/extensions/tagformat.html?highlight=section#example-section-numbering)の内容を利用することにしました。
これは、数式番号をセクションごとに付与する example で、例えばセクション2の3つ目の数式には「(2.3)」のように数式番号を付与することができます。
[こちらのissue](https://github.com/mathjax/MathJax/issues/2427#issuecomment-1127026610)にわかりやすい例があります。

私の場合、セクション番号は必要なく、セクションごとに数式番号をリセットしたいだけなので、以下のような設定を追加するだけで十分でした。
app/javascript/src/MathJax.js
```
MathJax = {
  loader: {load: ['[tex]/tagformat']}, 
  tex: {
    packages: {'[+]': ['tagformat', 'sections']},
  },
  startup: {
    ready() {
      const Configuration = MathJax._.input.tex.Configuration.Configuration;
      const CommandMap = MathJax._.input.tex.SymbolMap.CommandMap;
      new CommandMap('sections', {
        nextSection: 'NextSection'
      }, {
        NextSection(parser, name) {
          parser.tags.counter = parser.tags.allCounter = 0;
        }
      });
      Configuration.create(
        'sections', {handler: {macro: ['sections']}}
      );
      MathJax.startup.defaultReady();
```
まず、数式番号のフォーマットオプションを利用するために、`loader`、`tex.packages`の設定を追加します。
参考: [ドキュメント-tagformat](https://docs.mathjax.org/en/latest/input/tex/extensions/tagformat.html?highlight=section#tagformat)

さらに、`startup`セクションの`ready()`関数に、上記exampleで紹介されているコードのうち`\nextSection`でセクションを増加させるマクロの部分を追加しました。

これにより、ビューファイルの
```
span style="display: none;"
  | \(\nextSection\)
```
を記述したところでセクションが切り替わり、数式番号がリセットされます。

先ほどの例でいうと、ビューファイルを
```
== user_b.document_with_equation

span style="display: none;"
  | \(\nextSection\)

== user_a.document_with_equation
```
とすれば、以下のようになります！！
![スクリーンショット 2022-08-01 23.56.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2519173/16307dda-1d33-ed76-3e0f-74a236aea19c.png)