CLOVER🍀

That was when it all began.

テンプレートエンジンmustacheを使ってみる

いろんな言語で使うことができるテンプレートエンジン、mustacheというものがあるそうです。

「ロジック・レス・テンプレート」といってるだけあって、構文もシンプルな感じです。

mustache
http://mustache.github.com/

manual
http://mustache.github.com/mustache.5.html

正確には、いろんな言語で使うことができるというよりはいろんな言語の実装が用意されている、というのが正確な表現なのでしょうが。

今回は、Java版を使ってみます。

mustache.java
https://github.com/spullara/mustache.java

基本的な使い方

Mavenなり、Gradleなりを使って、依存関係の定義をしましょう。自分は、Gradleを使用しました。

apply plugin: 'java'

version = '0.0.1-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    compile 'com.github.spullara.mustache.java:compiler:0.8.8'
}

あとは、以下のコードを使って実行します。

        MustacheFactory mf = new DefaultMustacheFactory();
        Mustache mustache = mf.compile("テンプレートのパス");
        mustache.execute(結果を出力するWriterオブジェクト, テンプレートにバンドするオブジェクト).flush();

例えば、標準出力に結果を吐き出す場合は、こんな感じで。

package example.mustachej;

import java.io.PrintWriter;

import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheFactory;

public class Example {
    public static void main(String[] args) throws Exception {
        MustacheFactory mf = new DefaultMustacheFactory();
        Mustache mustache = mf.compile("template/simple-template.mustache");
        mustache.execute(new PrintWriter(System.out), new Example()).flush();
    }

    String message = "Hello World";
}

この場合は、Exampleクラス自身がテンプレートにバインドされるオブジェクトになっています。
バインドされるオブジェクトは、JavaのクラスかMapであればよいみたい。ただ、JavaBeansは相手にしていないみたいです。

テンプレートは、{{と}}を使って書きます。

Message Here
{{message}}

では、簡単なサンプルとテンプレートで機能を見ていきます。
以降のサンプルでは、テンプレートにExampleをバインドするものとして記載しています。

変数出力

{{変数名}}で書きます。

Java側

public class Example {
    String message = "Hello World";
}

テンプレート側

Message Here
{{message}}

出力結果

Message Here
Hello World

ちなみに、出力はHTMLエスケープされます。

public class Example {
    String message = "<p>Hello World</p>";
}

エスケープしないようにするには、括弧の数を増やすか、&を使用するらしいです。

Message Here
{{message}}
{{{message}}}
{{& message}}

出力結果

Message Here
&lt;p&gt;Hello World&lt;/p&gt;
<p>Hello World</p>
<p>Hello World</p>

セクション

{{#セクション名}}と{{/セクション名}}で囲います。
使い方は、いくつかあるみたいです

真偽判定

true/falseを適用した場合。

Java側

public class Example {
    boolean show = true;
    String message = "Hello World";
}

テンプレート側

{{#show}}
Message Here
  {{message}}
{{/show}}

出力結果

Message Here
  Hello World

Java側をこう変更すると…

public class Example {
    boolean show = false;
    String message = "Hello World";
}

何も出力されなくなります。

繰り返し

Iterableなコレクションを渡します。リストの中身が0件の時は、要はfalseとして扱われるみたいです。

Java側

public class Example {
    @SuppressWarnings("unchecked")
    List<Item> items() {
        return Arrays.asList(new Item[] {new Item("item 1", 100), new Item("item 2", 200)});
    }

    static class Item {
        String name;
        int price;
        Item(String name, int price) {
            this.name = name;
            this.price = price;
        }
    }
}

テンプレート側

Items Here.
{{#items}}
Item:
  name:{{name}}
  price::{{price}}
{{/items}}

出力結果

Items Here.
Item:
  name:item 1
  price::100
Item:
  name:item 2
  price::200

ここで、Java側を空のリストを返すようにすると…

public class Example {
    @SuppressWarnings("unchecked")
    List<Item> items() {
        return Collections.emptyList();
        // return Arrays.asList(new Item[] {new Item("item 1", 100), new Item("item 2", 200)});
    }
...
}

{{#items}}の中身は、評価されなくなります。

Items Here.

なお、{{#セクション名}}の中身は、別に繰り返し可能なオブジェクトであることが必須なわけではありません。

public class Example {
    Map<String, String> person() {
        Map<String, String> map = new HashMap<>();
        map.put("name", "Fred");
        return map;
    }
}

テンプレート側

{{#person}}
  Hi {{name}}!
{{/person}}

出力結果

  Hi Fred!

この場合は、単一の結果出力になりますね。

falseまたは繰り返しが0件の時に、別の結果を出力する

{{^セクション名}}を使用すればよいみたいです。

Items Here.
{{#items}}
Item:
  name:{{name}}
  price::{{price}}
{{/items}}
{{^items}}
Non Items
{{/items}}

繰り返しが1件以上ある時、先ほどの例のようになり、0件の時は以下のようになります。

Items Here.
Non Items

また、コレクションではなくtrue/falseをバインドしている時も使用することができますし、{{#セクション名}}とセットではなく、単独で使用することもできます。

{{^items}}
Non Items
{{/items}}
関数呼び出し

{{#セクション}}にバインドされる値に関数オブジェクトを使用することで、関数呼び出しを行うことができます。

関数クラスは、Google Guavaを使って実装されているということなので、それを使ってまずは実装してみました。

Java側

import com.google.common.base.Function;

public class Example {
    String message = "Hello World";
    public Function<String, String> decorate() {
        return new Function<String, String>() {
            @Override
            public String apply(String text) {
                return "<p>" + text + "</p>";
            }
        };
    }
}

テンプレート側

{{#decorate}}
  Message is {{message}}
{{/decorate}}

出力結果

<p>  Message is {{message}}
</p>

テンプレートに改行が入ってるので、それもFunction#applyの引数に入ったみたいですね…。
それより、{{message}}が展開されていません。

はて?と思ってドキュメントをもう1回見たら、その次の行に再パースしたかったらTemplateFunctionを使えと書いてあります…。

というわけで、もう1回。

Java側

import com.github.mustachejava.TemplateFunction;

public class Example {
    String message = "Hello World";
    public TemplateFunction decorate() {
        return new TemplateFunction() {
            @Override
            public String apply(String text) {
                return "<p>" + text + "</p>";
            }
        };
    }
}

テンプレートは変えず、もう1度実行

<p>  Message is Hello World
</p>

今度は、変数も展開されました。

コメント

{{! }}で囲みます。

{{! This is Comment }}

中に変数やセクションのような、}}で終わるものが含まれていると、そこでコメントアウトは終了するので注意しましょう。

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

{{> テンプレート名}}で、別のテンプレートをインクルードすることができるようです。インクルード先のテンプレートには、イメージ的には元のテンプレートに展開された状態で評価される感じっぽいですね。

Java側

public class Example {
    List<Map<String, String>> names() {
        List<Map<String, String>> names = new ArrayList<>();
        Map<String, String> user = new HashMap<>();
        user.put("name", "Ken");
        names.add(user);
        return names;
    }
}

ベーステンプレート

<h2>Names</h2>
{{#names}}
  {{> template/user}}
{{/names}}

サブテンプレート

<strong>{{name}}</strong>

実行結果

<h2>Names</h2>
  <strong>Ken</strong>

{{> テンプレート名}}で指定したファイル名には、暗黙的に「.mustache」が付与されるようなので注意してくださいね。