HugoのMarkdown Render Hooksでコードブロックの出力をカスタマイズする

HugoのMarkdown Render Hooksでコードブロックを表示するマークダウン記法のレンダリング処理をカスタマイズして数式を表示できるようにしてみました。

はじめに

HugoではMarkdown Render Hooksという機能を利用することでマークダウンのHTMLへのレンダリング処理をカスタマイズすることができます。

以前アップした記事「HugoのMarkdown Render Hooksで画像の出力をカスタマイズする」ではこの機能を利用して画像を表示するマークダウン記法のレンダリング処理をカスタマイズしました。

今回はMarkdown Render Hooksを利用してTexの数式をコードブロックに書くことでブラウザ上に数式が表示されるようにしました。

```math { title="行ベクトルと列ベクトルの内積" }
\begin{aligned}
  \begin{pmatrix} 1 & 2 \end{pmatrix}
  \begin{pmatrix} 3 \\ 4 \end{pmatrix}
  &= 1 \times 3 + 2 \times 4 \\
  &= 11
\end{aligned}
```
行ベクトルと列ベクトルの内積
$$\begin{aligned} \begin{pmatrix} 1 & 2 \end{pmatrix} \begin{pmatrix} 3 \\ 4 \end{pmatrix} &= 1 \times 3 + 2 \times 4 \\ &= 11 \end{aligned}$$

またオプションでタイトルを設定できるようにしました。

```math { title="行ベクトルと列ベクトルの内積" }

ショートコードで作成するか迷ったのですがMermaid Diagramsを表示するサンプルをみてMarkdown Render Hooksで作成することにしました。

数式を表示する方法について

Texの数式をブラウザ上に表示するライブラリではMathJaxが有名ですが、MathJaxより高速に動作するKaTeXを使用することにしました。

KaTeXを利用するには以下のドキュメントを参照してライブラリのスタイルシートとJavaScriptをページに読み込みます。

Browser · KaTeX

KaTeXではレンダリング処理を自動で行ってくれる拡張機能が用意されています。

初期設定では以下のようにページ内の$$のシンボルで囲まれた数式や\(\)のシンボルで囲まれたインラインの数式などがその対象となっています。

数式を\\(\\KaTeX\\)で表示します。
$$
\begin{pmatrix} 1 & 2 \end{pmatrix}
\begin{pmatrix} 3 \\\\ 4 \end{pmatrix}
$$

数式を\(\KaTeX\)で表示します。 $$ \begin{pmatrix} 1 & 2 \end{pmatrix} \begin{pmatrix} 3 \\ 4 \end{pmatrix} $$

自動でレンダリング処理を行う対象を指定するシンボルは変更することもできます。

数式をマークダウンに書く場合の注意点

マークダウンにTexの数式を書くだけでブラウザ上にKaTeXで数式がレンダリングされます。

ただしマークダウンではエスケープ文字が\となっているため以下のように\\を記述する場合\\\\のように2重で記述する必要があります。

$$
\begin{pmatrix} 1 & 2 \end{pmatrix}
\begin{pmatrix} 3 \\\\ 4 \end{pmatrix}
$$

HTMLタグで囲むことで回避することは可能です。

<span>
$$
\begin{pmatrix} 1 & 2 \end{pmatrix}
\begin{pmatrix} 3 \\\\ 4 \end{pmatrix}
$$
</span>
$$ \begin{pmatrix} 1 & 2 \end{pmatrix} \begin{pmatrix} 3 \\ 4 \end{pmatrix} $$

このエスケープ文字を2重で書くことを回避したいこともあり数式の表示にコードブロックのMarkdown Render Hooksを利用しようと考えました。

コードブロックのMarkdown Render Hooksについて

まずはコードブロックのMarkdown Render Hooksについて調べたことをまとめます。公式のドキュメントは以下です。

Markdown Render Hooks | Hugo

テンプレートを作成する場所

コードブロックのMarkdown Render Hooksではマークダウンのレンダリング処理をカスタマイズするテンプレートを作成します。

  • layouts
    • _defaultすべてのページで利用されるテンプレートはこのフォルダに配置します
      • _markup
        • render-codeblock.htmlコードブロック全般で利用されるテンプレートです
        • render-codeblock-math.htmlmathを指定したコードブロックで利用されるテンプレートです
        • render-codeblock-javascript.htmljavascriptを指定したコードブロックで利用されるテンプレートです
    • postsタイプがpostsのページで利用されるテンプレートはこのフォルダに配置します
      • _markup
        • render-codeblock.htmlタイプがpostsのページのコードブロック全般で利用されるテンプレートです

特定のページのタイプやセクションで利用したいテンプレートはそのページのタイプやセクションのフォルダ以下に作成します。

またファイル名に-言語をつけたテンプレートを作成することでコードブロックの言語ごとに利用されるテンプレートを切り替えることができます。

テンプレートに渡されるパラメータの確認

コードブロックのMarkdown Render Hooksのテンプレートに渡されるパラメータを確認するため以下のテンプレートを作成しました。

layouts/_default/_markup/render-codeblock-actionscript.html
<pre>
  .Page       |{{ .Page }}
  .Options    |{{ .Options }}
  .Attributes |{{ .Attributes }}
  .Type       |{{ .Type       | safeHTML }}
  .Inner      |{{ .Inner      | safeHTML }}
  .Ordinal    |{{ .Ordinal    | safeHTML }}
  .Position   |{{ .Position   | safeHTML }}
</pre>

テンプレートを作成したらコードブロックを表示するマークダウン記法を書いてレンダリングされた内容からパラメータの値を確認してみます。

```actionscript { hl_lines=1, title=Title }
<p>Inner Text</p>
```

以下のHTMLがレンダリングされました。

<pre>
  .Page       |{Page(/posts/blog/hugo-render-codeblock/index.md) nopPage nopPage}
  .Options    |map[hl_lines:1]
  .Attributes |map[title:Title]
  .Type       |actionscript
  .Inner      |<p>Inner Text</p>
  .Ordinal    |8
  .Position   |"E:\blog\content\posts\blog\hugo-render-codeblock\index.md:163:1"
</pre>

ドキュメントにもありますがOptionsにChroma Lexerで指定することができるオプションが設定されるのはコードブロックにChroma Lexerが対応している言語が指定されている場合のみです。

テンプレート名をrender-codeblock-math.htmlで作成した場合の出力は以下です。

<pre>
  .Page       |{Page(/posts/blog/hugo-render-codeblock/index.md) nopPage nopPage}
  .Options    |map[]
  .Attributes |map[hl_lines:1 title:Title]
  .Type       |math
  .Inner      |<p>Inner Text</p>
  .Ordinal    |10
  .Position   |"E:\blog\content\posts\blog\hugo-render-codeblock\index.md:164:1"
</pre>

Chroma Lexerの対応する言語にmathという言語がありませんのでChroma Lexerで指定することができるオプションもAttributesに設定されます。

コードブロックで数式を表示するテンプレートの作成

まずコードブロックで数式を表示するMarkdown Render Hooksのテンプレートを作成することで実現したいことは以下です。

  1. エスケープ文字を2重で書くことを回避する
  2. 自動でレンダリングされる数式のシンボルの記述を一か所で行う
  3. 数式を書いたときにだけKaTeXのライブラリを読み込むようにする

コードブロックにmathを指定した場合に利用されるMarkdown Render Hooksの以下のテンプレートを作成しました。

layouts/_default/_markup/render-codeblock-math.html
{{- .Page.Store.Set "useMath" true -}}
<figure class="codeblock-math-outer">
  {{- with .Attributes.title }}
  <figcaption>{{- . -}}</figcaption>
  {{- end }}
  <div class="codeblock-math">$${{ .Inner }}$$</div>
</figure>

Innerにはコードブロックの中に書いた数式が設定されているので$${{ .Inner }}$$とすることで常に同じシンボルで記述されることになります。

またInnerはマークダウンとしてレンダリングせず、そのまま出力することでエスケープ文字を2重で書くことを回避しています。

あとは「3. 数式を書いたときにだけKaTeXのライブラリを読み込むようにする」ですが、この数式を表示するテンプレートが使用されたかどうかのフラグuseMathをページのStoreに設定します。

{{- .Page.Store.Set "useMath" true -}}

レイアウト用のテンプレートにはページのStoreuseMathtrueの場合にKaTeXのライブラリを読み込むように設定を追加しました。

インラインの数式のみページに書いた場合のためページのParamsの設定でもKaTeXのライブラリを読み込むようにしています。

layouts/_default/single.html
{{- define "main" -}}
  {{- .Content -}}
  ...
  {{- if or (.Page.Store.Get "useMath") .Page.Params.useMath -}}
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css" integrity="sha384-KiWOvVjnN8qwAZbuQyWDIbfCLFhLXNETzBQjA/92pIowpC0d2O3nppDGQVgwd2nB" crossorigin="anonymous">
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.js" integrity="sha384-0fdwu/T/EQMsQlrHCCHoH10pkPLlKA1jL5dFyUOvB3lfeT2540/2g6YgSi2BL14p" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/contrib/auto-render.min.js" integrity="sha384-+XBljXPPiv+OzfbB3cVmLHf4hdUFHlWNZN5spNQ7rmHTXpd7WvJum6fIACpNNfIR" crossorigin="anonymous"></script>
    <script>
      renderMathInElement(document.body);
    </script>
  {{- end -}}
{{- end -}}

ページのContentの呼び出しよりも後ろに設定を追加している理由はMarkdown Render HooksではページのStoreに設定した値はページのContentが呼び出された後でしか参照することができないためです。

おわりに

今回はコードブロックのMarkdown Render Hooksについて調べてTexの数式を表示するために利用してみました。

ショートコードの場合は使用されたかどうかをページのContentを呼び出しより前にHasShortcodeでチェックすることができますがMarkdown Render Hooksの場合はContentを呼び出しより後でしか使用されたかどうかチェックできない点は少し使いずらいとは思います。

また今回の方法はmathという存在しない言語を利用した少し強引なやり方ではありますが今のところ満足しています。