はじめに
個人開発にて、Action TextとMathJax(バージョン 3.2) で、簡易的ではありますが数式を表示できるエディターを作成しました。
- Rails ガイドーAction Text
- MathJax
左の画像のように数式を入力してプレビューボタンを押すと、右のように数式が表示されます。
この記事では、実装した際につまづいた点をMathJaxを中心に説明したいと思います。
準備
Action Text
テーブル作成など、サーバー側の準備は Rails ガイドーAction Text を、
エディターの見た目やJSのイベントなど、フロント側の設定は trix の GitHubをご覧ください。
私は、フロント側に関しては以下の2点をデフォルトの設定から変更しました。
-
ツールバー
エディター上部のツールバーの見た目は、https://github.com/basecamp/trix/blob/main/assets/trix/stylesheets/toolbar.scss で定義されています。これを参考に以下のようにすることで、ツールバーのいくつかのアイテムを削除しました。
app/javascript/stylesheets/action_text.scsstrix-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; }
-
ファイル添付を無効化
Action Textはデフォルトでエディターにファイルを添付することができます。今回、この機能は必要ないため、こちらを参考にして無効化しました。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のドキュメントがとてもわかりやすいので、読めば大丈夫だと思います。
私の初期設定は以下のようになりました。
app/javascript/src/MathJax.js
MathJax = {
tex: {
inlineMath: [ ['$','$'], ['\\(','\\)'] ],
processEscapes: true,
tags: 'ams',
},
startup: {
elements: [".mathjax-initialize-typeset"]
}
};
startup
セクションには、MathJaxのスタートアップ時の設定を記述します。
参考: MathJax-Performing Actions During Startup
elements
オプションは、画面読み込み時にMathJaxによってタイプセットされるべきDOM要素を指定します(デフォルトの値は document body
です)。
参考: MathJax-Startup Options
今回であれば、例えばユーザーが入力するエディターはタイプセットされたくありません。そこで、画面読み込み時にタイプセットしてほしい要素に mathjax-initialize-typeset
というクラス名を付与し、これをelements
オプションの値に指定しておきます。
タイプセットすべき要素の設定は、Optionsセクションの skipHtmlTags
などでも指定できるようです。
参考: Document Options
ビューファイル
CSSに関するものなど、重要でないものは省略しています。
(注)BootStrap5、gem slim、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
ただし、タイプセットが実行されるたび自動的に数式番号が増えていくので、MathJax.texReset([n])
で数式番号をリセットします。
参考: ドキュメント-Resetting Automatic Equation Numbering
問題点
以上が基本的な構造ですが、このままでは以下の問題が発生します。
問題点1
ディスプレー数式の直後に改行が入ってしまい、見栄えが悪い。
プレビューのHTMLは概略次のようになります。
<mjx-container display="true">
(ディスプレー数式)
<mjx-container>
<br>
"が成り立つ。"
このように、ディスプレー数式の直後に br
タグができてしまうことが原因です。
問題点2
複数のユーザーが作成した文書を同一画面に表示すると、数式番号が続き番号になってしまいます。
ざっくりとした例ですが、ビューファイルを
== user_b.document_with_equation
== user_a.document_with_equation
問題点を解決する
問題点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
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の内容を利用することにしました。
これは、数式番号をセクションごとに付与する example で、例えばセクション2の3つ目の数式には「(2.3)」のように数式番号を付与することができます。
こちらのissueにわかりやすい例があります。
私の場合、セクション番号は必要なく、セクションごとに数式番号をリセットしたいだけなので、以下のような設定を追加するだけで十分でした。
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
さらに、startup
セクションのready()
関数に、上記exampleで紹介されているコードのうち\nextSection
でセクションを増加させるマクロの部分を追加しました。
これにより、ビューファイルの
span style="display: none;"
| \(\nextSection\)
を記述したところでセクションが切り替わり、数式番号がリセットされます。
先ほどの例でいうと、ビューファイルを
== user_b.document_with_equation
span style="display: none;"
| \(\nextSection\)
== user_a.document_with_equation