MTの管理画面でMT標準のJSテンプレートエンジンを使う
管理画面のカスタマイズをしていて、JSでDOMを弄ってHTMLを出力しようと思った時に、コードの見通しを確保するためにJSテンプレートエンジンが欲しいなと思いました。
ただ、そのためにライブラリ読み込むのも微妙だし、正規表現で頑張るかと思ったところ、管理画面でJSテンプレートエンジンが使われてたことを思い出しました。
MTの管理画面で使われている、JSテンプレートエンジンのライブラリは、/path/to/mt-static/js/common/Template.js
にあって、実装を確認したところ、シンプルで汎用的に使える小さなライブラリっぽいので、下記の通り機能をまとめました。
ちなみにこのライブラリは、数年前から更新が止まっていて、Github リポジトリの当該ファイルのコミット履歴も、「Common JavaScript ライブラリをリポジトリに入れました」的なログが一つあるだけのレベルです。
メンテが止まっていて、いつ役目を終えるかわからない感じの存在感なので、その点はご留意ください。
この記事の賞味期限もそれまでです。
使い方
下記のような感じで使えます。
var tmpl = new Template('Hello, [#= name #]!'); var context = new Template.Context({ name: 'taiju' }); tmpl.process(context); // => "Hello, taiju!" /* 下記で使う変数tmpl_textには下記のテンプレート文字列が保存されていると仮定する <ul> [# jQuery.each(vars, function (i, v) { #] <li>[#|h|u v#]</li> [# }) #] </ul> */ tmpl.compile(tmpl_text); context.vars = { vars: ["<script>alert('foo');</script>", "バー", "baz"] }; tmpl.process(context); /* => "<ul> <li><script>alert('foo');<%2Fscript></li> <li>%E3%83%90%E3%83%BC</li> <li>baz</li> </ul>"*/
Underscore.jsのテンプレート機能のように、普通に言語の機能がそのまま使えるタイプのシンプルなテンプレートエンジンです。この手のテンプレートは学習コストが低く、言語のパワーがそのまま使えるので、個人的に好むタイプです。
管理画面はjQueryがロードされているので、jQueryのユーティリティを組み合わせてテンプレートを書くのが良いかもしれません。
ライブラリの機能
Template.jsによって、Template, Template.Context, Template.Filterというクラスが提供されます。
Template.jsは、Core.jsに依存しているので、利用するにはCore.js等の依存ライブラリがあらかじめ読み込まれている必要があります。
主に使うのはTemplateクラスになりそうですが、先のサンプルのようにTemplateクラスのインスタンスを作って処理する他、Templateクラスのクラスメソッドを使って、JSテンプレートをビルドすることもできます。
var templates = { foo: '[#= foo #]' }; var vars = { foo: 'FOO' }; Template.process('foo', vars, templates); // => "FOO"
クラスメソッドの方が手軽に使えるかもしれません。
テンプレートの記法
[#
から#]
で囲まれた部分が JS テンプレートの記述部分になります。
JavaScript の任意の式や文が記述でき、式の結果を出力したり、出力結果にフィルタをかけたりできます。また、Template.ContextのインスタンスをTemplateクラスのインスタンス(compile済み)の持つ、processメソッドに渡すことで、テンプレートに任意の変数を渡すことができます。
[#
と#]
というテンプレートの開始位置、終了位置を指定するための文字列は、TemplateクラスのインスタンスのbeginToken
プロパティとendToken
プロパティを変更することで変更可能です。
頻繁に使うことになるのは、[# ... #] と [#= ... #] ですかね。テンプレートエンジンでよく見かける形式だと思います。
テンプレートのサンプル
[# ... #]
[# if (false) { #] ここは出力されない [# } #]
[#
から#]
で囲まれた範囲では、任意のJavaScriptの式や文を評価することができます。評価内容は出力されません。
[#-- ... --#]
[#-- 下記でゴニョゴニョする --#] ゴニョゴニョ
[#--
から--#]
で囲まれた範囲はコメントになります。コメント中の式や文は実行されず、出力もされません。
[#= ... #]
[#= true ? 1 : 0 #]
[#=
から#]
で囲まれた範囲は、任意のJavaScriptの式を記述でき、評価された値が出力されます。
[#* ... #]
[#= foo #]
[#* return #]
[#= baz #]<!-- この変数の値は出力されない -->
[#*
から#]
で囲まれた範囲は、定義された特定のコマンドを記述できます。
とは言え、実際には、returnしか定義されておらず、returnコマンドを記述すると、その記述以降のテンプレートは評価されません。
[#| ... #]
[#|i "foo ${bar} baz" #]<!-- ${foo} や $foo という記述が変数展開される --> [#|h "<script>alert(1);</script>" #]<!-- HTMLをエスケープする --> [#|H "<script>alert(1);</script>" #]<!-- HTMLをアンエスケープする --> [#|u "テスト" #]<!-- URLエンコードする --> [#|U "%E3%83%86%E3%82%B9%E3%83%88" #]<!-- URLデコードする --> [#|lc "ABC" #]<!-- 小文字にする --> [#|uc "abc" #]<!-- 大文字にする --> [#|substr(0, 3) "abcdefg" #]<!-- 指定した位置から指定した位置まで文字を切り出す --> [#|ws " foo " #]<!-- 前後の空白を除去する --> [#|trim(3) "abcdefg" #]<!-- 引数で指定した文字以降を…(U+2026)で省略する --> [#|date "2014-06-05T00:00:00+09:00" #]<!-- ISO形式の日付文字列をISO形式の年月日文字列に変換する --> [#|localeDate "2014-06-05T00:00:00+09:00" #]<!-- ISO形式の日付文字列をロケールに合わせた表記に変換する --> [#|rt "<script>alert(1);</script>" #]<!-- タグを削除する(内容は残す) --> [#|rp(/foo/g,"bar") "foobarfoobar" #]<!-- 引数に指定した文字を引数に指定した文字に置換する -->
[#|
から#]
で囲まれた範囲は、評価された式の値に、定義されたフィルタを適用した上で値を出力します。
上記の中でも、iフィルタや、dateフィルタは、内部で使っている関数がCore.jsに依存しています。
MTのJSライブラリあまり調べたことなかったので意識していなかったんですが、結構グローバルオブジェクトを拡張してるようです。
rpフィルタはうまく動いていなかったので、PRしました。
また、フィルタはUnixのパイプのような感じで、複数組み合わせることができます。
[#|ws|rt|uc " <span>abcd</span> " #]<!-- 前後の空白を除去してから、タグを除去して、内容を大文字にする -->
Template.Context, Template.Filter について
Template.Contextには、いろいろメソッドが用意されていますが、Templateクラスを通して透過的に利用することがほとんどな気がしたので、紹介は割愛します。フィルタに関しても、基本はテンプレート内に記述するケースがほとんどでしょう。
Template.Contextのincludeメソッドは単体でも使うかもしれません。
var templates = { header: 'Header!!', body: '[#= context.include("header") #] Body!! [#= context.include("footer") #]', footer: 'Footer!!' }; Template.process('body', {}, templates); // => "Header!! Body!! Footer!!"
まとめ
よく使う機能に関しては、上記で紹介した通りです。あとはソース読んで確認してください。
昔はいろいろとMT標準のJSライブラリについて、ナレッジが共有されていたのかもしれませんが、今は、ググってもあまり情報が見つからない感じがします。
Template.jsに限らず汎用的に使えそうなライブラリもありそうですが、ドキュメントないし、自分のような新参者はソース読むしかない感じです。公式ドキュメントが待たれるところ。
Template.jsは、汎用的に使えるテンプレートエンジンだと思うので、MTの管理画面のカスタマイズ用途等で、特に他のライブラリを読み込む予定が他にない時は、このJSテンプレートエンジンを使おうかなと思ってます。
ほなの。
PSGI + SeleniumでMTの管理画面のテストをする
MTの管理画面は、JavaScriptに依存した部分が結構多く、PhantomJS等のヘッドレスブラウザを使ったテストが必要になるケースがあります。
CasperJSや、Seleniumなど、いろいろなツールがありますが、MTの管理画面のロジックは、Perlで書かれているので、Perlで書けるのがベストです。Perlでデータの初期化とかしたいですからね。
Perlで書ける方法でツールを絞り込むと、現状では、Selenium::Remote::Driverを使うのが良い気がしています。
ということで、下記のようなテストを書いてみました。
テストの実行には、MTのGitHubリポジトリに含まれる、tディレクトリが必要です。あと、試す場合は環境変数MT_CONFIGを書き換えずに、MT::TestでDBを初期化すると、既存データが吹っ飛ぶので、テスト用の環境は別途用意した方が良いです。
Test::TCPで、GhostDriverと、MTのPSGIの管理画面を空いてるポートで起動して、Selenium::Remote::Driverを使って、管理画面を操作する感じです。
実際書いてみると、いろいろハマりどころがあった感じですが、ハマりどころを回避しつつテストを書くことになりそうです。
テストと同時にキャプチャを撮れるので、エビデンス対策も取れてよいですね。上記のコードではダッシュボードと、記事作成後の画面をキャプチャしていて、下記のような感じのキャプチャが撮れました。JavaScriptも処理されていて、ちゃんとした見た目になってます。
もう少し便利メソッドをカジュアルに使いたい場合は、Wight を使うのも良い気がします。
他言語でもSelenium使う機会があるかもしれないので、今後の学習コスト削減のために、今のところは単純なアダプタであるSelenium::Remote::Driverを使おうと思います。
ほなの。
MTオブジェクトの複製を作るツールスクリプトを作った
Movable Type使ってる時に、たまに、ページングのテストや負荷テスト等をしたい時に、記事を大量にコピーしたい時があって、これまで下記のようなスクリプトを叩いたりしてコピーを作ってました。
これをMTオブジェクト全般に広げても良いかなと思ったので、ObjectCloneというプラグインを作りました。
ObjectClone プラグイン
usage のままですが、下記のように使います。
USAGE: perl tools/object-clone --model=NAME --orig_id=NUM [OPTION] Requires: -m, --model=NAME Model name (object datasource) of object to make clone. -o, --orig_id=NUM Original object id clone object. Options: -a, --amount=NUM Amount of clones to make. Default 1. -d, --debug Output debug info to STDERR. -h, --help Show help. -r, --redefine=PAIR Redefine column value with column key and column value pair. -u, --usage Show usage. Examples: # Make clones of 100 objects of MT::Entry from original entry that entry_id is 1. $ tools/object-clone --model=entry --orig_id=1 --amount=100 # Make a clone of MT::Blog, and redefine name, site_path and site_url. $ tools/object-clone -m blog -o 2 -r 'name=Clone Blog' -r 'site_path=/path/to/site-path' -r 'site_url=/::/clone-blog/'
基本は、モデル名とコピー元のオブジェクトのIDを、それぞれ、--model
オプションと--orig_id
オプションで指定してクローンを作ります。
クローンを複数作る場合は、作る数を--amount
オプションで指定します。
他には、単に丸々コピーするだけではなくて、一部、値を変更する場合は、--redefine
オプションで、フィールド名とその値を=
で区切って指定します。
具体的には、Aというブログの記事を、Bというブログにコピーしたいような時などに、下記のように指定します。
$ perl tools/object-clone --model entry --orig_id 1 --amount 10 --redefine blog_id=3
コマンドラインオプションの単語が適切かどうかは微妙な感じもしますが、とりあえず必要最低限なオプションだけ用意しました。
値の変更が必要なケース
DB上の制約はないにも関わらず、実際にはMT::Entryのベースネームだとか、CustomFields::Fieldのタグ名は、値が重複しないようにCMS上で制限がかかっていたり、バックグラウンドでの値の生成の実装があったりします。
その辺の対応はオブジェクト毎に必要な感じなので、ObjectClone::Patcherとそのサブクラスに切り出しています。
単体で使うことはほぼないと思いますが、下記のような感じで使います。
use strict; use warnings; use lib qw(lib extlib plugins/ObjectClone/lib); use MT; use ObjectClone::Patcher; use ObjectClone::Patcher::MT::Entry; my $mt = MT->instance; my $orig_obj = MT::Entry->load(1) or die MT::Entry->errstr; my $new_obj = $orig_obj->clone; my $patcher = ObjectClone::Patcher->model('entry')->new($new_obj, $orig_obj); $patcher->remove_patch(basename => \&ObjectClone::Patcher::MT::Entry::basename) ->add_patch(basename => sub { 'new-basename' }) ->add_patch(convert_breaks => sub { 'markdown' }) ->apply_patch; $new_obj->save or die $new_obj->errstr;
デフォルトで下記のPatcherクラスを用意しています。
- ObjectClone::Patcher::MT::Entry
- ObjectClone::Patcher::MT::Category
- ObjectClone::Patcher::MT::Author
- ObjectClone::Patcher::CustomFields::Field
とは言っても実際に実装があるのは、ベースネームをユニークにする処理くらいです。
独自オブジェクトをtools/clone-objectで生成したいときなどは、上記のPatcherクラスを追加すれば対応ができます。( ObjectClone::Patcher::{オブジェクト名} )
後は、apply_patchメソッドで、パッチを適用する前に、before_apply_patchというコールバックが呼ばれるので、デフォルトのパッチ処理を無効にしたり、それに加えて何かの処理を実行したりする時は、コールバックプラグインで拡張できます。
コールバックプラグインのサンプル
下記のようにコールバックを使ったプラグインが書けます。
id: ObjectCloneCallback key: object-clone-callback name: ObjectCloneCallback callbacks: ObjectClone::Patcher::MT::Entry::before_apply_patch: | sub { my ($cb, $patcher) = @_; $patcher->add_patch('basename', sub { my ($new_obj, $orig_obj) = @_; 'new-basename'; }); }
まとめ
主に開発ツールとして作ったので、開発時には役に立つ気がしています。
シェルスクリプトと組み合わせたりするのも良いかもしれません。
ほなの。
MT::Object をワンライナーしやすくする拡張書いた
コマンドラインで、MT::Objectを取得して、加工して、出力してみたいなことすることが結構あるんですけど、MT::Objectは大変ワンライナーしにくい印象でした。
MTのシステム管理者のIDとパスワードがわからない時、コマンドで強制的に作ることがあるんですけど、MT::Objectがワンライナーしにくいので、下記のようなファイルを用意してから、実行するというまどろっこしさです。
試しにワンライナーしてみると下記のような感じでしょう。
$ perl -I{lib,extlib} -MMT -e 'my $taiju = MT->instance->model("author")->get_by_key({name=>"taiju"});$taiju->nickname("taiju");$taiju->email("higashi@taiju.info");$taiju->auth_type("MT");$taiju->status(1);$taiju->set_password("password");$taiju->is_superuser(1);$taiju->save or $taiju->errstr;printf "You can use taiju (id=%d) (password=password)! taiju is suupppeeeer user!!\n", $taiju->id;'
...セミコロン......
これがワンライナーと言えるなら、minifyされた jQueryですらワンライナーになってしまいますね。
MT::Object::Chaining
ということで作ったのが下記になります。
MT::Object::Chaining - Methods chaining for MT::Object
MT::Object::Chainingを使うと、下記のように書けます。
perlコマンドのように、Iオプションや、Mオプション指定して、インスタンス取得するのすらダルいので、tools/chainというスクリプトでその辺をカバーしてます。
mオプションでモデルを指定して、eオプションでeval文字列を指定します。
$ cd /path/to/mt && tools/chain -m author -e '$model->get_by_key({ name => "taiju" })->nickname("taiju")->email("higashi\@taiju.info")->auth_type("MT")->status(1)->set_password("password")->is_superuser(1)->save->tap(sub { printf "You can use taiju (id=%d) (password=password)! taiju is suupppeeeer user!!\n", shift->id })'
こんな感じでメソッドチェーンで書けるようにする機能を提供するような感じですね。
まとめ
これでMT::Objectをワンライナーしやすくなりました。
$ tools/chain -m author -e '$model->load->json' | jq '.[] | {name, email}'
みたいな感じでjqに渡したりなど、捗るかもしれません。
また、mapメソッドで一括置換して保存とか、reduceで加工した結果を出力するとか、普段使いに個人的に便利だと思う機能も入れました。
まだ、v0.1なのと、統一感に欠ける気もしているので、インターフェイスの変更するかもしれません。
詳しい使い方は、README.pod を参照下さい。
https://github.com/taiju/MT-Object-Chaining/blob/master/README.pod
ほなの。
MT アプリケーションを Sinatra like に記述できる MT::App::Lite 作った
Movable Type には、MT::App という Web アプリケーションを記述するための基底クラスがありますが、結構慣れが必要なのと癖があることもあって、もっと手軽に Sinatra like にアプリケーションを記述できる、フレームワーク的なものが欲しいなーと常々思っていました。
とりあえずいろいろやりたいことは残っていますが、動くところまでできたので、紹介します。
現状では、PSGI でのみ動作します。
概要
以下のような感じでMTアプリケーションを記述します。
package MyLiteApp; use strict; use MT::App::Lite; setup Renderer => 'Xslate'; get '/' => sub { my $app = shift; $app->render('index.tt', { blog => MT->model('blog')->load(1), entries => [MT->model('entry')->load({blog_id => 1})], }); }; get '/entry/:id' => sub { my $app = shift; $app->render('entry.tt', { blog => MT->model('blog')->load(1), entry => [MT->model('entry')->load($app->param('id'))], }); }; 1; __DATA__ @@ index.tt <!doctype html> <html> <head> <meta charset="utf-8"> <title><: $blog.name :></title> </head> <body> <ul> : for $entries -> $entry { <li><a href="<: $entry.permalink :>"><: $entry.title :></a></li> : } </ul> </body> </html> @@ entry.tt <!doctype html> <html> <head> <meta charset="utf-8"> <title><: $entry.title :> | <: $blog.name :></title> </head> <body> <h1><: $entry.title :></h1> <div> <: $entry.text | mark_raw :> </div> </body> </html>
1ファイル完結型です。(正確にはプラグインの config.yaml が必要)
多分、フォームとか作るのに便利だと思います。
仕様
MT アプリケーションのハンドラ内で、Router::Simple::Sinatraish でエクスポートされる、get や post などの関数を使って、ルートを記述します。
そのファイル中の __END__ セクションに、テンプレートを書きます。
__END__ セクションに記述したテンプレートは、Data::Section::Simple によって、MT::App::Lite に渡されます。
テンプレートは、Text::Xslate を標準のテンプレートエンジンにしています。申し訳程度に MTML 用のテンプレートエンジンも用意しましたが、ここでわざわざ使う必要はない気がします。
MT::App::Lite::Renderer::Foo という形で、render メソッドと render_string メソッドを用意すれば、任意のテンプレートエンジンを利用することは可能です。
また、MT::App::Lite を use すると、自動的に MT::App を継承することになるので、MT::App っぽい書き方もできるのかもしれません。
基本的には、MT::App に Web アプリケーションを記述するための機能はひと通り揃っているので、便利っぽいモジュールを糊付けしたような実装です。
MT アプリケーションのプラグインを作成する
このモジュールを使った、MTアプリケーションのプラグインの作り方です。
最小限の config.yaml と、ハンドラを用意します。
config.yaml
name: MyLiteApp id: myliteapp applications: lite_app: handler: MyLiteApp script: sub { 'app' } cgi_path: sub { '/' }
MyLiteApp (handler)
package MyLiteApp; use strict; use MT::App::Lite; setup Renderer => 'Xslate'; get '/' => sub { my $app = shift; $app->render('index.tt', { blog => MT->model('blog')->load(1), entries => [MT->model('entry')->load({blog_id => 1})], }); }; get '/entry/:id' => sub { my $app = shift; $app->render('entry.tt', { blog => MT->model('blog')->load(1), entry => [MT->model('entry')->load($app->param('id'))], }); }; 1; __DATA__ @@ index.tt <!doctype html> <html> <head> <meta charset="utf-8"> <title><: $blog.name :></title> </head> <body> <ul> : for $entries -> $entry { <li><a href="<: $entry.permalink :>"><: $entry.title :></a></li> : } </ul> </body> </html> @@ entry.tt <!doctype html> <html> <head> <meta charset="utf-8"> <title><: $entry.title :> | <: $blog.name :></title> </head> <body> <h1><: $entry.title :></h1> <div> <: $entry.text | mark_raw :> </div> </body> </html>
これで、http://yourdomain/app/ や、http://yourdomain/app/entry/:id にアクセスするとテンプレートの内容が描画されます。
まとめ
本当は、Movable Type Advent Calendar 2013 の自分担当日のネタとして作ろうと考えていたんですけど、実装する暇がなく、今日に至ります。
便利な気がするので、メンテしていこうと思います。まだいろいろと実装したいことや、実装すべきことがあります。
現時点ではちょっとした Viewer やフォームを作るのには便利な気がしています。
Ruby の method_missing 的な tag_missing コールバックを MT に追加してみる
Movable Type Advent Calendar 2013 の8日目です。
Ruby の method_missing
だったり、Perl の AUTOLOAD
的なものが、MT のタグを利用する時に使えると便利かなと思って、TagMissingプラグインなるものを作ってみました。存在しないタグを利用しようとした時に、tag_missing
というコールバックを呼ぶ単純なものです。ダイナミックは未対応です。
ただ、これを実現する方法は、MT の処理をモンキーパッチするしかなさそうだったのと、作ってみたものの、インターフェイスも微妙な感じがしますが、せっかくなので公開します。
TagMissing プラグイン
タグが存在しない時のエラー処理は、MT::Builder::build を見る限り、下記のあたりにあって、
my $hdlr = $ctx->handler_for( $t->tag ); my ( $h, $type, $orig ) = $hdlr->values; my $conditional = defined $type && $type == 2; if ($h) { # ... 省略 ... } else { if ( $t->tag !~ m/^_/ ) { # placeholder tag. just ignore return $build->error( MT->translate( "Unknown tag found: [_1]", $t->tag ) ); } }
MT::Template::Context::handler_for メソッドの結果次第で、分岐の経路が変わる感じなので、MT::Template::Context::handler_for の中にコールバックを仕込むのが確実かなと思ったので、TagMissing プラグインでは、handler_for
をパッチする方針にしました。
モンキーパッチする時は、本体のバージョンアップの影響をなるべく受けないようにした方が良いので、無駄が多い感じですが、handler_for
をコールバック前後で2回呼び出す形にしました。
package TagMissing::Patch; use strict; use warnings; sub init {} use MT::Template::Context; my $orig_handler_for = \&MT::Template::Context::handler_for; { no warnings 'redefine'; *MT::Template::Context::handler_for = sub { my $ctx = shift; my $tag = lc $_[0]; my $hdlr = $orig_handler_for->( $ctx, $tag ); my ( $h ) = $hdlr->values; MT->run_callbacks( 'tag_missing', $ctx, $tag ) unless $h; $orig_handler_for->( $ctx, $tag ) }; } 1;
インターフェイス的にかなり微妙な感じですが、tag_missing コールバックの中で、$ctx->{__handlers}{tag_name_foo}
に MT::Templage::Handler 形式でハンドラを代入するなどして、タグの動作を動的に指定することができます。
AutoUnless プラグイン
そんな感じで、tag_missing コールバックを使ったタグのリファレンス実装として、AutoUnless プラグインというのも作ってみました。
mt:IfWebsite
みたいな、If という単語の含まれたタグがいろいろありますが、mt:UnlessWebsite
みたいな逆の意味を持つタグがあったりなかったりするので、mt:*unless*
がなければ自動的に定義するプラグインです。
tag_missing
が呼ばれた時に、動的に定義する感じの実装で、tag_missing コールバックのサンプルプラグインにもなっています。
MT の t ディレクトリにある、35-tags.t を丸々移して、mt:*If*
を mt:*Unless*
に書き換えた上で、テスト結果を逆にしたテストを書いてみましたが、一部を除いてうまく動いているようです。
https://github.com/taiju/mt-plugin-AutoUnless/blob/master/plugins/AutoUnless/t/tags.t
動的にいくつかのタグを定義するような時には、tag_missing コールバックは便利に使えそうです。
まとめ
あると便利な気はするんですけど、アイデアありきで作ってみたものの、実装は正直微妙な感じになってしまいました。
今年はコードを書くより、読む時間が圧倒的に多い年でした。来年は、書く時間を増やせたらいいなと思います。