Re: Chromeのスタートページで自分のはてブをインクリメンタル検索する拡張(ただしPerlで書いた)

http://d.hatena.ne.jp/Cside/20110214/p1
こんなに簡単にChrome拡張が作れるとは!と感動する一方で、Xslateの使い方がいまいちだったので直してみました。pull-reqだけでいいかとも思ったのですが、これらの修正はすべて何度か見たことのあるFAQ的なものなので、エントリにすることにしました。

具体的には、

  • Xslateインスタンスをリクエスト毎に作るのは非効率なので、最初に作って使いまわす
  • render_string()は非常に遅いのでテストやデバッグ以外の用途で使うべきではない
    • 今回のケースでは普通にファイル名を渡すだけでよい
    • スクリプト中にテンプレートを書きたい場合は、pathオプションにハッシュリファレンスを渡すとよい
  • Xslateは基本的にテキスト文字列(decodeされたもの)を扱うので、functionはテキスト文字列を受け取って文字列を返すべき。よって、encode_utf8()して返してはいけない*1 *2
    • functionの引数も本当はテキスト文字列であるべきだが、実際にはこれは難しいことがあるので引数をdecode_utf8()するのはあり。
  • URI escapeを行うbuilt-in functionとしてuriがある。よってmodule => ['URI::Escape']は必要ない

という感じです。

差分:

commit d1457d2d476b3bde1dae89880f2b728870ca2aba
Author: Fuji, Goro <gfuji@cpan.org>
Date:   Mon Feb 14 12:05:54 2011 +0900

    Fix usage of Xslate

diff --git a/app.psgi b/app.psgi
index d520cde..b806ae2
--- a/app.psgi
+++ b/app.psgi
@@ -6,7 +6,6 @@ use lib 'lib';
 use utf8;
 use Encode;
 use Config::Pit;
-use Path::Class;
 
 use Perl6::Say;
 use MyBookmark;
@@ -27,6 +26,31 @@ my $my_bookmark = MyBookmark->new(
     password => $config->{password},
 );
 
+my $tx = Text::Xslate->new(
+    syntax => 'TTerse',
+    module => [
+        'Text::Xslate::Bridge::TT2Like',
+    ],
+    function => {
+        truncates => sub {
+            my ($str, $size, $suffix) = @_;
+            $str = decode_utf8($str);
+            $str    = ''    unless $str;
+            $size   = 64    unless $size;
+            $suffix = "..." unless $suffix;
+            my $b = 0;
+            for (my $i = 0; $i < length $str; $i++) {
+                $b += length(encode_utf8 substr($str, $i, 1)) == 1 ? 1 : 2;
+                if ($b > $size) {
+                    return substr($str, 0, $i) . $suffix;
+                }
+            }
+            return $str;
+        },
+    },
+    path => ['template'],
+);
+
 my $app = sub {
     my $env = shift;
     my $req = Plack::Request->new($env);
@@ -52,44 +76,10 @@ my $app = sub {
     }
     my $res = $req->new_response($result ? 200 : 404);
     $res->content_type('text/html; charset=utf-8');
-    $res->body(encode_utf8(render(
+    $res->body(encode_utf8($tx->render('index.html', {
         components => $result ne 'empty' ? $result : [],
         q => $q
-    )));
+    })));
     $res->finalize;
 };
 
-sub render {
-    my $tx = Text::Xslate->new(
-        syntax => 'TTerse',
-        module => [
-            'URI::Escape',
-            'Encode' => ['encode_utf8'],
-            'Text::Xslate::Bridge::TT2Like',
-        ],
-        function => {
-            truncates => sub {
-                my ($str, $size, $suffix) = @_;
-                $str = decode_utf8($str);
-                $str    = ''    unless $str;
-                $size   = 64    unless $size;
-                $suffix = "..." unless $suffix;
-                my $b = 0;
-                for (my $i = 0; $i < length $str; $i++) {
-                    $b += length(encode_utf8 substr($str, $i, 1)) == 1 ? 1 : 2;
-                    if ($b > $size) {
-                        return substr($str, 0, $i) . $suffix;
-                    }
-                }
-                encode_utf8 $str;
-            },
-        },
-    );
-    my $template = decode_utf8(scalar file('template/index.html')->slurp);
-    my $result = $tx->render_string(
-        $template,
-        { @_ },
-    );
-}
-
-$app;

*1:実際には、実装上のミスによりバイナリ文字列(encode_utf8()したもの)でも文字化けせずに動いてしまう。しかし、この動作に頼るのは誤り。

*2:なお、コンストラクタに input_layer => ':bytes' を与えることでバイナリ文字列のまま操作することもできるが、通常は必要ない。