2006年09月26日

JSONPなどでデータを取得して、HTML中の任意なAttributeにマクロを埋むタイプのテンプレートキットを作りました。
別にJSONPじゃなくても、staticもデータ構造を定義する事も出来るし、Ajaxサポート書けばAjax経由でデータを取得できます。
マクロ展開はDOM探索で色々処理をしています。 Model = JSONP 、 View = DOM って感じかも。

他にもJKL.Hinaや、JSmartyなどがあります、大きな違いは専用構文を利用しないでテンプレート展開が出来たりと、かなりシンプルです。
他の特徴は
・DOM操作のみでテンプレート展開
・JSONP対応
・テンプレート用のデータ領域を用意する事無く、HTMLに直接テンプレートを記入出来る
・データ展開先を設定不要。class要素などからテンプレートエンジンが、自動的に展開先を選択する
・DOM操作を基本としてる為、if/foreach等のスコープが実際のHTML構造と連動していて直感的
・DOM要素の属性を柔軟にカスタマイズできる
・フィルタチェイン
・フィルタの拡張が可能
などなどです。

デモ用のサンプルはhttp://tech.yappo.jp/demo/simplemacro/
tracはhttp://trac.yappo.jp/trac/browser/javascript/cjtk/trunk
svnは

svn co http://svn.yappo.jp/repos/public/javascript/cjtk/trunk

チュートリアル


simple injection

マクロと言っても、簡単に使う分には専用の言語らしき物は見えません。
たとえばdel.icio.usのJSONPだと(http://del.icio.us/feeds/json/yappo?callback=delisious.callback)

new Array(
{
u: url,
d: タイトル,
n: コメント,
t: new Array(tag1, tag2)
},
{
u: url,
d: タイトル,
n: コメント,
t: new Array(tag1, tag2)
}
);
の様な構造になっているので、これを簡単に展開したい場合には
<javascript>
var config = {
classname: 'delicious', // class属性がdeliciousのDOM要素をテンプレートとして使用する
foreach: true, // 上記で取得したDOM要素を元にして、JSONPで取得じた全レコード分を展開
url: 'http://del.icio.us/feeds/json/yappo?callback=delisious.callback', // JSONPのURL
};
var delisious = new Cjtk(config);
delisious.process();
</javascript>
<div class="delicious">
<div class="u">URL:</div>
<div class="d">タイトル:</div>

</div>

とかけば、「URL:」や「タイトル:」の後に、それぞれURLとタイトルが挿入されます。
と、単純なデータ/展開方法であれば非常に完結にテンプレートを組めます。

attribute injection


さて、単純なのは良いのですが、これではリンクを張ることが出来ません。
そこで、マクロ構文が出て来ます。
リンクを張るためには、HTMLを次のように変更します。
<div class="delicious">
<div>URL:<a class="[% $u | attr 'set' 'href' %][% $u | inner %]</a></div>
<div>タイトル:<a class="[% $u | attr 'set' 'href' %][% $d | inner %]</a></div>

</div>

これで、URLとタイトルそれぞれにURLへのリンクが張られます。
このマクロの構文は微妙にTTっぽい気もしますが、UNIXのpipeの概念の方が近いです。
たとえば
<div class="[% 'D' | $d %][% 'A' | cat 'B' 'C' | cat $d | inner %]"></div>
なんて事が書けますが、これを分りやすくコメントを付けると
<div class="[% 
'D' | $d # $dという変数に文字列'D'をセット
%][%
'A' | # 文字列'A'を次の処理へ転送
cat 'B' 'C' | # 文字列'A', 'B', 'C'を順番に結合して、文字列'ABC'を作成して次の処理へ転送
cat $d | # 文字列'ABC'と変数$dの中の文字列'D'を結合して、文字列'ABCD'を作成して次の処理へ転送
inner # 転送されて来た文字列'ABCD'を、現在のDOM要素の子TextNodeとして追加する
%]"></div>
と、こんな感じになります(実際は#のコメントは書けません)。

if / unless


次に条件によって要素を表示したく無い場合があるでしょう。
たとえばdel.icio.usのコメント何かがそれにあたります。
そんな場合はifやunlessが使えます。
<div class="delicious">
<div class="u">URL:</div>

<div class="d">タイトル:</div>
<div class="[% $n | if %]"><div class="n">コメント:</div></div>
</div>

ifはパイプの前の処理結果があれば(もしくは0でなければ)子要素を表示します。
unlessは逆の条件です。
もしifの条件に一致すれば
<div class="delicious">
<div class="u">URL:</div>

<div class="d">タイトル:</div>
<div class="n">コメント:</div>
</div>

と表示され、一致しなければ
<div class="delicious">
<div class="u">URL:</div>

<div class="d">タイトル:</div>
</div>

となります、defaultではif/unless文のあるDOM要素は削除されます。
もし削除したくない場合にはBKっぽく
<div class="delicious">
<div class="u">URL:</div>
<div class="d">タイトル:</div>

<div class="[% $n | if '1' %][% $n | inner %]">コメント:</div>
</div>

と書くことも可能です。
ifのあとに1を加える事で、ifがあるDOM要素を削除しません。

foreach


del.icio.usの場合はtag要素が配列として渡されます。
配列要素を展開する場合にはforeachが使えます。
<div class="delicious">
<div class="u">URL:</div>

<div class="d">タイトル:</div>
<ul class="[% $t | foreach 'tag' '1' %]">
<li><a href="http://del.icio.us/tag/" class="[% $tag.v | attr 'append' 'href' %][% $tag.v | inner %]"></a></li>
</ul>

</div>

ulというDOM要素を削除しないままtagの数だけliタグを展開します。
attr 'append'することで、aタグ中のhref="http://del.icio.us/tag/"を消さずに、http://del.icio.us/tag/tagの様なリンクを張るようになってます。

応用


レコードIDが確定してる場合

次のようなレコードのIDが固定的なデータの場合にはforeachオプションでテンプレート展開をせずに、レコードIDを指定したテンプレート構築が出来ます。
レコードIDはdefaultではtitle属性に入れます。
例えば

var json = {
hatena: {
url: 'http://www.hatena.ne.jp/',
cto: 'naoya'
},
mixi: {
url: 'http://www.mixi.co.jp/',
cto: 'Boofy'
}
};
なデータの時には
<javascript>
var config = {
classname: 'mixna', // class属性がmixnaのDOM要素をテンプレートとして使用する
params: json // 入力データをJSONをやめてローカルなデータを使う
};
var c = new Cjtk(config);
c.process();
</javascript>
<div class="mixna" title="hatena">

はてな
<div class="url"></div>
<div class="cto"></div>
</div>
<div class="mixna" title="mixi">
みくしい
<div class="url"></div>

<div class="cto"></div>
</div>

という記述が可能です。

ちなみにこの例ではJSONPを使っていませんが、JSONPを使用する場合には該当するDOMのtitleからレコードIDを全てかき集めてJSONPでリクエストする時にクエリパラメータとして送信します。
今回の例だと

json.js?id=hatena;mixi
といった形でクエリを自動的に構成する事が出来ます。
defaultのセパレータは;ですが変更出来ます。

構文拡張

プラグイン機構を使って、マクロを追加出来ます。

Cjtk.register_function(拡張名, function(context, obj, func, stdin, args){});
と拡張を登録する事によって
[% 拡張名.マクロ名 %]
という風に追加したマクロが使えます。
基本のマクロも、これを使って実装しています。

マクロシンタックス

全てのマクロは|(pipe)で処理をつなげる事が出来ます。
マクロの戻り値が次のマクロの入力に使われます。
例外としてマクロの最後に$valueのような変数が指定されていた場合は、変数に文字列が代入されます。

[% 'test' | lc | replace 'T' 'P' | $value %]
だったら、$valueに'PESP'が入ります。

if

分岐条件です。

if 

直前のマクロの結果が真なら、現在のDOM要素以下を表示し、偽なら現在のDOM要素以下を削除します。
偽の場合は、if文以降のマクロは処理しません。
[% '0' | if %] # 条件成立せず
[% '' | if %] # 条件成立せず
[% 'a' | if %] # 条件成立
[% '' | if | cat 'a' | inner %] # cat以降は処理しない
[% '' | if %][% 'test' | attr 'set' 'title' %] # 'test'以降は処理しない
if文に引数を与えると、if文があるDOM要素の削除を行いません。
<div class="[% '1' | if %]">test</div>

これが
test
となるところを
<div class="[% '1' | if '1' %]">test</div>

<div>test</div>
となる

unless

ifの真偽の条件が逆になっただけです。


foreach

入力で与えられた配列データをforeach展開します。

foreach 展開後の変数名 
flagを与えるとifの時のように、自DOM要素の削除を行いません。

inner

子DOM要素にTextノードを作成して、入力のあった文字列を挿入します。

inner <'set|append'>
setを指定すると、子要素全てを削除してから挿入を行います。

attr

自DOM要素の指定した属性を操作します。

attr 'get|set|append' '属性名'
getを指定すると指定した属生の値を取得します。
setを指定すると、入力した値で上書きします。
appendだと追記します。


cat

文字列を結合します。
joinのような感じです。
入力値があれば入力値を第一引数としてあつかいます。

lc

lcです。

uc

ucです。

replace

javascriptのreplaceです。

ret.stdout = typeof stdin == 'string' ? stdin.replace(args<0], args[1>) : '';
こんな実装。


html

htmlエスケープさせるフィルタ。

nop

何もしません。
if文の後にnopを置くと、次のマクロにif文の出力を引き継がないのでcatつか使えるようになります。

コンフィグ

new Cjtk(config);した時に指定出来る設定は次の通り

var config= {
type: 'text/javascript', # JSONP利用時のscriptタグで仕様
charset: 'utf-8', # JSONP利用時のscriptタグで仕様 JSONPのcharsetを入れる
separator: ';', # foreach展開をしない時にレコードIDをクエリパラメータに含める際のセパレータ
foreach: false, # foreach展開をするか
ajax: false, # ajax利用するか(ajaxのコードは未実装)
start_tag: '[%', # マクロの開始タグ
end_tag: '%]', # マクロの終了タグ
macro_attribute: 'class', # マクロを入れる属性名
rowid_attribute: 'title', # レコードIDを入れる属性名
params: '', # JSONPなどを利用しない場合は、ここにデータを指定する
process_callback: function(context){} # テンプレート展開が終わった時に呼び出される関数
};


いかがでしょう?

Posted by Yappo at 2006年09月26日 13:28 | TrackBack | tech
Comments

こちらも勉強させていただきます。
本当にありがとうございます。
Yatena

Posted by: Yatena at 2006年09月28日 22:41
Post a comment









Remember personal info?






コメントを投稿する前に↓の場所にnospamと入力してください。