LoginSignup
9
11

More than 5 years have passed since last update.

テンプレートエンジンPebble(Java) - ガイド編

Last updated at Posted at 2017-11-04

TwigとかDjangoっぽい構文のJavaのテンプレートエンジンPebbleのドキュメントです。
結構カスタムできるみたいです。
Pebbleの基本的な使い方(はじめての導入からカスタムしたい上級者の方まで)を説明します。
デフォルトで使えるタグ、フィルター、関数、テスト、演算子の説明は↓に書きました。
テンプレートエンジンPebble(Java) - タグ・フィルター・関数・テスト・演算子編 - Qiita

この記事は以下ドキュメントを意訳・補足追加して作成しております。
http://www.mitchellbosecke.com/pebble/documentation
記載内容間違ってても責任取りませんが、教えていただけるとありがたいです。

インストール&設定

インストール

Maven Central Repositoryに公開されているので、pom.xmlに以下追加するだけで完了です。

pom.xml
<!-- https://mvnrepository.com/artifact/io.pebbletemplates/pebble -->
<dependency>
    <groupId>io.pebbletemplates</groupId>
    <artifactId>pebble</artifactId>
    <version>2.6.2</version>
</dependency>

補足:2.4.0以下のバージョンを使いたい場合はgroupIdが違うようです。
xml:pom.xml
<!-- https://mvnrepository.com/artifact/com.mitchellbosecke/pebble -->
<dependency>
<groupId>com.mitchellbosecke</groupId>
<artifactId>pebble</artifactId>
<version>2.4.0</version>
</dependency>
xml:pom.xml

セットアップ

もし、SpringMVCと統合したい場合こちらへ

PebbleEngineを始めるには、
テンプレートの編集、そしてPebbleにテンプレートをセットしてあげましょう。
テンプレートをコンパイルして結果を文字列で出すサンプルとなります。

PebbleEngine engine = new PebbleEngine.Builder().build();
PebbleTemplate compiledTemplate = engine.getTemplate("templateName");
Writer writer = new StringWriter();

Map<String, Object> context = new HashMap<>();
context.put("name", "Mitchell");

compiledTemplate.evaluate(writer, context);

String output = writer.toString();

Template Loader

PebbleEngineはテンプレートの読み込み方法を指定できます。
loaderを指定しない場合は、ClasspathLoaderとFileLoaderが自動的に内部でセットされます。

servletContextを使う場合
//loaderの引数でテンプレート読み込み方法を指定
PebbleEngine engine = new PebbleEngine.Builder()
    .loader(new ServletLoader(servletContext))
    .build();
Loader 説明 デフォルト
ClasspathLoader classpathから探す ON
FileLoader ファイルパスで探す ON
ServletLoader servlet context から探す。アプリケーションサーバー使う場合はこれを推奨。 OFF
StringLoader デバッグ用。テンプレート名のところをテンプレート内容と見なして動作します。 OFF
DelegatingLoader コレクションでLoaderを複数指定する場合に使います。 ON

Pebble Engine Settings

Setting Description Default
cache guavaを使ってテンプレートをキャッシュします。もしnullをセットした場合はキャッシュが無効になり、engine.getTemplateを呼び出す度にコンパイルします。 200テンプレートがキャッシュされる
defaultLocale コンパイルされた各テンプレートに渡されるデフォルトのロケール。 テンプレートは、i18nなどの関数にこのロケールを使用します。テンプレートには、評価中に一意のロケールを付けることもできます。 Locale.getDefault()
executorService parallelタグを使うときに使う。マルチスレッド用。 null
loader さっき説明した… DelegatingLoaderでClasspathLoaderとFileLoaderが有効になってます。
strictVariables trueをセットすると存在しない変数、属性にアクセスしたとき、nullの変数の属性にアクセスしようとしたときに例外を投げます。falseの場合は存在しない変数、属性の場合はスキップして処理します。 false

Springと連携

サンプル

完全に動くサンプルプロジェクトを参照用としてgithubに置いてあります。
プロジェクトをビルドするときはmvn installでビルドし、出力されたwarをデプロイできます。

Setup

Pebbleは3.xと4.x、SpringBootをサポートしています。それらは別々のライブラリとなります。
pom.xmlを以下追加してください。
これにより必要なViewResolver、Viewクラスが提供されます。

pom.xml
<dependency>
    <groupId>com.mitchellbosecke</groupId>
    <artifactId>pebble-spring{version}</artifactId>
    <version>${pebble.version}</version>
</dependency>

Spring Bootの場合

pom.xml
<!-- https://mvnrepository.com/artifact/com.mitchellbosecke/pebble-spring-boot-starter -->
<dependency>
    <groupId>com.mitchellbosecke</groupId>
    <artifactId>pebble-spring-boot-starter</artifactId>
    <version>2.4.0</version>
</dependency>

補足:マイクロフレームワークのspark frameworkにも対応してるみたいです。他にも対応してるFWあるっぽい?
サンプルソース※GitHub

pom.xml
<!-- https://mvnrepository.com/artifact/com.sparkjava/spark-template-pebble -->
<dependency>
    <groupId>com.sparkjava</groupId>
    <artifactId>spark-template-pebble</artifactId>
    <version>2.5.5</version>
</dependency>

次にテンプレートファイルがクラスパス上にあることを確認(例:/WEB-INF/templates/)
PebbleEngineとPebbleViewResolverを定義します。

MvcConfig.java
@Configuration
@ComponentScan(basePackages = { "com.example.controller", "com.example.service" })
@EnableWebMvc
public class MvcConfig extends WebMvcConfigurerAdapter {

    @Autowired
    private ServletContext servletContext;

    @Bean
    public Loader templateLoader(){
        return new ServletLoader(servletContext);
    }

    @Bean
    public SpringExtension springExtension() {
        return new SpringExtension();
    }

    @Bean 
    public PebbleEngine pebbleEngine() {
         return new PebbleEngine.Builder()
                .loader(this.templateLoader())
                .extension(springExtension())
                .build();
    }

    @Bean
    public ViewResolver viewResolver() {
        PebbleViewResolver viewResolver = new PebbleViewResolver();
        viewResolver.setPrefix("/WEB-INF/templates/");
        viewResolver.setSuffix(".html");
        viewResolver.setPebbleEngine(pebbleEngine());
        return viewResolver;
    }

}

@Controllerアノテーションがついたクラスは通常通りjspを使用するときと同様にテンプレート名を返すようにします。

ProfileController.java
@Controller
@RequestMapping(value = "/profile")
public class ProfileController {

    @Autowired
    private UserService userService;

    @RequestMapping
    public ModelAndView getUserProfile(@RequestParam("id") long id) {
        ModelAndView mav = new ModelAndView();
        mav.addObject("user", userService.getUser(id));
        mav.setViewName("profile");
        return mav;
    }

}

上記の例は\WEB-INF\templates\profile.htmlをレンダリングします。
そしてuserオブジェクトはテンプレート内で使用できます。

Pebbleのもついくつかの機能

Spring beansへのアクセスについて

もちろんテンプレート内でSpring beansにアクセスできます。

{{ beans.beanName }}

httpリクエスト、httpレスポンス、httpセッションにもアクセスできます。

{{ request.contextPath }}
{{ response.contentType }}
{{ session.maxInactiveInterval }}

springのための拡張機能

Href function
コンテキストパスを自動的に付与してくれる

<a href="{{ href('/foobar') }}">Example</a>

Message function
i18n関数と同じような動作をします。少し違うのは、設定されたSpring messageSource(通常はResourceBundleMessageSource)を使います

Label = {{ message('label.test') }}
Label with params = {{ message('label.test.params', 'params1', 'params2') }}

Spring validations and error messages

To check if there's any error:

エラーがあるか無いかを知りたい場合

{{ hasErrors('formName' }}

{{ hasGlobalErrors('formName' }}

{{ hasFieldErrors('formName', 'fieldName' }}

複数のエラーを処理する場合

{% for err in getAllErrors('formName') %}
    <p>{{ err }}</p>
{% endfor %}

{% for err in getGlobalErrors('formName') %}
    <p>{{ err }}</p>
{% endfor %}

{% for err in getFieldErrors('formName', 'fieldName') %}
    <p>{{ err }}</p>
{% endfor %}

Timer
PebbleViewを使用して、テンプレートの処理に要した時間を出力できます。
log4j.xmlに追記するだけです。

log4j.xml
<Logger name="com.mitchellbosecke.pebble.spring.PebbleView.timer" level="DEBUG" additivity="false">
      <AppenderRef ref="STDOUT" />
</Logger> 

基本的な使い方

紹介

Pebbleテンプレートはあらゆるものをテキスト出力できます。
一般的な使い方ではHTMLを想定していますが、CSS, XML, JSなどにも使えます(補足:僕はSQLのテンプレートエンジンに使ってるよ)。
テンプレートには出力したい言語の構文とPebbleの構文の両方が入ることになります。
ここにちょっとしたHTMLのテンプレート例を出します。

<html>
    <head>
        <title>{{ websiteTitle }}</title>
    </head>
    <body>
        {{ content }}
    </body>
</html>

このテンプレートを評価(つまりevaluateメソッドの呼び出し)する際は、
上記のwebsiteTitlecontentをキーにしたmapオブジェクトが必要です。

Pebbleエンジンのオブジェクトの作成、テンプレートのコンパイルには以下コードが必要です。

PebbleEngine engine = new PebbleEngine.Builder().build();

Pebbleエンジンによるコンパイルは以下の通り

PebbleTemplate compiledTemplate = engine.getTemplate("templates/home.html");

最後に、java.io.Writerオブジェクトと、Mapオブジェクトを用意してください。

Writer writer = new StringWriter();

Map<String, Object> context = new HashMap<>();
context.put("websiteTitle", "My First Website");
context.put("content", "My Interesting Content");

compiledTemplate.evaluate(writer, context);

String output = writer.toString();

構文

説明
{{ ... }} 式の結果を返します。変数名などを指定してください。
{% ... %} テンプレートのフローを制御します。例えば、{% if... %},{% extends... %},{% block... %}があります。

変数

変数を直接テンプレートへ出力する例です。
"bar"という値が入ったfooという変数がmapオブジェクトに含まれている場合、"bar"と出力します。

{{ foo }}

ドット(.)を使って、変数の属性にもアクセスできます。属性値が不定の場合は連想配列チックに[]も使えます。

{{ foo.bar }}
{{ foo["bar"] }}

foo.barと記述した場合、内部の動作として、以下アクセスを試みます。

  • If foo is a map, foo.get("bar")
  • foo.getBar()
  • foo.isBar()
  • foo.hasBar()
  • foo.bar()
  • foo.bar

もし、値がnullの場合は、空文字を出力します(デフォルト設定では)。

型安全

Pebbleは動的な型を持っています。そして、実際に評価されるまでは、型安全の問題は発生しません。
補足:テンプレートには実際に無い変数名も記述可能だという意味だと思われます。
しかし、Pebbleでは、型安全の問題の処理方法をstrictVariablesの設定で選べます。
デフォルトではfalseになっています。

{{ foo.bar }}

例えば、この例で、fooがbarという属性を持っていない、且つ、strictVariablesがtrueの設定の場合、
テンプレートを評価した際にExceptionが投げられます。
strictVariablesがfalseの場合は、nullsafeでもあるので、以下のfooやbarがnullでも空文字を出力します。

{{ foo.bar.baz }}

defaultフィルターがこの状況では最適かもしれません。
補足:defaultフィルターは値がnullや空文字・空リストの場合にデフォルトの文言を出力してくれます。

フィルター

フィルターは出力からさらに出力を修正できる機能です。
パイプ(|)を使って変数名、フィルター名(とその引数)を区切ります。
複数のフィルタを使う場合はつなげて書けます。このように…。

{{ "If life gives you lemons, eat lemons." | upper | abbreviate(13) }}

上の例では、以下のように出力されます。

IF LIFE GI...

関数

フィルターは出力を修正するだけのものでしたが、
関数は、新しい機能を作り出すものです。他の言語と一緒ですね。()を使って呼び出します。

{{ max(user.score, highscore) }}

制御構文

Pebbleではいくつかの制御構文があります。メインとしてforとifです。

{% for article in articles %}
    <h3>{{ article.title }}</h3>
    <p>{{ article.content }}</p>
{% else %}
    <p> There are no articles. </p>
{% endfor %}
{% if category == "news" %}
    {{ news }}
{% elseif category == "sports" %}
    {{ sports }}
{% else %}
    <p>Please select a category</p>
{% endif %}

他テンプレートからinclude

includeタグを使って、他テンプレートから出力を持ってくることができます。

<div class="sidebar">
    {% include "advertisement.html" %}
</div>

テンプレートの継承

Pebbleの強みでもある機能です。
子テンプレートにOverride可能なセクションを設けることができます。
親テンプレートにblockタグを記述します。
まず、この親テンプレートを見てください。

<html>
<head>
    <title>{% block title %}My Website{% endblock %}</title>
</head>
<body>
    <div id="content">
        {% block content %}{% endblock %}
    </div>
    <div id="footer">
        {% block footer %}
            Copyright 2013
        {% endblock %}
    </div>
</body>
</html>

上記は、blockタグ箇所が子テンプレートでoverride可能です。

子テンプレートです。

{% extends "./parent.html" %}

{% block title %} Home {% endblock %}

{% block content %}
    <h1> Home </h1>
    <p> Welcome to my home page.</p>
{% endblock %}

extendsタグがありますね。まず、最初にこのタグがないとダメです。1個しか使えません。
(補足:Javaと似てますね)。

この子テンプレートを評価すると、このように出力されます。


<html>
<head>
    <title>Home</title>
</head>
<body>
    <div id="content">
        <h1> Home </h1>
        <p> Welcome to my home page.</p>
    </div>
    <div id="footer">
        Copyright 2013
    </div>
</body>
</html>

この例では、footerブロックを子テンプレートではoverrideしていないですね。
その場合は、親テンプレートで記述したものが使用されます。

動的な継承も使えます。

{% extends ajax ? 'ajax.html' : 'base.html' %}

マクロ

マクロはテンプレートのパーツを再利用できます。
macroタグを使います。
補足:似たようなことはfunctionの拡張のJavaコードを書くことでもできますが、
HTMLのコードをjava内に書きたくない場合とか、特に演算処理は無くて、テンプレートの記述だけで十分な場合とかに使うんでしょうかね。

{% macro input(type, name) %}
    <input type="{{ type }}" name="{{ name }}" />
{% endmacro %}

関数のように呼び出して使えます。

{{ input("text", "name", "Mitchell") }}

子テンプレートは親テンプレートで定義したマクロを使用できます。
違うファイルにあるマクロを使いたい場合、importタグを使用してください。
マクロはMapオブジェクト(context)にはアクセスできません。
引数にしかアクセスできません。

名前付き引数

フィルター、関数、テスト、マクロで名前付き引数が使えます。
デフォルトの値から変更しない場合、引数の指定をしなくてもよくなります。

{{ stringDate | date(existingFormat="yyyy-MMMM-d", format="yyyy/MMMM/d") }}

位置指定による引数と名前付き引数はごちゃまぜにできますが、
位置指定による引数は名前付き引数より前に来ないといけません。

{{ stringDate | date("yyyy/MMMM/d", existingFormat="yyyy-MMMM-d") }}

マクロを使うケースでは、特に役に立ちます。
というのも、しばしば未使用の引数が出てくることが多いからです。
```
{% macro input(type="text", name, value) %}

{% endmacro %}

{{ input(name="country") }}

{# will output: #}
```

エスケープ

XSSの脆弱性はWEBアプリの代表的なものですね。
それを回避するために、潜在的に安全でないデータをエスケープする必要があります。
Pebbleはオートエスケープの機能がデフォルトで有効になっています。
オートエスケープは無効にもできます。
Pebbleではもっと細かいエスケープの設定ができます。

変数にセットした値がどのようにエスケープされるかの例です。

{% set danger = "<br>" %}
{{ danger }}

{# will output: &lt;br&gt; #}

もし、オートエスケープを無効にしてる場合、このようにしてエスケープしてください。

{% set danger = "<br>" %}
{{ danger | escape }}

{# will output: &lt;br&gt; #}

デフォルトではHTMLのエスケープの設定となっています。
escapingストラテジー(補足:内部ロジックがストラテジーパターンになっているようです)は選べます。

{% set danger = "alert(...)" %}
<script>var username="{{ danger | escape(strategy="js") }}"</script>

エスケープガイドのほうも参照ください。
もっと詳しい情報があります。どうやって無効にするか、どうやってescapingストラテジーを切り替えるかといった内容があります。

空白

Pebbleタグ直後にある最初の改行はデフォルトでは無視されます。
それ以外は通常通りに出力します。

Pebbleは前後にある空白をトリムする機能も持っています。このように記述します。{{- ... -}}というデリミタを使います。

<p>         {{- "no whitespace" -}}     </p>
{# output: "<p>no whitespace</p>" #}

このように記述すると片方のみトリムしてくれます。

<p>         {{- "no leading whitespace" }}      </p>
{# output: "<p>no whitespace        </p>" #}

コメント

テンプレートのパーツにコメントをつけることもできます。
{# ... #}というデリミタです。これは出力されないです。

{# THIS IS A COMMENT #}
{% for article in articles %}
    <h3>{{ article.title }}</h3>
    <p>{{ article.content }}</p>
{% endfor %}

Pebbleのテンプレートに出てくる式はJavaに出てくるものと非常に似ています。

リテラル

リテラルは非常にシンプルな式ですね。JavaのStringと整数型が使えます。

  • "Hello World":シングルクォーテーションかダブルクォーテーションでくくった文字列です。バックスラッシュのエスケープが使えます。
  • 100 * 2.5:整数や小数が使えます。Javaと同様です。
  • true / false:Booleanです。これもJavaと同様です。
  • null:これもJavaと同様です。noneはnullの別名となります。

コレクション

リストやマップをテンプレート内で直接使えます。

  • ["apple", "banana", "pear"]: A list of strings
  • {"apple":"red", "banana":"yellow", "pear":"green"}: A map of strings

算術

基本的な算術演算もサポートしています。これらをサポートしています。

  • +: Addition
  • -: Subtraction
  • /: Division
  • %: Modulus
  • *: Multiplication

ロジック

論理演算で2つの式を結合できます。これらが使えます。

  • and: Returns true if both operands are true
  • or: Returns true if either operand is true
  • not: Negates an expression
  • (...): Groups expressions together

比較演算

これらがサポートされています。

==, !=, <, >, >=<=

{% if user.age >= 18 %}
    ...
{% endif %}

テスト

is演算子はテストを実行します。
このテスト機能は特定の値などを判定するために使用できます。
isの右側のオペランドはテストの名前です。

{% if 3 is odd %}
    ...
{% endif %}

テスト機能はnot演算子により、否定が使えます。

{% if name is not null %}
    ...
{% endif %}

三項演算子

三項演算子も使えます。

{{ foo ? "yes" : "no" }}

演算子の優先順位

上が優先順位高です。

  • .
  • |
  • %, /, *
  • -, +
  • ==, !=, >, <, >=, <=
  • is, is not
  • and
  • or

Pebbleの拡張

概要

Pibbleは柔軟に色々なプロジェクトに対応できるように設計されています。
タグ、関数、演算子、フィルター、テストクラス、グローバル変数を自分で作って追加することもできます。
これらの実装は、ほぼ簡単ですぐにできます。
まず、Extensionインタフェースを実装したクラスを作成しましょう。
それには楽なやり方があります。Extensionインタフェースを実装済みのAbstractExtensionを継承したクラスを作成することです。
そして実装したら、テンプレートのコンパイル処理を行う前のところでPebbleEngineに登録しましょう。

PebbleEngine engine = new PebbleEngine.Builder().extension(new CustomExtension()).build();

フィルター

カスタムフィルターを作るには、まず先ほど説明したExtensionクラスのgetFilters()メソッドを実装します。
このメソッドはkey=フィルター名、value=フィルタークラスのインスタンスとなるMapオブジェクトを返すように作成します。
実装したら、次はFilterインターフェイスを実装しなければなりません。
FilterクラスはgetArgumentNames()とapply()の2つのメソッドを実装します。
getArgumentNamesメソッドは仮引数の名前と順序を定義するListを返すように実装します。
(スクリプト言語で言うキーワード引数のようなものです。)
補足:定義無しで良い場合はnullを返すように実装します。

applyメソッドは実際のFilter処理を実装します。
第1引数はフィルターしたいデータです。
第2引数ははフィルターの引数です。
(補足:{{ 'Mitchell' | slice(1,3) }}←この1,3という引数のことです)
Pebbleは動的型付けのため、Object型からダウンキャストしてお使いください。
フィルターのサンプルです。

public UpperFilter implements Filter {

    @Override
    public List<String> getArgumentNames() {
        return null;
    }

    @Override
    public Object apply(Object input, Map<String, Object> args){
        if(input == null){
            return null;
        }
        String str = (String) input;
        return str.toUpperCase();
    }

}

args mapはデフォルトで2要素含んでいます。

_self: PebbleTemplateのインスタンス。テンプレート名を取得したりなどできます。
_context: EvaluationContextのインスタンス。localeを取得したりなどできます。

テストクラス

フィルターとほぼ同様に実装できます。test名と該当するテストオブジェクトのマップを返すgetTests()を実装するだけです。
そして、テストクラスはTestinterfaceを実装します。
TestFilterと同じようなインターフェイスを持っています。
Filterのapplyメソッドは任意のオブジェクトを返していましたが、Testクラスではbooleanを返すようにします。

evenテストの実装例です。

public EvenTest implements Test {

    @Override
    public List<String> getArgumentNames() {
        return null;
    }

    @Override
    public boolean apply(Object input, Map<String,Object> args){
        Integer in = (Integer) input;
        return (in % 2 == 0);
    }

}

Functions

functionsもまたFilterと似たようなものです。
しかし、どちらで実装すべきか使い分けがが微妙なので、ちゃんと理解しておきましょう。
Filterの場合は今ある既存のコンテンツを修正し、新たなコンテンツを出力することを意図しています。

ではfunctionsを実装してみましょう。function名と該当するfunctionオブジェクトのマップを返すgetFunctions()を実装します。
そして、Functionインターフェイスを実装したfunctionクラスを実装します。
FilterTestと同じようなものです。
ここにfibonacciStringfunctionの実装例を示します(pebbleライブラリには無いものです)。

public FibonnaciStringFunction implements Function() {

    @Override
    public List<String> getArgumentNames() {
        List<String> names = new ArrayList<>();
        names.put("length");
        return names;
    }

    @Override
    public Object execute(Map<String,Object> args) {
        Integer length = (Integer)args.get("length");
        Integer prev1 = 0;
        Integer prev2 = 1;

        StringBuilder result = new StringBuilder();

        result.append("01");

        for(int i = 2; i < length; i++){
            Integer next = prev1 + prev2;
            result.append(next);
            prev1 = prev2;
            prev2 = next;
        }
        return result.toString();

    }
}

args mapはデフォルトで2要素含んでいます。

_self: PebbleTemplateのインスタンス。テンプレート名を取得したりなどできます。
_context: EvaluationContextのインスタンス。localeを取得したりなどできます。

位置と引数名について

Filter、Tests、Functionsはnullを返す場合でもgetArgumentNamesメソッドの実装が必要です。
文字列のListを返すようにすると、テンプレート作成者は名前付きのパラメーターを使用できます。
先ほどのfibonacciStringfunctionでの使用例です。2通りの呼び出し方があります。

{{ fibonacci(10) }}
{{ fibonacci(length=10) }}

テンプレート作成者がパラメーターに名前を省略し、定位置で引数を書いた場合、
Functionを呼び出すときに本来の名前(補足:getArgumentNamesで定義した名前)でマッピングされます。
そのため、実装する際にテンプレート作成者が名前付きパラメーターで呼び出したか、
名前を省略して呼び出したかを考慮する必要はありません。
要するに、filter/function/testが複数のパラメーターを持つ場合は、テンプレート作成者が名前付きパラメーターを使わない場合、
引数の順序を伝える必要があります。

minFunctionやmaxFunctionは引数を無制限に入れることができます。
このようなタイプのfunctionは名前付き引数を使うことができません。
この場合のgetArgumentNamesメソッドの実装はnullか空のリストを返すようにしてください。
その場合は、Pebble側で自動的にキーを付与したmapがexecuteメソッドへ渡されます。
(補足:"1","2"...といったキーでmapが作られます。)

グローバル変数

すべてのテンプレートからアクセスできるグローバル変数を作ることも簡単です。
カスタムしたExtensionのgetGlobalVariables()をMapを返すように実装するだけです。
テンプレートとマージした際(補足:template.evaluate(writer, context)を呼んだ際)に各contextとマージされます。

演算子

演算子はfilterやtestsより実装は複雑です。
カスタムExtensionのgetBinaryOperators()メソッドとgetUnaryOperators()メソッドの片方、または両方を実装します。
それぞれList、Listを返すようにします。

BinaryOperatorを実装するには以下が必要になります。

  • 優先順位: int型で演算子の優先順位を付けます

  • 記号: 実際の演算子の記号です。たいてい1文字ですが、そうでなくてもOKです。

  • Expression Class: BinaryExpressionクラスを返します。このクラスは実際の演算子の処理を実装します。

  • 演算子が作用する方向(右か左か)(補足:実装的には左を指定すると優先順位※後述が+1になるようです)

unary operatorの実装はUnaryExpressionの継承とassociativityの定義が不要な点以外はBinaryOperatorと同じです。

優先順位(precedence)の一覧です。

or: 10
and: 15
is: 20
is not: 20
==: 30
!=: 30
>: 30
<: 30
>=: 30
<=: 30
+: 40
-: 40
not: 50 (Unary)
*: 60
/: 60
%: 60
|: 100
+: 500 (Unary)
-: 500 (Unary)

+演算子の実装例です。

public AdditionOperator implements BinaryOperator {

    public int getPrecedence(){
        return 30;
    }

    public String getSymbol(){
        return "+";
    }

    public Class<? extends BinaryExpression<?>> getNodeClass(){
        return AdditionExpression.class;
    }

    public Associativity getAssociativity(){
        return Associativity.LEFT;
    }

}

さらに演算子の実際の処理を記述するBinaryExpressionクラスの実装が必要です。
上記のサンプルのAdditionOperatorが参照しているAdditionExpressionの実装例です。

public AdditionExpression extends BinaryExpression<Object> {

    @Override
    public Object evaluate(PebbleTemplateImpl self, EvaluationContext context){
        Integer left = (Integer)getLeftExpression().evaluate(self, context);
        Integer right = (Integer)getRightExpression().evaluate(self, context);

        return left + right;
    }

}

上記例外にleftExpressionrightExpressionがありますね。
それはオペランド(補足:5*4の場合「5」と「4」がオペランドとなります)です。
上記サンプルではIntegerへキャストしていますが、必ずしもキャストがうまくいくとは限りません。
本来の加算する演算子の場合、Integer以外も想定する必要があり、実際はもっと複雑な実装となります。

タグ

新しいタグの作成は、Pebbleの最も強力な機能の1つです。
まず、getTokenParsers()メソッドの実装が必要です。
com.mitchellbosecke.pebble.tokenParser.TokenParserインターフェイスは適切なRenderableNodeを返すようにします。
parseメソッドのtoken引数は演算子、空白、変数名、デリミタなどの情報を持っており、
RenderableNodeは出力用の型です。

setタグの実装例です。

public class SetTokenParser extends AbstractTokenParser {

    @Override
    public RenderableNode parse(Token token, Parser parser) throws ParserException {
        TokenStream stream = parser.getStream();
        int lineNumber = token.getLineNumber();

        // skip the 'set' token
        stream.next();

        String name = parser.getExpressionParser().parseNewVariableName();

        stream.expect(Token.Type.PUNCTUATION, "=");

        Expression<?> value = parser.getExpressionParser().parseExpression();

        stream.expect(Token.Type.EXECUTE_END);

        return new SetNode(lineNumber, name, value);
    }

    @Override
    public String getTag() {
        return "set";
    }
}

getTag()メソッドはタグ名を返すようにしてください。
そうすれば、Pebbleのメインパーサはカスタムパーサーへ処理を委譲します。

parseメソッドはPebbleの一次パーサがカスタムタグを検出する度に呼び出されます。
このメソッドはWriterオブジェクトへ書き込みするRenderableNodeインスタンスを返す必要があります。
RenderableNodeに子要素が含まれる場合、nodeクラス内のrenderメソッド内で処理する必要があります。
構文解析の方法について学ぶのに以下のソースを見ることをおすすめします。
TokenParser
Parser
SetTokenParser
ForTokenParser
IfNode
SetNode

9
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
11