複合的なKidテンプレート

このエントリは2007/10/06の再掲です

TurboGearsの標準テンプレートエンジン、Kidの説明です。Kidの基本構文は簡単なので一見初めてでもすぐできそうですが、いざtg- admin quickstartで作成されたwelcome.kidとmaster.kidを見てみると、本当に初めてでは、どういう理屈で組み合わされてページが作られているのかさっぱりわかりません。そこで、少し詳しくメモを残しました。

Kidテンプレートエンジンは、表面的には TapestoryのPython版です。が、実行イメージはJSPです。実行イメージでいえば、Javaサーブレットに対するJSPがそうであるように、Kidはテンプレート言語というよりも、むしろPythonコードの別の書き方だといえるでしょう。でもそれだけではありません。KidはもっともっとPython的です。つまり、どんなに固定的な設定でも、それらはPythonコードで記述されるのです。スクリプトレットだけが危険なネイティブコードで、他は安全なタグライブラリと式言語、というわけではありません。

部分をコードで置き換えるテンプレート言語と異なり、Kid はテンプレート言語で書かれたソース全体がプログラムコードになります。ページデザイナーは暗黙的にプログラムしているのです。なので、つねにスクリプトが逐次処理される動作イメージを持たなければなりません。じっさい、­Kidのコンパイル結果はpycになり、構文エラーは例外とスタックトレースを吐きます。

テンプレートファイルはひとつの関数に見立てることができます。Kidレンダラに与えられたマップオブジェクトは、その関数に与えられたキーワード引数として展開されます。関数内に定義された関数から、ローカル変数をレキシカルに参照できるように、Kidもまた、その内部のいかなる箇所からでも、もっとも外側の関数の引数を参照できます。

それとは別に、特別な変数があります。それが、今回の本題である、item変数です。

Kid が個々の要素(XMLでいう要素のこと、DOM系のAPIではElementクラスに相当)をレンダリングするとき、要素は一時的にitem変数に格納されます。もし、py:match属性を持つ要素(以下マッチング置換要素と呼ぶ)をひとつでも書いていると、ここで、すべてのマッチング置換要素について、繰り返しpy:match内の式が評価されます。評価対象は当然、「現在処理しようとしている要素」を指すitem変数です。

どんな式を書くとよいかはともかく、評価結果が真を返すと、本来その場に展開されるはずの要素であるitem変数はローカルスコープに保持されたままになり、入れ替わりで、マッチング置換要素のレンダリングが始まります。さて、マッチング置換要素をレンダリングするとき、item変数がスコープに保持されたままになっているということは…。

そう、マッチング置換要素の中で、このオリジナル要素構造をレンダリングすれば、オリジナル文書で割り込みをいっさい想定しなくても、より賢い他者がその外から、関心のある部分を探し出してくれ、元の処理を尊重したまま、任意の処理を飾り付けることができるのです。「賢い他者」が、凡庸なすべてのコードの面倒を見てくれて、横断的に関心事を挿入してくれる…、つまり、アスペクト指向です。マッチ条件はポイントカットの記述方法、マッチング置換要素の中身はアドバイスと考えると、py:matchをもっとも的確に理解できるでしょう。

では実験。単独でレンダリング可能なKidファイルでいいので、以下ように記述してレンダリングしてみましょう。

<div py:match="item.tag=='{http://www.w3.org/1999/xhtml}div' and item.attrib.get('align')=='left'" style="background-color:#f00;padding:4px;">
    <p>Hey! matching point was found.</p>
    <div style="background-color:#ffd;margin:4px;" py:content="item">&nbsp;</div>
    <p>is it you expected?</p>
</div>
<div id="cutpoint1" align="center" style="color:#f00;font-size:0.7em;">
	I am simple text node #1 not to be match
</div>
<div id="cutpoint2" align="left" style="color:#f00;font-size:0.7em;">
	I am simple text node #2 to be match
</div>

条件式の記述方法はともかく、とりあえず、似た2つの要素のうち、〜simple text node #2〜だけが、py:matchを持つ要素でデコレーションされました。マッチング置換要素の中では、content="item"とし、たしかに、 itemをレンダリングに使っています。

残念ながら、入門ドキュメントにはあまり詳しいitem変数(というよりElementクラス)の仕様がありません。次にとるべき、もっともPython的な行動は、「だいたいわかったら、dirで覗いて片っ端から使ってみる」ですね。

<div py:match="item.attrib.get('id')=='cutpoint'">
    <h1>item ${str(type(item))}</h1>
    <div style="background-color:#ffd">${item}</div>
    
    <h1>str(item)</h1>
    <pre style="background-color:#ffd">${str(item)}</pre>
    
    <h1>item.__dict__</h1>
    <pre style="background-color:#ffd">${", ".join(item.__dict__)}</pre>
    
    <h1>item.tag ${str(type(item.tag))}</h1>
    <pre style="background-color:#ffd">${item.tag}</pre>
    
    <h1>item.attrib ${str(type(item.attrib))}</h1>
    <pre style="background-color:#ffd">${["%s = %s\n" % (k,v) for k,v in item.attrib.items()]}</pre>
    
    <h1>item.items() ${str(type(item.items()))}</h1>
    <pre style="background-color:#ffd">${["%s = %s\n" % (k,v) for k,v in item.items()]}</pre>
    
    <h1>item.text ${str(type(item.text))}</h1>
    <div style="background-color:#ffd">${item.text}</div>
    
    <h1>len(item)</h1>
    <div style="background-color:#ffd">${len(item)}</div>
    
    <h1>item[:] ${str(type(item[:]))}</h1>
    <div style="background-color:#ffd">${item[:]}</div>
    
    <h1>[item.text] + item[:] ${str(type([item.text] + item[:]))}</h1>
    <div style="background-color:#ffd">${[item.text]+item[:]}</div>
</div>
<div id="cutpoint" align="center" style="color:#f00;font-size:0.7em;">
    I am simple text node #1
    <p>I am a text node within paragraph</p>
    I am simple text node #2 trailing br tag
    <p><b><i>I am a text node within bold italic</i></b></p>
    I am simple text node #3
</div>

片っ端から使いました(笑)。

itemはシーケンスに見えるオブジェクトで、かつ、マップに見えるオブジェクトでもあるようです。シーケンスとして見れば子要素の列で、マップとして見れば属性の辞書としてふるまいます。が、そのどちらからも、自分自身をあらわす十分な値は見えません。自分自身についての情報は、オブジェクトの属性にアクセスします。

使い勝手についてまとめると、こんな感じでしょうか。

  • item.attribはかなり使える(py:attrs="item.items()"という、item.attrib.items()のショートカットもある)
  • item.tagは{http://www.w3.org/1999­/xhtml}div のようなQNameなので注意
  • item変数をそのままKidに流すと、元の要素を完全再現する
  • [item.text] + item[:]は、元の要素の中身だけを再現する

これだけわかれば、十分に使えるんじゃないでしょうか?ちょっと変わっているのは、item.textです。Kidの基本は要素のツリーで、ノードのツリーではありません。なので、最初の子要素までの間に挟まっているテキストノードは、その要素のオマケとしてtext属性に入るようです。W3C的じゃなくて素敵。

さて、アスペクト指向できることはわかったものの、隣り合った場所にアスペクトを置いても仕方ありません。どうにか、実装コードとアスペクトを分離できないものでしょうか?それには、折り込みする者とされる者の関係の方向によって、2種類の選択肢が準備されています。一方はpy:extends、もう一方は py:layoutです。

py:extendsを要素に付け加えると、指定したファイルに入っているマッチング置換要素が、自分のドキュメントにアスペクトを折り込んでくれます。「CSSJavaScriptのリンクを、必要に応じて要るものだけ入れてくれる」なんていうアスペクトは、まさに横断的関心事ですね。ちなみにpy:extendsには、複数のファイルを指定することができます。

py:layoutはその逆で、指定した基本ファイルに対して、自分のほうからマッチング置換要素を用いてアスペクトを折り込み、その基本ファイルのドキュメントを操作して結果を得る方法です。レイアウトデザインされた静的なHTMLに、外から動的ページ要素を入れ込むことが可能です。

たしかに、py:matchは少し高度ですが、ちょっとその高度さを乗りこなせさえすれば、他のすべての仕事が、普通よりもぐんと楽になります。

いやぁ、やっぱりPython文化は、専門のナントカ技術を勉強した人のものではなく、ハッカーと普通の人に嬉しい文化ですね。そりゃあ、GoogleがサラリーマンPGを排除するのに使うわけだ(笑)。←ジョークですよ、ジョーク