まだText::XslateでもText::MicroTemplateでもなくTemplate::Toolkitを主に使っているわけですが。。
やりたいこと
テンプレート中に出てくるURLを自動でアンカーテキストにしたい。
http://example.com/
が自動的に
<a href="http://example.com/">http://example.com/</a>
になってほしい。このはてなダイアリーでURL書くだけでhttp://example.com/みたいに自動でリンクになるように。
Template::Plugin::AutoLink
Template::Plugin::AutoLink - search.cpan.org
というモジュールがあります。これを使えば、期待通りの動作をします。
use Test::More; use strict; use warnings; use Template; use Template::Plugin::AutoLink; my $t = Template->new; $t->process(\'[% USE AutoLink %]'); my $url = 'http://example.com/'; my $text = "$url"; my $template = '[% text | auto_link %]'; ok $t->process(\$template, { text => $text }, \my $output); cmp_ok $output, '=~', qr!<a\s+href="$url">$url</a>!; done_testing;
$target = "hoge $url fuga"のように無関係な文字列が前後にあっても大抵の場合はURLの部分だけを変換してくれます。
ところがhtmlフィルタと組み合わせると
TTは自動エスケープ機能がないので、よくhtmlフィルタを使用します。
use Test::More; use strict; use warnings; use Template; my $t = Template->new; my $text = '<script>alert("こんにちはこんにちは")</script>'; my $template = '[% text | html %]'; # エスケープされて # <script>alert("こんにちはこんにちは")</script> # となる ok $t->process(\$template, { text => $text }, \my $output); cmp_ok $output, 'ne', $text; done_testing;
で、このフィルタとT::P::AutoLinkを併用すると、以下が通らなくなります。
use Test::More; use strict; use warnings; use Template; use Template::Plugin::AutoLink; my $t = Template->new; $t->process(\'[% USE AutoLink %]'); my $url = 'http://example.com/'; my $text = "<$url>"; my $template = '[% text | html | auto_link %]'; ok $t->process(\$template, { text => $text }, \my $output); cmp_ok $output, '=~', qr!<a\s+href="$url">$url</a>!; done_testing;
URLの末尾にそのまま " や < > が繋がっていると、htmlフィルタを通った後が
<http://example.com/>
のようになり、"&"でクエリパラメータとしてURLが続くような文字列になり、T::P::AutoLinkはそれを検知できずに丸ごとアンカーテキストにしてしまい
'<<a href="http://example.com/>">http://example.com/></a>'
という結果になってしまいます。
Text::AutoLink?
では自分でフィルタを作成して使うのが良い?
TTとは別にText::AutoLinkというモジュールがあります。
http://search.cpan.org/~dmaki/Text-AutoLink-0.03000/lib/Text/AutoLink.pm
これは中でウマいことparseしてくれるので、
use Test::More; use strict; use warnings; use Template; use Text::AutoLink; my $t = Template->new; my $url = 'http://example.com/'; my $text = qq!"$url"!; my $template = '[% text | html %]'; ok $t->process(\$template, { text => $text }, \my $output); $output = Text::AutoLink->new->parse_string($output); cmp_ok $output, '=~', qr!<a\s+href="$url">$url</a>!; done_testing;
とやると無事に通ります。が、これは中の仕組み上htmlフィルタでエスケープしたものが元に戻ってしまうようで htmlフィルタと組み合わせるにはちょっと微妙かも。
自前で置換する
正規表現を使って、
- URLはアンカーテキストに変換
- それ以外の文字列はすべてHTMLエスケープ
という変換をテキスト全体に施す。
use Test::More; use strict; use warnings; use Regexp::Common 'URI'; use Template::Filters; my $url = 'http://example.com/'; my $text = qq!"$url"!; my $output = autolink_and_escape($text); cmp_ok $output, '=~', qr!<a\s+href="$url">$url</a>!; done_testing; sub autolink_and_escape { my $text = shift; my $re_uri = qr/($RE{URI}{HTTP})/; my @arr = split $re_uri, $text; for (0..$#arr) { if ($_ % 2) { $arr[$_] = qq!<a href="$arr[$_]">$arr[$_]</a>!; } else { $arr[$_] = $Template::Filters::FILTERS->{html}->($arr[$_]); } } return join '', @arr; }
実行効率は良くないかもしれないけど、このautolink_and_escapeの機能を持つフィルタをTemplate::Pluginで作ってやれば、とりあえずやりたいと思っていたことは実現できそう。
他のTemplateエンジンではどうする?
T::MTやT::Xslateでは、こういうことをやろうとするとどうなるのだろう? デフォルトでhtmlエスケープをするとなると、HTMLタグを敢えて挿入するような処理は向いてなさそうな…?
むしろテンプレートやサーバー側での処理は行わず、表示させてからJavaScriptで変換させるとかいう処理になるのかな…?
はてなダイアリーはどうやってこれを実現しているのだろう?
余談
URLの末尾に"#hoge"とか(フラグメント識別子 fragment identifier というらしい)がついている場合、この部分はRegexp::Common::URIでは無視されてしまう。ので、この部分まで含めたい場合はhttp URIの正規表現に付け足してやる必要があるみたい。
my $hex = q{[0-9A-Fa-f]}; my $escaped = qq{%$hex$hex}; my $uric = q{(?:[-_.!~*'()a-zA-Z0-9;/?:@&=+$,]} . qq{|$escaped)}; my $fragment = qq{$uric*}; my $re_uri = qq{$RE{URI}{HTTP}(?:#$fragment)?};
追記
@tokuhiromさんからコメントいただきました。ありがとうございます! TMTなどの場合 encoded_string()をつかって
use Test::More; use strict; use warnings; use Text::AutoLink; use Text::MicroTemplate qw/render_mt encoded_string/; my $url = 'http://example.com/'; my $text = qq!<"$url">!; my $template = '<?= encoded_string(Text::AutoLink->new->parse_string($_[0])) ?>'; my $output = render_mt($template, $text); cmp_ok $output, '=~', qr!<a\s+href="$url">$url</a>!; done_testing;
のようにするとdouble encodedにはならない、とのこと。
あれ、でもこの場合 $outputは
<"<a href="http://example.com/">http://example.com/</a> ">
となってしまう。。 URLはaタグで囲むとして、それ以外の先頭、末尾の'<"', '">' はエスケープしてもらって
<"<a href="http://example.com/">http://example.com/</a>">
になってほしいのだけど… ソースをみた限りではencoded_string()つかってもそういう処理は難しそうな…! よくわからなくなってきた! ><