JekyllのテンプレートのListとHashで結構いけるんじゃないかな


[Jekyll](http://jekyllrb.com)のいいところは、Front-matter（テンプレートの先頭から---で区切られたヘッダ部）にListとHashが使えるので、かなりプログラマ好みの書き方ができるところです。

テンプレートエンジンの[Liquid](http://docs.shopify.com/themes/liquid-basics)の記述方法と合わせて、試行錯誤した結果をメモしておきます。

※ Front-matterはYAMLフォーマットです。YAMLについては[プログラマーのためのYAML入門(初級編)](http://magazine.rubyist.net/?0009-YAML)が詳しいです。

JekyllはVersion1.3.0を前提としています。

##Front-matterにListとHash構造を定義する

まず、Front-matter及びbody部をこのように定義してみます。

```template1.html
---
title: template1
layout: default
hash1:
  name: Hash構造
  value: 値が入る
list2: 
  - 一番目
  - 二番目
---
{{ page.hash1.name }}<br />
{{ page.hash1.value }}<br />
<br />
{% for data in page.list2 }}
{{ data }}<br />
{% endfor %}
```
結果はこうなります。
> Hash構造
> 値が入る
>
> 一番目
> 二番目

ポイントは、記述した定義は全て"page"というhashに格納されるということです。

記述の注意として、hash1,list2の次の行はインデント（スペース１つ以上）されていないと、ListやHashと見なされません。

ここまでは基本ですが、ListのHashとかHashのListになると表現力が増します。

```template2.html
---
title: template2
layout: default
hash3:
  name: 
  - 三番目
  - 四番目
  value: 値が入る
list4: 
  - list: [11,12, 13 ,"文字も入るよ"] 
  - list: [22, 23 ,24,スペース も入るよ]
---
{{ page.hash3.name }}<br />
{{ page.hash3.value }}<br />
<br />
{% for data in page.list4 }}
{% for item in data.list }}
{{ data }}<br />
{% endfor %}
{% endfor %}
```

結果はこうなります。
> 三番目四番目
> 値が入る
>
> 11
> 12
> 13
> 文字も入るよ
> 21
> 22
> 23
> スペース も入るよ

Listの記述方法として、インデントして頭に"-"をつける方法と、

hash3.nameはList構造ですが、List全体を指定するとList内を連結して文字列として扱います。

記述の注意として、hash3の後のリストはインデントが一つ前の行より手前の位置にあるとエラーになります。

文字列は""で括ったり括らなかったりでしたが、文字列の並びによってはうまく処理されないことがあります。その場合、カンマの前後にスペースを空けるとうまくいくようです。

##include先にListとHashを渡す

Jekyllのincludeタグは、include先にパラメータを渡すことができます。include先に渡したタグは、全て"include"という名前のhashに格納されます。

```template3.html
---
title: template3
layout: default
hash1:
  name: Hash構造
  value: 値が入る
list2: 
  - 一番目
  - 二番目
---
{% include include1.liquid param1=page.hash1 param2=page.list2 %}
```
```_include/include1.liquid
{{ include.param1.name }}<br />
{{ include.param1.value }}<br />
<br />
{% for data in include.param2 %}
{{ data }}<br />
{% endfor %}
```

結果はtemplate1.htmlと同じになります。

##include先に動的に作ったListを渡す

include先にFront-matterで作成したListやHashを渡せることはわかりました。ただ、残念な事に動的にテンプレート内でListを作って渡すということができません。

```template4.html
---
title: template4
layout: default
hash1:
  name: Hash構造
  value: 値が入る
hash2:
  name: Hash構造2
  value: 値が入る2
---
{% include include2.liquid param=[page.hash1, page.hash2] %} 
```
```_include/include2.liquid
{% for data in include.param %}
{{ data.name }}<br />
{{ data.value }}<br />
{% endfor %}
```
これはjekyllのレンダリング処理でエラーとなります。

jekyllにデフォルトで組み込まれている、include.rbは自前でincludeタグのパラメータを分解しているのですが、Listを受け入れることを想定していないのでした。

あちこち探したところ、splitを使ってListを作るという提案がありました。

```template5.html
---
title: template5
layout: default
hash1:
  name: Hash構造
  value: 値が入る
hash2:
  name: Hash構造2
  value: 値が入る2
---
{% assign list = "page.hash1|page.hash2" | split: "|" %}
{% include include2.liquid param=list %} 
```
結果は、何も表示されません。
Listに入っているのは文字列"page.hash1"と"page.hash2"でした。文字列をデータ構造に置き換えるには、splitだけでは無理なようです。

##include先に動的に作ったListを渡す（その２）

splitでListにするところまではよかったのですが、もう一工夫必要なようです。

List内の文字列をキーとして、Jekyll内部からデータを拾ってくればできそうですので、プラグインを作りました。


```_plugins/contenxtarray.rb
module Jekyll
  module ContextArray

    def contextarray(input)
      if !input.kind_of?(Array)
        src = input  
        input = [src]
      end
      data = []
      input.each {|a|
        data.push(@context[a])
      }
      data
    end

  end
end

Liquid::Template.register_filter(Jekyll::ContextArray)
```
このフィルターをこのように使います。

```template6.html
---
title: template6
layout: default
hash1:
  name: Hash構造
  value: 値が入る
hash2:
  name: Hash構造2
  value: 値が入る2
---
{% assign list = "page.hash1|page.hash2" | split: "|" | contextarray %}
{% include include2.liquid param=list %} 
```
ようやく結果が出力されました。

> Hash構造
> 値が入る
> Hash構造2
> 値が入る2


##include先に動的に作ったListを渡す（その３）

その２までで、目的は果たした気もするのですが、なんとなく直感的ではありません。気持ちよくコーディングをするためには不便を減らしていくのがプログラマの矜持ではないでしょうか。

ということで、なんとなく難しそうな本丸、include.rbを攻略することにしました。

※　Jekyllのバージョンによってinclude.rbの内容が異なりました。ここでは1.3.0版を修正しています。（1.1.2版と1.3.0版で確認）

実際に試しただけなので正確な挙動は確かめて頂きたいところですが、gems上のプラグインは、_pluginsに置いてあるプラグインで上書きされるようです。ここでは、_pluginsディレクトリに、修正したinclude.rbを配置します。

全部書くと長くなるので修正点だけ抜粋しています。

```_plugins/include.rb_modified_VALID_SYNTAX
VALID_SYNTAX = /([\w-]+)\s*=\s*(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|([\w\.-]+)|\[([^\[\]]+)\])/
```
```_plugins/include.rb_modified_parse_params
      def parse_params(context)
        params = {}
        markup = @params

        while match = VALID_SYNTAX.match(markup) do
          markup = markup[match.end(0)..-1]

          value = if match[2]
            match[2].gsub(/\\"/, '"')
          elsif match[3]
            match[3].gsub(/\\'/, "'")
          elsif match[4]
            context[match[4]]
          elsif match[5]
            array = []
            match[5].split(/\s*,\s*/).each {|i|
              avalue = if md = i.match(/^(?:"([^"\\]*(?:\\.[^"\\]*)*)")$/)
                md[1].gsub(/\\"/, '"')
              elsif md = i.match(/^(?:'([^'\\]*(?:\\.[^'\\]*)*)')$/)
                md[1].gsub(/\\'/, "'")
              elsif context.has_key?(i)
                context[i]
              else
                i
              end
              array.push(avalue)
            }
            array
          end

          params[match[1]] = value
        end
        params
      end
```
修正されたinclude.rbを使うと、template4.htmlがきちんと動くようになります。

##include先に動的に作ったListを渡す（おまけ）

構造をそのままinclude先に渡せるようになったので、こんなことができます。

```template7.html
---
title: template7
layout: default
item1:
 name: 日付
 value: [11/11, 11/11, 11/13, 11/14]
item2:
 name: 用途
 value: [弁当, ガンプラ, 映画, 漫画]
item3:
 name: 金額
 value: [300, 4300, 1800, 680]
 numfilter: 1
---
{% include table.liquid item=[page.item1, page.item2, page.item3] %}
```
```_include/table.liquid
{% capture counti %}{{ include.item[0].value | size | minus:'1' }}{% endcapture %}
{% capture countj %}{{ include.item | size | minus:'1' }}{% endcapture %}
<table>

<tr>
{% for itemdata in include.item %}
<th>{{ itemdata.name }}</th>
{% endfor %}
</tr>

{% for i in (0..counti) %}
<tr>
{% for j in (0..countj) %}
{% if include.item[j].numfilter %}
<td>{{ include.item[j].value[i] | numberformat }}</td>
{% else %}
<td>{{ include.item[j].value[i] }}</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}

</table>
```
```_plugins/numberformat.rb
module Jekyll
  module NumberFormat

    def numberformat(number)
      number.to_s.reverse.gsub( /(\d{3})(?=\d)/, '\1,' ).reverse
    end

  end
end

Liquid::Template.register_filter(Jekyll::NumberFormat)
```

結果はこうなります。テンプレートエンジンだけで、シンプルなデータ構造から複雑な表現をできることがわかります。

| 日付 | 用途 | 金額 |
|:----------|:----------|--------:|
| 11/11  | 弁当       |    300  |
| 11/11  | ガンプラ |  4,300 |
| 11/13  |  映画     |   1,800 |
| 11/14  | 漫画      |    680  |

##最後に

Jekyllでちょっと凝った事をしようとしてはまった人の参考になれば幸いです。