前回の記事を作ろうとした際に数式の表示で死ぬほど苦労したのでその悔しさを忘れないための備忘録です。
同じように苦しんでいる方の参考になれば幸いです。
前提
数式表示ライブラリの選択肢
MathJax
最もポピュラーな数式表示ライブラリです。
LaTeXとMathMLの表記が使えます。
アメリカ数学会によって管理されているらしいのでほぼデファクトスタンダードになっているみたいです。
その割にはサーバーサイドレンダリングとかNext.jsに関連した情報があまりなく、個人的には使いにくかったので使いませんでした。
KaTeX
数式の高速描画のためのライブラリです。
こちらもLaTeX表記が使えますが、MathJaxと比べ表示できる記号類が限られているようです。
今回、こちらのほうがシンプルにサーバーサイドレンダリングができると判断したためこちらを利用しました。
この記事の内容
この記事ではKaTeXとNext.jsのSSG機能を使ってサーバーサイドで数式表示されたHTMLを静的に作ろうという内容です。
CMSにはmicroCMSを使っているので、Markdownファイルをgithubで管理するような構成の場合はちょっと変更が必要になると思われます。
また、今回の実装は正直かなりゴリ押しです。
おそらくKaTeXのオプションなどをうまく使えばもっとスマートにできるでしょうが、ひとまず目標の機能は得られたのでこの実装でオーケーとしておきます。
実装(下準備)
流れ
全体の流れとしては
- コンテンツを取得
- コンテンツを整形(今回はcheerioを使っています)
- 数式を変換(←ここがメインディッシュ)
となります。
上2つはmicroCMSやcheerioを使っており、構成にかなり依存するので構成が違う方は3つ目のみ参照してみてください。
コンテンツを取得
まずはコンテンツ(ここではmicroCMSのリッチエディタのコンテンツ)を取得します。
microcms-sdkを使えばこれは簡単にできます。
この辺の細かい内容は↓の記事に詳しいです。
コンテンツを整形
コンテンツの整形にはcheerioを使っています。
microCMSのブログで詳しく解説がされているので、そちらを見ることをおすすめします。
個人的にはパースの処理は分けたいので別ファイルを作っています。
この際のスタイリングにはTailwindCSSを使っています。
import cheerio from 'cheerio'
export const parseParagraph = (paragraph: string) => {
const $ = cheerio.load(paragraph)
$("h1").each((_, element) => {
$(element).addClass('ml-8 my-5 text-3xl font-semibold font-body')
$(element).wrap('<div class="bg-slate-100 mb-5 mt-20 flex"></div>')
$(element).parent().prepend('<div class="w-2 bg-yellow-400"></div>')
})
$("h2").each((_, element) => {
$(element).addClass('ml-4 my-2 text-xl font-semibold font-body')
$(element).wrap('<div class="mb-5 flex"></div>')
$(element).parent().prepend('<div class="w-2 bg-yellow-400"></div>')
})
$('p').each((_, element) => {
$(element).addClass('text-lg font-body leading-loose')
$(element).wrap('<div class="mt-5 mb-10"></div>')
})
$('ul').each((_, element) => {
$(element).addClass('list-disc list-inside text-lg space-y-2 ml-6 pl-4 indent-[-1em]')
$(element).children().addClass('font-thin')
})
$('blockquote').each((_, element) => {
$(element).addClass('ml-4 text-xl opacity-90 font-body')
$(element).wrap('<div class="flex"></div>')
$(element).parent().prepend('<div class="w-2 bg-slate-200"></div>')
})
return $("body").html() as string
}
KaTeXを使った数式の変換
KaTeXの導入
KaTeXの導入には
- ローカルにインストール
- CDN経由で利用
の二種類の方法があります。
ローカルに入れる場合は次のようにします。
yarn add katex
CDN経由で利用したい場合については今回はあまり調べていないのですが、公式を見るのがベストかと思われます。
ただし、Next.jsの場合は_document.jsに色々書き込むと思うのですが、HTMLの内に書き込むのとは少しだけ勝手が違うので注意が必要です。
数式の変換
KaTeXを使って数式を変換します。
サーバーサイドで処理したい場合にはkatex.renderToString
を使います。
const html = katex.renderToString("c\\pm\\sqrt{a^2 + b^2}")
引数としてlatexの数式を渡せば変換した結果を文字列として返してくれます。
この結果は次のような形でレンダリングすることができます。
<div dangerouslySetInnerHTML={{ __html: html }} />
数式を変換するだけならこれだけなのですが、ブログなどで使っていくとなるとこれだけでは少々問題です。
というのも、renderToString
に文字列すべてを投げるのは現実的ではありませんし、変換してほしくない部分まで変換してしまう危険があります。
おそらくほとんどの人はlatex等と同様に"$"や"["などで囲った部分のみ変換したいと思うはずなのですが、私の調べた範囲ではKaTeXの設定ではこれができませんでした。
なので、今回はjavascriptの文字列操作で強引に数式部分を抜き出してしまおうという作戦に出ました。
なお、クライアントサイドで数式を処理する場合には"$"で囲った部分を自動で処理するようにできるようです。
数式の抜き出し
作戦としてはまず、
htmlText.replace(正規表現, 変換処理)
の形でhtmlTextから数式(今回は"$"で囲った部分)を抜き出して、これに対してrenderToString
を適用していきます。
例えば次のようになります。
const htmlReplaced = htmlText.replaceAll(/\$\$[^\$]*\$\$/g, (substring) =>
katex.renderToString(substring.replaceAll("$", "").replaceAll(/(<br>|<\\br>| |amp;)/g, ""),
{ output: "mathml", displayMode: true, strict: "ignore" }))
htmlTextには数式を含むコンテンツが入っています。
そこからreplaceAll
を使ってパターンに該当する数式部分をKaTeXで変換します。
この際、正規表現を使っていますが、これの意味としては「ドルマーク2つで囲まれた部分を取り出し、その間にはドルマークを含まない文字列が入る」ようなものをマッチングします。
このように取り出したものはドルマーク2つで囲まれた文字列("$x=e^{10}$"のような形)になっているので、ドルマークを削除するためにもう一度replaceを行っています。
これだけでとりあえず機能的には完成なのですが、今回はmicroCMSのリッチエディタを使っているので、その中で書きやすくするためにいくつか無視したい文字列を削除する処理を行っています。
今回削除したのは
(改行タグ)、 (空白文字)、amp;(&)の3つです。
この辺は今後増えていくかもしれません。
まとめ
今回はKaTeXを使ったサーバーサイドでの数式の静的レンダリングについて解説しました。
やっぱり自作ブログは作りたてだとちょっとした機能がなくて不便しますよね。
まあ、こういうのを地道に作るのが醍醐味なんですが。
これからもちょっとずつ気長に機能を作っていこうかと思います。