やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

zola include, import, extendsのスコープ

 includeextendsは呼出側でsetすれば、被呼出側でその変数を参照できる。それ以外は一切、ファイルをまたいで変数を参照することができない。

成果物

情報源

 ファイルをまたいだ変数の参照について、掲載されていないと思う。たぶん。

前回

疑問

 スコープはどうなっているのか。{% set %}した変数は、外部ファイル同士で参照できるのか?

結論

機能 定義元 呼出元 是非
include set定義 set参照
include set参照 set定義
import set定義 set参照
import set参照 set定義
extends set定義 set参照
extends set参照 set定義

include

import

extends

エラー集

includeされる側で定義した変数は、includeする側で参照できなかった

Error: Failed to render section '/tmp/work/Shell.Zola.Include.Import.Extends.Scope.20210803135518/src/1/ytyaru-zola/content/_index.md'
Reason: Failed to render 'index.html'
Reason: Variable `title` not found in context while rendering 'index.html'

importする側で定義した変数は、importされる側で参照できなかった(先頭import版)

Error: Failed to render section '/tmp/work/Shell.Zola.Include.Import.Extends.Scope.20210803135518/src/2/ytyaru-zola/content/_index.md'
Reason: Failed to render 'index.html': error while rendering macro `body::body`
Reason: Variable `title` not found in context while rendering 'macro/body.html'

importする側で定義した変数は、importされる側で参照できなかった(先頭set版)

Error: Error parsing templates
Reason: 
* Failed to parse "/tmp/work/Shell.Zola.Include.Import.Extends.Scope.20210803135518/src/2/ytyaru-zola/templates/index.html"
 --> 3:1
  |
3 | {% import "macro/body.html" as body %}␊
  | ^---
  |
  = unexpected tag; expected end of input or some content

欠陥ドキュメントへのグチ

 インポートはファイルの先頭でしかできない。だからこんなエラーになったんだと思う。そういうことはドキュメントに書いてほしい。

 ドキュメントは巧妙に都合の悪いところを避けて書いている。私がインポートにどれだけ文句をつけたかわからないのに。すごいな。自画自賛スキル高い。阿部晋三のような信者。いや教祖か。

 ていうか必要条件くらい書いてくれよ。「ファイルの先頭に書かないとエラーになる」ってドキュメントに書くべきことだろ。なんなの? 嫌がらせなの? デバックやらされている気分だよ。いちいち「使ってみなきゃわからない」とか苦痛すぎる。事前にドキュメントでルールを教えてくれたらムダな失敗を避けられるのに。クソが。これだから自分に都合のいいことばかり言うクズは嫌いなんだ。なにがポジティブだ。お前らの自画自賛オナニーのせいで、こっちがネガティブになるんだよ。

importされる側で定義した変数は、importする側で参照できなかった(先頭macro版)

Error: Failed to render section '/tmp/work/Shell.Zola.Include.Import.Extends.Scope.20210803135518/src/import/2/ytyaru-zola/content/_index.md'
Reason: Failed to render 'index.html'
Reason: Variable `title` not found in context while rendering 'index.html'

importされる側で定義した変数は、importする側で参照できなかった(先頭set版)

Error: Failed to render section '/tmp/work/Shell.Zola.Include.Import.Extends.Scope.20210803135518/src/import/3/ytyaru-zola/content/_index.md'
Reason: Failed to render 'index.html'
Reason: Variable `title` not found in context while rendering 'index.html'

importされる側で定義した変数は、importされる側でさえ参照できなかった(先頭set版)

Error: Failed to render section '/tmp/work/Shell.Zola.Include.Import.Extends.Scope.20210803135518/src/import/4/ytyaru-zola/content/_index.md'
Reason: Failed to render 'index.html': error while rendering macro `body::body`
Reason: Variable `title` not found in context while rendering 'macro/body.html'

だれも参照できない変数が作れてしまうのに構文エラーにならないことへのグチ

 だったらもう構文エラーにしろや!

 つまりマクロを定義したHTML内で、{% macro 名前() %}より上に{% set %}を定義できちゃうけど、それはマクロを定義したファイルからも、それをインポートする側からも、一切参照できないってこと。

 バカなの? だれも参照できない変数を宣言することができちゃうって、バカなの?

 構文エラーにしろや。

 こういうテキトーな感じ、すごくモヤモヤする。もっとキッチリ・カッチリやってくれ。

 それとも私がなにか勘違いしている? でもだってこんなふうに書けちゃうよ?

import/4/.../templates/macro/body.html

{% set title = "importのスコープ調査" %}
{% set summary = "importされる側で定義した変数は、importされる側でさえ参照できなかった。<code>Reason: Variable `title` not found in context while rendering 'macro/body.html'</code>" %}
{% macro body() %}
<h1>{{ title }}</h1>
<p>{{ summary }}</p>
{% endmacro body %}

 そしてエラーが以下だよ? 変数titleがないって? 書いてるんですけど? こっちが勝手に「ははーん、さては変数がスコープ外なんだな?」と推測せねばならない。だったらそうと書いてくれ。ドキュメントには仕様が明記されていない。ムリだろ。エラーメッセージも結論しか言ってない。なぜそうなったか、どうすればいいかってことを教えてほしいのに。

Reason: Variable `title` not found in context while rendering 'macro/body.html'

 仕方ないから、どうすればいいかは私が書こう。{% set %}{% macro %}から{% endmacro %}の中に書くべし。というか、マクロファイルの先頭と末尾はかならず{% macro %}{% endmacro %}にすべし。

{% macro body() %}
{% set title = "importのスコープ調査" %}
{% set summary = "importされる側で定義した変数は、importされる側でさえ参照できなかった。<code>Reason: Variable `title` not found in context while rendering 'macro/body.html'</code>" %}
<h1>{{ title }}</h1>
<p>{{ summary }}</p>
{% endmacro body %}

 だからそうでないケースは構文エラーにしてほしいんだけどなぁ。

extendsされる側で定義した変数は、extendsする側で参照できなかった(setはblockの後)

Error: Failed to render section '/tmp/work/Shell.Zola.Include.Import.Extends.Scope.20210803135518/src/extends/1/ytyaru-zola/content/_index.md'
Reason: Failed to render 'index.html'
Reason: Variable `title` not found in context while rendering 'index.html'

extendsする側で定義した変数は、extendsされる側で参照できなかった(setはextendsの後でblockの前。block内はsuper())

Failed to build the site
Error: Failed to render section '/tmp/work/Shell.Zola.Include.Import.Extends.Scope.20210803135518/src/extends/2/ytyaru-zola/content/_index.md'
Reason: Failed to render 'index.html' (error happened in 'extends/base.html').
Reason: Variable `title` not found in context while rendering 'index.html'

extendsする側で定義した変数は、extendsする側でさえ参照できなかった(setはextendsの後でblockの前。block内で参照)。

Error: Failed to render section '/tmp/work/Shell.Zola.Include.Import.Extends.Scope.20210803135518/src/extends/3/ytyaru-zola/content/_index.md'
Reason: Failed to render 'index.html'
Reason: Variable `title` not found in context while rendering 'index.html'

調査するまでもないパターン

  • includeする側で定義した変数は、includeする側で参照できるはず
  • includeされる側で定義した変数は、includeされる側で参照できるはず
  • importする側で定義した変数は、importするる側で参照できるはず
  • importされる側で定義した変数は、importされる側で参照できるはず
  • extendsする側で定義した変数は、extendsする側で参照できるはず
  • extendsされる側で定義した変数は、extendsされる側で参照できるはず

 残念ながら、書き方によっては上記の期待を裏切られることがあった。importだ。{% macro 名前() %}から{% endmacro %}の中で{% set 変数 = 1 %}した変数を、おなじくその中で{{ 変数 }}と参照することはできる。ただし、{% macro 名前() %}の外側で定義した変数は参照できない。むしろどうやっても参照できない変数ができあがってしまう。

{% set_global %}

  • {% set_global v = 1 %}のようにグローバル変数にすれば、宣言順序さえ先にやっていればファイルをまたいでも参照できるはず

 と思っているのだが、本当にそうだろうか。一部期待を裏切られたせいで、確認しないと安心できなくなった。

 というか、考えてみればsetが実行順序に依存するなら、セクション間をまたいだ参照はできないのでは? なにせセクションが複数あるとき、その実行順序はおそらく順不同か、セクション名の辞書順だろう。もしそうなら困る。事前にすべてのset_global変数値を取得していてほしい。でも複数のセクション間で共有し、かつ演算しているのだとしたら、実行順序によって状態が変わってくるはず。なんかもうバグを作り込む予感しかしない。C言語でいうスパゲッティ状態。

 あ、set_globalについて書いてあった。for文の中で値を保持するためだけにset_globalがあるっぽい。なにそれ、ぜんぜんグローバルじゃなくね?

 とにかくset_globalはファイルをまたいで参照できないと考えてよいだろう。for文で保持できる以外はsetと同じらしいので、あらたに調査する必要もなさそう。

結論

  • includeextendsは呼出側でsetすれば、被呼出側でその変数を参照できる

 上記以外は一切、ファイルをまたいで変数を参照することができない。

使い分け

 じゃあ、ファイル分割したとき、どうやって変数を使えばいいの?

 別のHTML箇所で、同じデータを出力したいとき、変数をつかう。それぞれ他のファイルに分割して書きたい。このとき、共通データを変数にもたせることで、DRYに書ける。

 つまりDRYに書くためには、どうしたらいいの? 以下の2パターンある。

  • macroimportする(変数の参照はできない)
  • include, extendsする(親の変数を子が参照できる)

 上記2つのあわせ技はできない。マクロで親の変数を使うことはできないからだ。マクロはインポートする必要がある。インポートはファイルの先頭でしかできない。そのせいでインポートする前にsetで変数を定義できない。よって、マクロでは呼出元である親の変数を使うことができない。

いつ、どちらを使うべき?

 macroinclude/extendsか。一体いつ、どれを使うべきなの? なにを観点にして判断すればいいの? つぎの優先順位で使っていくとよい。

  1. macro
  2. extends
  3. include

 できるだけmacroを使うことをおすすめする。なぜなら変数をカプセル化できるから。マクロは{% set %}により他のファイルと共有することが一切できない。なのでマクロを定義したファイル内で完結する。絶対にほかのファイルで宣言された変数に影響されない。唯一、呼出元でわたされた引数のみ気にしていればいい。そのおかげで、読むべきコードの範囲が狭まる。理解しやすくなりバグが出づらい。

 つぎはextends。これはHTMLの外側が共通しているときに使う。<html><body>など、あきらかに全ページにおいて共通するであろうものだけに厳選する。これで使える部分はかなりかぎられる。

 最後にinclude。これは外側でなく内側の共通部分にもちいる。それでいて引数でなく{% set %}変数で呼出元データを参照したいときに使う。includeが最も幅広く使えるだろう。記述も簡単。ただし親の変数を参照するせいで、自分より親をすべてみないと変数の値がわからない。バグがあったとき読むべきファイル(コード)の範囲が増えてしまう。

macroincludeset

 extendsはほとんど一番最初の<html>くらいにしか使えない。なのでこれは使いどころに悩まずに済む。

 ほかはほぼmacroincludeを使ってDRYに書いてゆく。setを使ってもいい。では、どれを使えばいいのか。なにを基準に判断すればよいか。

 いずれも「共通部分をそれらで定義する」ということは同じだ。では、共通部分のうち、どんな特徴があるところを、どちらで共通化すればいいのか。答えは以下。

  • set: 値(bool,int,float,string)
  • macro, include: HTML

 setは短いデータを共通化できる。たとえば以下のように。

...
{% set title = "表題A" %}
...
<title>{{ title }}</title>
...
<h1>{{ title }}</h1>
...

 それに対してマクロやインポートは、HTMLを共通化する。

 マクロとインクルードはどう使い分けるべきか。

  • macro: set変数を使わずカプセル化したい
  • include: 親のset変数を参照したい
機能 引数 親のset変数を参照できる
macro
include

 マクロはまとまったHTML文字列のときだけ共通化できる。たとえば以下のように。決して<h1><p>を離れた場所へ出力できない。なのでこれらが塊のときだけマクロで共通化すること。

 さらにインクルードとは違い、親のset変数を参照できない。その変わりマクロ内だけで参照できる引数が使える。

{% macro heading(heading, summary) %}
<h1>{{ heading }}</h1>
<p>{{ summary }}</p>
{% endmacro %}

 もちろんマクロ内では変数が使える。ただしマクロ内でのみ。

{% macro heading() %}
{% set heading = "見出しA" %}
{% set summary = "要約A" %}
<h1>{{ heading }}</h1>
<p>{{ summary }}</p>
{% endmacro %}

 インクルードは引数がない。その代わり親から継承した変数が使える。

<h1>{{ heading }}</h1>
<p>{{ summary }}</p>

 もちろん変数もつかえる。自分のファイル内であればどこでも。

{% set heading = "見出しA" %}
{% set summary = "要約A" %}
<h1>{{ heading }}</h1>
<p>{{ summary }}</p>

スコープは狭くすべき

 バグを減らすためにスコープは狭くすべき。スコープが狭い順は以下。

  1. macro
  2. for
  3. get-env

 forもマクロとおなじくローカルスコープだ。変数は自分の{% end* %}タグ範囲内でしか使えない。だが、forに限ってはset_globalで変数を外出しできる。なのでマクロよりスコープが広い。

 また、get-env環境変数を取得できる。これを使えば全ファイルで共通する変数を定義できると思う。でも、set-envがない。つまり事前に環境変数をなんとかして設定しておかねばならない。[zola][]だけで完結できなくなってしまう。使わないほうがよさそうだ。

所感

 マクロうぜぇって思ってたけど、スコープが狭いおかげで保守性が高いところはステキ。

対象環境

$ uname -a
Linux raspberrypi 5.4.83-v7l+ #1379 SMP Mon Dec 14 13:11:54 GMT 2020 armv7l GNU/Linux