Diary?

2009-06-04
Thu

(02:05)

仕事で StringTemplate の使用を検討しているので、こいつの仕様とかを調査してた。まず最初に断っておくと、俺は StringTemplate の開発者 Terence Parr の意見である「テンプレートは表示だけしてりゃいいんだよ。ビジネスロジックを書けるテンプレートエンジンとか死ねよ(意訳)」には全面的に賛成であり、早く PHP とか滅べとか思うし、未だに JSP でスクリプトレット書いてる奴は解雇あるいは切腹に処すべきだと思うし、テンプレートエンジンにビジネスロジックを書けるだけの機能を詰め込んでる開発者にはいい加減にしろといいたい(俺は大学の頃からこういうことを主張してた気がするけど、あんまり受け入れられた事が無かった気がする)。

というわけで根本的な思想には全面的に賛同した上で、ちょっと StringTemplate のアレな部分を1ヶ所指摘。といっても人によっては許容範囲かもしれないし、実際俺も完全に否定する物でもないんだが。とりあえずお馴染みの Hello, World。

import stringtemplate3
hello = stringtemplate3.StringTemplate("Hello, $name$")
hello["name"] = "World."
print str(hello)

こいつを実行すると "Hello, World." と表示され、それはまあ想像通りだろう。それでは、次のコードはどうだろう。

import stringtemplate3
hello = stringtemplate3.StringTemplate("Hello, $name$")
hello["name"] = "World."
hello["name"] = " Fuck you."
print str(hello)

こいつを実行すると "Hello, World. Fuck you." と表示される。つまり同一のアイテムへの代入操作がリストへの追加になっているわけだが、オイコラ Terence Parr、 a = b のような代入をした後で a == b にならねえのはバグの温床だろ、何やってんだ。これって C# のプロパティとかでも似たようなことができるけど、俺はこういうのはどうかと思うぜ。もしも似たような事を俺が考えるなら、まず次のようなクラスを作り、それをテンプレートの各プレースホルダで扱われる値とする。

class TemplateValue(object):
    def __init__(self, initial_value):
        self.data = [initial_value]
    
    def __iadd__(self, value):
        self.data.append(value)
        return self.data

こいつの使い方はこんな感じ。

v = TemplateValue(1) # data == [1]
v += 2               # data ==[1, 2]

StringTemplate は __setitem__ を使って代入をリストへの追加にしてるんだけど、だったらこういうクラス使った方が良いと思うんだけどなあ。

とはいえ、ざっとドキュメントに目を通してテンプレートを書いたりした限りでは、他の不満点は繰り返しのキーワードが foreach とかそんぐらいだったんだが。致命的な弱点としてはデフォルトで HTML 特殊文字がエスケープされないことが発覚。これは StringTemplate があくまでも汎用的なテンプレートエンジンで、特に Web ページの出力に特化しているわけじゃないのが原因だろう。

追記:あれー、 foreach 構文が見つからないぞ!? 前にどっかで見つけたときはあったはずなんだけど、もしかして別のエンジンと混同してた?

追記2:違う、ある。ただ構文が超絶変態的なだけだった。 $val:{arg|value=$varg$}$ とか舐めてんのか。

最初は出力時のフィルターでどうにかならないかと思ったが、フィルターでは普通にテンプレートに書かれている文字列(=エスケープしちゃダメな文字列)も一緒くたに扱ってしまっているので、フィルターに渡る前の段階でどうにかしないとマズそうだ。ぶっちゃけ変数の内容を片っ端からエスケープすりゃいいと思うので、テンプレートに値を突っ込むところでどうにかすればいいだろう。というわけで自前のテンプレートクラスを StringTemplate クラスから派生させて作ろうかと思ったが、このテンプレートクラスは以下のような入力にも対応しないといけないらしい。

import stringtemplate3
hello = stringtemplate3.StringTemplate("Hello, $name$")
hello["name"] = ["World.", " Fuck you."]
print str(hello)

まだコードをきちんと追ってないので不明な部分があるが、結構複雑なテンプレートの構築にも対応しているようだ。そのため StringTemplate の該当部分のコードは思ってたより複雑で(といってもたかが知れてるが)、鼻クソほじりながら改造というわけには行かなさそう。まあこれは今日の午後辺りに手を付けようかな。

(18:37)

とりあえず StringTemplate についてさらに調査を進めたので、件の HTML エスケープ問題についての現時点での対処の方針などを書いておく。結論から書くと、 AttributeRenderer で どうにかなりそうではある。

まず最初に俺の考えたやり方というのは、 StringTemplate の Writer クラスを自前で作り(上に書いた中でフィルターと呼んでいる部分だ)、そこで HTML 特殊文字のエスケープを行うというものだ。その Writer のコードはこんな感じ(実装は適当)。

class HTMLWriter(stringtemplate3.NoIndentWriter):
    def write(self, string):
        value = string.replace('&', '&')\
            .replace('<', '&lt;')\
            .replace('>', '&gt;')\
            .replace("'", '&#39;')\
            .replace('"', '&quot;')
        self.out.write(value)
        return len(value)

ところがこれは上手く行かない。以下のコードを実行すると、残念な結果になってしまう。

html = stringtemplate3.StringTemplate("<p>$p$</p>")
html['p'] = '<>'
html.write(HTMLWriter(sys.stdout))

期待される出力は "<p><></p>" だが、実際の出力は "&lt;p&gt;&lt;&gt;&lt;/p&gt;" で、つまりテンプレート変数に埋め込まれる値もテンプレート本体の文字列も、一緒くたに全部エスケープされてしまう。

こういう時に使うのが AttributeRenderer で、例えば問答無用に HTML 特殊文字をエスケープする素朴な実装はこんな感じになる。

class HTMLRenderer(stringtemplate3.AttributeRenderer):
    def toString(self, o, formatName=None):
        value = o.replace('&', '&amp;')\
            .replace('<', '&lt;')\
            .replace('>', '&gt;')\
            .replace("'", '&#39;')\
            .replace('"', '&quot;')
        return value

使い方はこう。

html = stringtemplate3.StringTemplate("<p>$p$</p>")
html['p'] = "<>"
html.registerRenderer(unicode, HTMLRenderer())
print html

formatName 属性をテンプレートに指定することでも制御が可能で、例えば日付のフォーマットぐらいはテンプレート側で指定できてもバチは当たらないだろうから、次のようなテンプレートと Renderer を考えてみる(全力で手抜き)。

class DateRenderer(stringtemplate3.AttributeRenderer):
    def toString(self, o, format=None):
        return o.strftime(str(format))
        
tmpl = stringtemplate3.StringTemplate('date1: $d1;format="%Y/%m/%d"$\ndate2: $d2;format="%m/%d"$')
tmpl['d1'] = date(year=2009, month=6, day=4)
tmpl['d2'] = date(year=2009, month=6, day=4)
tmpl.registerRenderer(date, DateRenderer())
print tmpl

出力結果は次の通り。

date1: 2009/06/04
date2: 06/04

というわけで、檜山さんの懸念である「div要素内とpre要素内とscript要素内では、エスケープ方法や改行の扱いが微妙に違ったり」という部分については、おそらく上記の format を用いてテンプレート側で指示を出し、それに対して Renderer 側で処理を分けるという方法で対処可能では、というのが結論ですかね。

また Smarty の修飾子や Django のフィルタとの関連性について書くと、実はこの Renderer が修飾子/フィルターの役割を果たしているというのが StringTemplate のアーキテクチャであり、「Renderer と修飾子の実行順序」については、実は両者は同じ概念なので気にしなくて良いのでは、という事になります。

Creative Commons
この怪文書はクリエイティブ・コモンズ・ライセンスの元でライセンスされています。引用した文章など Kuwata Chikara に著作権のないものについては、それらの著作権保持者に帰属します。