読者です 読者をやめる 読者になる 読者になる

anti scroll

ブラウザと小説の新しい関係を模索する

jinja2互換のocaml製template engine 「jingoo」の紹介

 先日公開したjinja2クローンのOcaml製テンプレートエンジン「jingoo」ですが、jinja2のことを知らない人のために、ちょっとしたgetting started & cheatsheet的な捕捉です。

 より詳しいシンタックスを知りたい人は、せっかく日本語訳を書いてくれた人もいることですし、jinja2のドキュメントを読むことをあわせてお勧めします。

 またテンプレートをコンパイルする方法については、exampleディレクトリを参照してください。

テンプレートの呼び出し

open Jg_types

Jg_template.from_file "hello.html" ~models:[
  ("msg", Tstr "hello, jingoo!");
]

 テンプレートパスを指定する場合は、環境変数を指定します。全変数を指定すると面倒なんで、標準の環境変数レコード(std_env)の一部を書き換えて指定するとこんな感じです。

open Jg_types

Jg_template.from_file "hello.html"
  ~env:{
     std_env with template_dirs = ["/path/to/templates"]
   }
  ~models:[
    ("msg", Tstr "hello, jingoo!");
  ]

 これで、/path/to/templates/hello.html を発見できるようになりました。何にも指定しない場合はカレントディレクトリからhello.htmlを探します。

 続いてテンプレートのシンタックスですが、ちょっとした例外はありますが基本的にjinja2と一緒です。

 なおコード中の{# ... #} はコメントになります。

テンプレート変数の展開

テンプレートに割り当てた変数を展開します。

{# 単純な展開 #}
{{ msg }}

{# エスケープしないで展開 #}
{{ msg|safe }}

{# 展開後にフィルタ関数upperを使って大文字に #}
{{ msg|upper }}

算術計算

上から加算、減算、乗算、除算、剰余、累乗です。

{{ 1 + 1 }}
{{ 1 - 1 }}
{{ 2 * 2 }}
{{ 4 / 2 }}
{{ 4 % 3 }}
{{ 2 ** 3 }}

テンプレート内部からの変数セット

テンプレート内部で変数をセットする場合は set構文を使います。

{% set pi = 3.14 %}

ループ

ループです。イテレーションは、python流に for ~ in で指定します。ここではrange関数を使って0 ~ 10(長さ11)のリストを作ってその中身をイテレートします。

またループ中では、特殊変数「loop」が使用できます。

loop.cycleは引数の中身を循環して吐き出す関数です(サンプル内のcycleは、1,2,3,1...と出力します)。

{% for x in range(0,10) %}
x = {{x}}
1から始まるindex = {{ loop.index }}
0から始まるindex = {{ loop.index0 }}
1で終わる逆方向index = {{ loop.revindex }}
0で終わる逆方向index = {{ loop.revindex0 }}
ループの長さ = {{ loop.length }}
cycle = {{ loop.cycle(1,2,3) }}
{% endfor %}

条件分岐

{% if %}, {% elseif %}, {% else %} で分岐を指定します。

{% if msg == "hoge" %}
yes, msg is hoge
{% elseif msg == "hige" %}
msg is {{ msg }}
{% else %}
oh msg is {{ msg }}
{% endif %}

テンプレートのインクルード

{% include "table.html" %}
{% include "table.html" with context %}
{% include "table.html" without context %}

デフォルトでは呼び出し元のコンテキストがインクルード先のテンプレートに引き継がれます。

引き継ぎたくない場合は without contextを指定します。

テンプレートの継承

例えばサイトに共通したフォーマットを次のような感じで記述し、base.htmlとして保存したとします。

<html>
  <head>
  <省略>
  </head>
  <body>

  <div class="main">
  {% block main %}{% endblock %}
  </div>

  <div class="sidemenu">
  {% block sidemenu %}{% endblock %}
  </div>

  </body>
</html>

中に「block main」と「block sidemenu」というブロックが定義されていますね。

で、このベーステンプレートをextends構文で継承すると、このmainブロックと、sidemenuブロックの中身「だけ」を記述し、残りはベーステンプレートの内容をそのまま使う……といったことが出来ます。

{% extends "base.html" %}

{% block main %}
this is main block
{% endblock %}

{% block sidemenu %}
this is side menu
{% endblock %}

マクロ

マクロはこんな風に定義します。

{# 単純なマクロ #}
{% macro say_word2(x,y) %}
I say {{x}} and {{y}}.
{% endmacro %}

{# マクロ呼び出し #}
{{ say_word2("hello", "world!") }}

さらにマクロの中身からはcallerというマクロが呼べます。

例えばこんな風に。

{% macro say_word2(x,y) %}
I say {{x}} and {{y}}.
{{ caller() }}
{% endmacro %}

このcaller()は、say_word2マクロを直接呼んだときは表示されません。

しかしcallという構文を経由してsay_word2を呼ぶと、caller()の部分に、callの中身を埋め込むことができます。

例えば

{% call say_word2("hello","world") %}
this is text via call!
{% endcall %}

と呼ぶと

I say hello and world.
this is text via call!

なんて表示されます。

caller()の部分が、{% call %} ... {% endcall %} の中身(this is text via call!)になってますね。

callは引数をとることも出来ます。

例えば次のように引数付きにして、

{% call(x,y) say_word2("hello", "world") %}
this is text via call!
arg from say_word2 is {{x}}, {{y}}.
{% endcall %}

say_word2マクロの中から、呼び出し元に引数を渡すことができます。

{% macro say_word2(x,y) %}
I say {{x}} and {{y}}.
{# 引数つきでcallerを呼ぶ #}
{{ caller(10,20) }}
{% endmacro %}

結果は、

I say hello and world.
this is text via call!
arg from say_word2 is 10, 20.

と表示されます。

マクロインポート

マクロははインクルードして使うこともできますが、インポートして名前空間を割り当てる事もできます。
こんな感じ。pythonに慣れた人ならお馴染みのシンタックスだと思います。

{# そのままインポート #}
{% import "my_form_macro.html" %}

{# 名前空間付きでインポート #}
{% import "my_form_macro.html" as form_macro %}

{# 名前を指定して特定のものだけインポート #}
{% from "my_macro.html" import hoge_macro, hige_macro %}

{# 名前を指定し、別名をつけてインポート #}
{% from "my_macro.html" import hoge_macro as hoge, hige_macro as hige %}

{{ form_macro.input("name", "no name") }}
{{ hoge_macro("hello", "world!" }}
{{ hige("hello", "jingoo") }}

局所スコープ

{% with %} ~ {% endwith %} 構文を使うと、局所スコープを作る事が出来ます。

{% with hoge = "hoge" %}
 in scope, hoge is {{ hoge }}.
{% endwith %}
 
 out of scope, hoge is {{ hoge }}.

withの外にある二番目の {{hoge}} はundefinedです。

ちなみに withは引数をとらなくてもよくて、

{% with %}
{% set hoge = "hoge" %}
 in scope, hoge is {{ hoge }}.
{% endwith %}

  out of scope, hoge is {{ hoge }}.

って書いても同じです。

raw構文

テンプレートで使用するシンタックスを展開しないでそのまま表示したい時は{% raw %} ... {% endraw %}を使用します。

{% raw %}
{{ value }}

{% endraw %}

こう書くと value に設定された値でなく、そのまま {{ value }} と表示されます。

オートエスケープの有効化、無効化

全てのテンプレート変数や展開した文字列は自動でエスケープされる設定になっていますが、これを部分的に無効にしたり有効にしたりするブロックを作る事が出来ます。

{% set value = "<script>alert(1)</script>" %}

{% autoescape false %}
{# value はエスケープされない #}
{{ value }}
{% endautoescape %}

{# value はエスケープされる #}
{{ value }}

自動エスケープはデフォルトで有効になっていますが、これを無効にしたい時はテンプレートを読み込むときに与える環境変数レコードの中でautoescapeフィールドを無効にします。

open Jg_types

Jg_template.from_file "hello.html"
  ~env:{
    std_env with
    template_dirs = ["/path/to/templates"];
    <strong>autoescape = false;</strong>
  }
  ~models:[
    ("msg", Tstr "hello, jingoo!");
  ]

色んなヘルパー関数

jinja2のヘルパー関数と標準フィルタ、テスト関数はほぼ全てサポートされています。

例えば

{# 3と表示される #}
{{ "日本語"|length }}
{{ length("日本語") }}

{# [1,2,3]が返ってくる #}
{{ "1,2,3"|split(",") }}
{{ split(",", "1,2,3" }}

みたいなやつです。

ただ内部のカリー化と型の一貫性の都合で、キーワード引数のキーワード名を省略する表記は出来ません。

例えば指定した長さでリストをスライスし、あまった分はfill_withキーワードで指定できるsplice関数ですが、

{{ splice(3,[1,2,3,4], fill_with=0) }}

は list list で [[1,2,3], [4,0,0]] を返しますが、これと同じものを

{{ splice(3,[1,2,3,4], 0) }}

と書いてもfill_withキーワードがないので余った領域が0で埋められずに、[[1,2,3], [4]] が返ってきます。

その他諸々

えっと、長くなってきたのでこの辺にしときますが、他にも

{{ some_value is defined }}

みたいな is 構文である変数が定義されているかを知る事が出来たり、

{{ eval("{% set hoge = 'hoge' %}") }} 

みたいな荒業(これはjingooオンリー)もありますが、その辺はjingooのexampleフォルダとかtestsフォルダとかを見つつ確認してみてください。

現在はgithubで公開しているのですが、バグ報告やらforkなどは大歓迎です。