通常は.TableOfContents
で呼び出すことができます。
しかし、テーマによっては何らかの事情で.TableOfContents
が使えない可能性があるので、
今回は Hugo のテンプレート文だけでがんばって実装してみたいと思います。
.TableOfContents
の構造
HTML を見てみると、.TableOfContents
が呼び出された部分は次のようになっています。
1<nav id="TableOfContents">2<ul>3<li>4<a href="#見出し1">見出し1</a>5<ul>6<li>7<a href="#見出し2">見出し2</a>8</li>9<li>10<a href="#見出し3">見出し3</a>11</li>12</ul>13</li>14<li>15<a href="#見出し4">見出し4</a>16</li>17</ul>18</nav>
この場合、記事のマークダウンファイルは、
1## 見出し123...45### 見出し267...89### 見出し31011...1213## 見出し41415...
となり、
HTML では
1<h2 id="#見出し1">見出し1</h2>2<p>...</p>3...4<h3 id="#見出し2">見出し2</h3>5<p>...</p>6...7<h3 id="#見出し3">見出し3</h3>8<p>...</p>9...10<h2 id="#見出し4">見出し4</h2>11<p>...</p>12...
となります。
ところで、記事の HTML は.Content
で参照できるので、
記事の中から h タグを探して、それらをもとに目次を作ることができます。
テンプレートのコーディング
目次を作成するテンプレートファイルを作りましょう。
目次の項目は h2 から始まることにします。
h タグを検索する
記事内容は.Content
で参照できますが、テーマ独自で何か改変が加えられていることもあります。
最終的な出力内容は、例えばlayouts/partials/post.html
などを確認してください。
$Content
のようになっていれば、直前に{{ $Content := .Scratch.Get "Content" }}
などと書かれていると思います。
.Scratch
って?
Hugo では変数は{{ $var := value }}
のように定義できますが、$
がつく形の変数はローカルにしか参照できません。
別のファイルに持ち込んだり、range
やwith
などの関数の外部から参照することができないので、
このような場合には.Scratch
を使います。
{{ .Scratch.Set "name" $inner_var }}
とすると、外部からも{{ $outer_var := .Scratch.Get "name" }}
とすることで同じ値を呼び出すことができるようになります。
ここでは、ある程度の処理が加えられた後の.Content
のデータが.Scratch.Get "Content"
で呼び出すことができ、それが最終的に出力されているものとして進めていきます。
layouts/partials/custom/toc.html1{{- /* Content を呼び出す */ -}}2{{- $Content := .Scratch.Get "Content" -}}
次にfindRE
を使って h タグを探します。
toc.html1{{- /* Content を呼び出す */ -}}2{{- $Content := .Scratch.Get "Content" -}}3{{- /* 検索に使用する正規表現パターン */ -}}4{{- $pattern := `<h[2-9] id="(.+?)".*?>((?:.|\n)*?)</h[2-9]>` -}}5{{- /* 正規表現により h タグを検索 */ -}}6{{- $headers := findRE $pattern $Content -}}
正規表現の部分について簡単に説明しておくと、
[2-9]
は「2-9のうちのどれか一文字」を表し、*?
は「前の文字の任意の0回以上の繰り返しで最短のもの」を表しています。
.
は「改行以外の任意の一文字」、\n
は「改行」を表します。
つまりここでは、id 属性以外の
の有無を問わずレベル2から9の h タグがマッチします。()
で囲った部分はグループ化され、後から$1
、$2
、…という形で参照することができますが、
(?:)
で囲った部分は無視されます。
h タグの要素を取り出す
$headers
にはパターンにマッチした要素がリストとして渡されています。
それぞれの要素を取り出していきますが、ここでrange
を使います。
toc.html1{{- /* Content を呼び出す */ -}}2{{- $Content := .Scratch.Get "Content" -}}3{{- /* 検索に使用する正規表現パターン */ -}}4{{- $pattern := `<h[2-9] id="(.+?)".*?>((?:.|\n)*?)</h[2-9]>` -}}5{{- /* 正規表現により h タグを検索 */ -}}6{{- $headers := findRE $pattern $Content -}}7{{- range $headers -}}8{{- /* ヘッダーへのリンクアドレス */ -}}9{{- $id := . | replaceRE $pattern `$1` -}}10{{- /* ヘッダー名 */ -}}11{{- $header := . | replaceRE $pattern `$2` | safeHTML -}}12{{- end -}}
9行目の.
というのは$headers
の要素のうちのひとつを表しています(ここでは扱いませんがwith
関数でも同じような省略が起こります)。
9行目は、「$headers
内の各要素について、$pattern
にマッチする部分をパターンの第一グループに置き換える」という意味になります。
第一グループというのは$pattern
で id 属性の後に書いた(.+?)
のことです。
つまり、この一連の処理によって、例えば
<h2 id="ミダシ">見出し</h2>
→ $id
:ミダシ
, $header
:見出し
となって、$id
と$header
に格納されるわけですね。
safeHTML
というのは HTML 文がエスケープされないようにするということです。
ところで、このままでは h2 タグと h3 タグは区別できていないので、
階層構造のある目次にはなりません。
階層を区別する
h タグの数字の部分を取り出しましょう。
正規表現の部分に修正を加えます。
そして新たに$depth
に階層の深さの情報を入れます。
toc.html1{{- /* Content を呼び出す */ -}}2{{- $Content := .Scratch.Get "Content" -}}3{{- /* 検索に使用する正規表現パターン */ -}}4{{- $pattern := `<h([2-9]) id="(.+?)".*?>((?:.|\n)*?)</h[2-9]>` -}}5{{- /* 正規表現により h タグを検索 */ -}}6{{- $headers := findRE $pattern $Content -}}7{{- range $headers -}}8{{- /* ヘッダーの深さ */ -}}9{{- $depth := . | replaceRE $pattern `$1` -}}10{{- /* ヘッダーへのリンクアドレス */ -}}11{{- $id := . | replaceRE $pattern `$2` -}}12{{- /* ヘッダー名 */ -}}13{{- $header := . | replaceRE $pattern `$3` | safeHTML -}}14{{- end -}}
かっこを後ろにつけて...</h([2-9])>
としても大丈夫です。
さて、これで.TableOfContents
をつくる準備は整いました。
あとはこれをもとに HTML を組んでいけばOKです。
HTML のコーディング
さきほど示した.TableOfContents
の構造をまねて、 HTML を書いていきます。
ここはもうパズルです笑
toc.html1...2{{- /* ループで直前に調べたものの階層の深さ */ -}}3{{- $scratch := newScratch -}}4{{- $scratch.Set "prev_depth" 0 -}}5<nav id="TableOfContents">6<ul>7{{- range $headers -}}8...9{{- $prev_depth := $scratch.Get "prev_depth" -}}10{{- /* h2 のとき */ -}}11{{- if eq $depth `2` -}}12{{- /* 直前の h タグが h2 のとき */ -}}13{{- if eq $prev_depth `2` -}}14</li>15{{- /* 直前の h タグが h3 のとき */ -}}16{{- else if eq $prev_depth `3` -}}17</ul></li>18{{- end -}}19<li><a href="#{{ $id }}">{{ $header }}</a>20{{- /* h3 のとき */ -}}21{{- else if eq $depth `3` -}}22{{- /* 直前の h タグが h2 のとき */ -}}23{{- if eq $prev_depth `2` -}}24<ul>25{{- end -}}26<li><a href="#{{ $id }}">{{ $header }}</a></li>27{{- end -}}28{{- $scratch.Set "prev_depth" $depth -}}29{{- end -}}30{{- $prev_depth := $scratch.Get "prev_depth" -}}31{{- /* 直前の h タグが h2 のとき */ -}}32{{- if eq $prev_depth `2` -}}33</li>34{{- /* 直前の h タグが h3 のとき */ -}}35{{- else if eq $prev_depth `3` -}}36</ul></li>37{{- end -}}38</ul>39</nav>
3行目のnewScratch
というのはローカルなスクラッチだそうです。
.Scratch
を使うと範囲が広すぎて他のものと名前衝突を起こし上書きされてしまう危険性があるので、転ばぬ先の杖です。
eq
は等号で、eq a b
でa == b
の意味です。
注意しなくてはならないのは、取得した数字は文字列なので、文字列として扱わなければif
にひっかからないということです。
このコードはループで直前に調べたものの階層の深さとループ中のものの階層の深さを利用して、
どこで何のタグをどれくらい開いたり閉じたりするかを指定しています。
h2 と h3 しか使わないよという人なら、これで十分です(といっても全部あわせると五十文あります…)。
もっといえば、さきほどの正規表現の[2-9]
も[23]
に書き換えてしまってOKです。
目次なら二階層もあれば大丈夫だと思うんですが、もし h4 も使いたいなら、脳内シミュレーションしつつ同じようにコーディングしていけばいいと思います。
コード全文(h2 h3 しか使わない場合)
toc.html1{{- /* Content を呼び出す */ -}}2{{- $Content := .Scratch.Get "Content" -}}3{{- /* 検索に使用する正規表現パターン */ -}}4{{- $pattern := `<h([23]) id="(.+?)".*?>((?:.|\n)*?)</h[23]>` -}}5{{- /* 正規表現により h タグを検索 */ -}}6{{- $headers := findRE $pattern $Content -}}7{{- /* ループで直前に調べたものの階層の深さ */ -}}8{{- $scratch := newScratch -}}9{{- $scratch.Set "prev_depth" 0 -}}10<nav id="TableOfContents">11<ul>12{{- range $headers -}}13{{- /* ヘッダーの深さ */ -}}14{{- $depth := . | replaceRE $pattern `$1` -}}15{{- /* ヘッダーへのリンクアドレス */ -}}16{{- $id := . | replaceRE $pattern `$2` -}}17{{- /* ヘッダー名 */ -}}18{{- $header := . | replaceRE $pattern `$3` | safeHTML -}}19{{- $prev_depth := $scratch.Get "prev_depth" -}}20{{- /* h2 のとき */ -}}21{{- if eq $depth `2` -}}22{{- /* 直前の h タグが h2 のとき */ -}}23{{- if eq $prev_depth `2` -}}24</li>25{{- /* 直前の h タグが h3 のとき */ -}}26{{- else if eq $prev_depth `3` -}}27</ul></li>28{{- end -}}29<li><a href="#{{ $id }}">{{ $header }}</a>30{{- /* h3 のとき */ -}}31{{- else if eq $depth `3` -}}32{{- /* 直前の h タグが h2 のとき */ -}}33{{- if eq $prev_depth `2` -}}34<ul>35{{- end -}}36<li><a href="#{{ $id }}">{{ $header }}</a></li>37{{- end -}}38{{- $scratch.Set "prev_depth" $depth -}}39{{- end -}}40{{- $prev_depth := $scratch.Get "prev_depth" -}}41{{- /* 直前の h タグが h2 のとき */ -}}42{{- if eq $prev_depth `2` -}}43</li>44{{- /* 直前の h タグが h3 のとき */ -}}45{{- else if eq $prev_depth `3` -}}46</ul></li>47{{- end -}}48</ul>49</nav>
これだけ(これ以上)のことをたった一行.TableOfContents
でやってくれるのはありがたいですね笑
読んでくださりありがとうございました!
それでは~