[Hugo]{{ hoge }}と{{- hoge -}}の違い

本記事ではHugoの{{ hoge }}と{{- hoge -}}の違いをご紹介します。

参考情報

違い

違いは「空白を取り除く」ことです。

  • {{ hoge }}:空白が入る
  • {{- hoge -}}:空白が入らない

Hugoが定義している「空白」は以下の通りです。

  • spaces(スペース)
  • horizontal tabs(タブ)
  • carriage returns(CR) ※改行の制御文字
  • newlines(LF) ※改行の制御文字

{{ hoge }}の場合

以下の処理を実行した場合、

1
2
3
4
5
<meta charset='utf-8'>
{{ with resources.Get "js/newrelic.js" }}
<script type="text/javascript" src="{{ .RelPermalink }}"></script>
{{ end }}
<meta name='viewport' content='width=device-width, initial-scale=1'>

ビルド後のHTMLファイルは以下のようになります。

1
2
3
4
5
<meta charset='utf-8'>

<script type="text/javascript" src="/js/newrelic.js"></script>

<meta name='viewport' content='width=device-width, initial-scale=1'>

{{ hoge }}が空白行として反映されてしまいました。
今回の例では空白行が入っていても動作影響はありませんが、意図のない空白行となっていてイマイチです。

{{- hoge -}}の場合

以下の処理を実行した場合、

1
2
3
4
5
<meta charset='utf-8'>
{{- with resources.Get "js/newrelic.js" -}}
<script type="text/javascript" src="{{ .RelPermalink }}"></script>
{{- end -}}
<meta name='viewport' content='width=device-width, initial-scale=1'>

ビルド後のHTMLファイルは以下のようになります。

1
<meta charset='utf-8'><script type="text/javascript" src="/js/newrelic.js"></script><meta name='viewport' content='width=device-width, initial-scale=1'>

{{- hoge -}}は空白行とならず、意図通りに反映されています。
意図のない空白行が削除され、スッキリしたコードとなりました。

{{ hoge -}}の場合

では、片方だけ-を指定するとどうなるのでしょうか。
先頭の-を削除して{{ hoge -}}とした場合、

1
2
3
4
5
<meta charset='utf-8'>
{{ with resources.Get "js/newrelic.js" -}}
<script type="text/javascript" src="{{ .RelPermalink }}"></script>
{{ end -}}
<meta name='viewport' content='width=device-width, initial-scale=1'>

ビルド後のHTMLファイルは以下のようになります。

1
2
3
<meta charset='utf-8'>
<script type="text/javascript" src="/js/newrelic.js"></script>
<meta name='viewport' content='width=device-width, initial-scale=1'>

{{ hoge }}のように空白行とはなりませんでしたが、改行されています。
-が1回分の空白を削除したと考えると、納得の挙動です。

補足

Hugo実装レベルでの確認

Hugoの実装として上記の仕様に関係ありそうな箇所を確認してきました。
解釈違いや認識不足があればXでアドバイスいただけますと幸いです。
※私はGo言語の経験値がありません…
 憧れのような気持ちと、理解度を上げていきたい気持ちはあります。

空白削除に関連する変数定義

2024年9月時点では以下の内容で定義されています。
違いに記載していた内容がspaceCharsに定義されていることを確認できました。

hugo/tpl/internal/go_templates/texttemplate/parse/lex.go(L103-L107)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Trimming spaces.  
// If the action begins "{{- " rather than "{{", then all space/tab/newlines  
// preceding the action are trimmed; conversely if it ends " -}}" the  
// leading spaces are trimmed. This is done entirely in the lexer; the  
// parser never sees it happen. We require an ASCII space (' ', \t, \r, \n)  
// to be present to avoid ambiguity with things like "{{-3}}". It reads  
// better with the space present anyway. For simplicity, only ASCII  
// does the job.  
const (  
  spaceChars    = " \t\r\n"  // These are the space characters defined by Go itself.  
  trimMarker    = '-'        // Attached to left/right delimiter, trims trailing spaces from preceding/following text.  
  trimMarkerLen = Pos(1 + 1) // marker plus space before or after  
)

空白削除処理

空白削除は文字列の長さを軸に実装しているようです。(自信なし)
trimMarkerが区切り文字({{,}})と隣接している場合、対象変数のlengthを調整することで空白除外しているように見えます。(自信なし)

hugo/tpl/internal/go_templates/texttemplate/parse/lex.go(L270-L298)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func lexText(l *lexer) stateFn {
 if x := strings.Index(l.input[l.pos:], l.leftDelim); x >= 0 {
  if x > 0 {
   l.pos += Pos(x)
   // Do we trim any trailing space?
   trimLength := Pos(0)
   delimEnd := l.pos + Pos(len(l.leftDelim))
   if hasLeftTrimMarker(l.input[delimEnd:]) {
    trimLength = rightTrimLength(l.input[l.start:l.pos])
   }
   l.pos -= trimLength
   l.line += strings.Count(l.input[l.start:l.pos], "\n")
   i := l.thisItem(itemText)
   l.pos += trimLength
   l.ignore()
   if len(i.val) > 0 {
    return l.emitItem(i)
   }
  }
  return lexLeftDelim
 }
 l.pos = Pos(len(l.input))
 // Correctly reached EOF.
 if l.pos > l.start {
  l.line += strings.Count(l.input[l.start:l.pos], "\n")
  return l.emit(itemText)
 }
 return l.emit(itemEOF)
}

関数や変数の呼び出しが多く、上記のコードだけでは読み解けません。
私は以下の変数・関数の定義を追いかけることで先ほど記載した解釈をしました。

  • leftDelim
  • hasLeftTrimMarker
  • rightTrimLength

まとめ

本記事では{{ hoge }}と{{- hoge -}}の違いをご紹介しました。
違いを知ることで、Hugoをカスタムする時の悩みが減りそうな気がします。