Ruby の tilt-mustache パッケージを使って Mustache テンプレートを処理する

## 概要

- Ruby の tilt-mustache パッケージを使って Mustache テンプレートを処理する

## 今回の環境

- macOS Catalina
- Ruby 2.7.0
- tilt-mustache 0.0.2
- Mustache 1.1.1
- Tilt 2.0.10
- Sinatra 2.0.8.1
- Puma 4.3.1

## Tilt とは

様々なテンプレートエンジンを統一して扱えるインターフェースを持ったライブラリ。

[tilt \| RubyGems\.org \| コミュニティのGemホスティングサービス](https://rubygems.org/gems/tilt)

> Generic interface to multiple Ruby template engines

tilt を導入することで、複数のテンプレートエンジンに対応したライブラリ (Web アプリケーションフレームワークや静的サイトジェネレーターなど) が作りやすくなる。

[rtomayko/tilt: Generic interface to multiple Ruby template engines](https://github.com/rtomayko/tilt)

> Tilt is a thin interface over a bunch of different Ruby template engines in an attempt to make their usage as generic as possible. This is useful for web frameworks, static site generators, and other systems that support multiple template engines but don't want to code for each of them individually.

## Tilt は一時期 Mustache をサポートしていたが今はしていない

Tilt と Mustache のコンセプト的なところで合わないということなのだろうか。

[Tilt integration · Issue \#72 · mustache/mustache](https://github.com/mustache/mustache/issues/72)

> Tilt had a MustacheTemplate at one point but we ripped it out because of Mustache's inverted view/template relationship made it feel a little wonky. I actually prefer using Mustache's normal view-oriented interface in Sinatra as opposed to the render support. I think you miss out on a lot of the benefits of mustache when you try to shoehorn it into the single template file approach used by most other template systems.

[Any Reason For Using Only Symbols in Locals Hash? · Issue \#72 · rtomayko/tilt](https://github.com/rtomayko/tilt/issues/72)

> That would require some more render-time code. I'm not sure if it's worth adding edge cases like this when they will both decrease performance and make the code more complex. Tilt doesn't have to be flexible when it comes to the locals-hash.
> 
> Anyway, we should still raise some exceptions if any of the keys are not a symbol.

[Mustache support by Becojo · Pull Request \#51 · rtomayko/tilt](https://github.com/rtomayko/tilt/pull/51)

> Mustache isn't a regular template engine in the way that it requires two parts: a template and an implementation (in Ruby). Chris (of Mustache) closed the equivalent issue at Mustache's issue tracker because a Tilt implementation (at least the one provided here) wouldn't be able to take advantage of Mustache's core concept.
> 
> This patch also uses an interpreted mode even when Mustache is actually compiled behind the scenes.

## tilt-mustache パッケージとは

Tilt に採用されなかった Pull Request のコードを元に、別にパッケージ化したものが tilt-mustache パッケージらしい。

[tilt\-mustache \| RubyGems\.org \| コミュニティのGemホスティングサービス](https://rubygems.org/gems/tilt-mustache/versions/0.0.2)

> Add Mustache to Tilt

[DAddYE/tilt\-mustache: Add Mustache to Tilt](https://github.com/DAddYE/tilt-mustache)

> This gem is a verbatim copy of this tilt pull request done by @Becojo

## tilt-mustache パッケージのインストール

依存関係で mustache と tilt の現時点(2020年2月10日現在)での最新版もインストールされる。

```
$ gem install tilt-mustache
Fetching mustache-1.1.1.gem
Fetching tilt-mustache-0.0.2.gem
Fetching tilt-2.0.10.gem
(以下略)
```

## Mustache テンプレートの拡張子

tilt-mustache 0.0.2 では Mustache テンプレートは拡張子 mustache と ms に関連付けられている。

[tilt\-mustache/tilt\-mustache\.rb at v0\.0\.2 · DAddYE/tilt\-mustache](https://github.com/DAddYE/tilt-mustache/blob/v0.0.2/lib/tilt-mustache.rb)

```rb
register 'mustache', MustacheTemplate
register 'ms', MustacheTemplate
```

## Hello World

HTML を記述した Mustache テンプレートファイルを用意。
今回は hello.ms というファイル名で保存する。

```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">

<!-- title を出力-->
<title>{{title}}</title>

</head>
<body>

<!-- message を出力-->
<p>{{message}}</p>

</body>
</html>
```

ソースコード。

```rb
require 'tilt-mustache'

# 変数を定義
title   = 'Hello, world.<&>'
message = 'こんにちは、世界。'

# Tile に包まれた Mustache テンプレートエンジン
template = Tilt.new('hello.ms')

# self スコープでレンダリング
output = template.render(self, :title => title, 'message' => message)

# 結果を出力
puts output
```

実行結果。

```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">

<!-- title を出力-->
<title>Hello, world.&lt;&amp;&gt;</title>

</head>
<body>

<!-- message を出力-->
<p>こんにちは、世界。</p>

</body>
</html>
```

## ループや条件分岐など

HTML を記述した Mustache テンプレートファイルを用意。
今回は my-template.mustache というファイル名で保存する。

```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- title を出力-->
<title>{{title}}</title>
</head>
<body>

<!-- mydata オブジェクトの message を出力-->
<p>{{mydata.message}}</p>

<p>
<!-- ループ -->
{{#mydata}}
  {{#list}}
    {{.}}<br>
  {{/list}}
{{/mydata}}
</p>

<p>
<!-- hoge が存在する場合に出力-->
{{#mydata.hoge}}
  Hoge exists.
{{/mydata.hoge}}
</p>

<p>
<!-- fuga が存在しない場合に出力-->
{{^mydata.fuga}}
  Fuga does not exists.
{{/mydata.fuga}}
</p>

</body>
</html>
```

ソースコード。

```rb
require 'tilt-mustache'

# Tile に包まれた Mustache テンプレートオブジェクト
template = Tilt.new('my-template.mustache')

# 第一引数に他に影響されないスコープとして Object.new を指定
# 第二引数以降にHashオブジェクト
output = template.render(
  Object.new,
  :title => 'タイトル',
  :mydata => {
    :message => 'メッセージ',
    'list' => ['foo', 'bar', 'baz'],
    'hoge' => 'ほげ'
  })

# 結果を出力
puts output
```

実行結果。

```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- title を出力-->
<title>タイトル</title>
</head>
<body>

<!-- mydata オブジェクトの message を出力-->
<p>メッセージ</p>

<p>
<!-- ループ -->
    foo<br>
    bar<br>
    baz<br>
</p>

<p>
<!-- hoge が存在する場合に出力-->
  Hoge exists.
</p>

<p>
<!-- fuga が存在しない場合に出力-->
  Fuga does not exists.
</p>

</body>
</html>
```

## Sinatra で Mustache を使う

### ソースコード一覧

```
├── app.rb
└── views
    └── hello.mustache
```

### app.rb

Mustache は Sinatra でサポートされていない。

Sinatra にテンプレートエンジンを追加するには、Tilt でテンプレートエンジンを登録するのと、レンダリングメソッドを作る必要がある。

Tilt でテンプレートエンジンを登録するのは tilt-mustache パッケージがやってくれている。

レンダリングメソッドを作るのは自分でやる必要がある。

```rb
require 'sinatra'
require 'tilt-mustache'

# レンダリングメソッドを作る
helpers do
  def mustache(*args) render(:mustache, *args) end
end

get '/hello/:message' do

  # HTML に埋め込む値
  values = {
    :message    => params['message'],
    'ruby_desc' => RUBY_DESCRIPTION
  }

  # HTML を表示
  mustache :hello, :locals => values
end
```

### views/hello.mustache

HTML を記述した Mustache テンプレートファイル。

```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello, World!</title>
</head>
<body>
<p>{{ message }}</p>
<p>{{ ruby_desc }}</p>
</body>
</html>
```

### サーバを起動

```
$ ruby app.rb 
== Sinatra (v2.0.8.1) has taken the stage on 4567 for development with backup from Puma
Puma starting in single mode...
* Version 4.3.1 (ruby 2.7.0-p0), codename: Mysterious Traveller
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://127.0.0.1:4567
* Listening on tcp://[::1]:4567
Use Ctrl-C to stop
```

### curl でアクセス

必要な値が HTML に埋め込まれている。

```
$ curl -i "http://localhost:4567/hello/こんにちは&世界"
HTTP/1.1 200 OK
Content-Type: text/html;charset=utf-8
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Content-Length: 219

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello, World!</title>
</head>
<body>
<p>こんにちは&amp;世界</p>
<p>ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-darwin19]</p>
</body>
</html>
```

## 参考資料

- [DAddYE/tilt\-mustache: Add Mustache to Tilt](https://github.com/daddye/tilt-mustache)
- [Sinatra: README \(Japanese\)](http://sinatrarb.com/intro-ja.html)
- [Ruby \+ Mustache で Hello World \- Qiita](https://qiita.com/niwasawa/items/5f0fe45a870dfb2f6f4e)
- [Ruby の tilt パッケージを使って ERB テンプレートを処理する \- Qiita](https://qiita.com/niwasawa/items/9cdc0d33020fb82ba1c8)
- [Ruby \+ Sinatra でテンプレートエンジンに Mustache \(mustache\-sinatra\) を使う \- Qiita](https://qiita.com/niwasawa/items/bb4d4e5eb4a737fd8950)