すぎゃーんメモ Twitter

2010-09-03

Template中のURLを自動で賢くアンカーテキストにしたい

まだ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 %]';

# エスケープされて
# &lt;script&gt;alert(&quot;こんにちはこんにちは&quot;)&lt;/script&gt;
# となる
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フィルタを通った後が

&lt;http://example.com/&gt;

のようになり、"&"でクエリパラメータとしてURLが続くような文字列になり、T::P::AutoLinkはそれを検知できずに丸ごとアンカーテキストにしてしまい

'&lt;<a  href="http://example.com/&gt;">http://example.com/&gt;</a>'

という結果になってしまいます。


Text::AutoLink?

では自分でフィルタを作成して使うのが良い?

TTとは別にText::AutoLinkというモジュールがあります。

Text::AutoLink - search.cpan.org

これは中でウマいこと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フィルタと組み合わせるにはちょっと微妙かも。


自前で置換する

正規表現を使って、

という変換をテキスト全体に施す。

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)?};

(参考:http://www.din.or.jp/~ohzaki/perl.htm#httpURL)


追記

@さんからコメントいただきました。ありがとうございます! 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タグで囲むとして、それ以外の先頭、末尾の'<"', '">' はエスケープしてもらって

&lt;&quot;<a href="http://example.com/">http://example.com/</a>&quot;&gt;

になってほしいのだけど… ソースをみた限りではencoded_string()つかってもそういう処理は難しそうな…! よくわからなくなってきた! ><

tokuhiromtokuhirom 2010/09/03 10:01 TMT の場合、以下のように、encoded_string() すると、この変数はエスケープ済ですよ、という印がつけられるので、double encoded にはならないので大丈夫です。

Xslate も同様。

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;

lestrratlestrrat 2010/09/09 12:22 Text::AutoLink 作者だからはっきり言えるけど、3年とか4年とかメンテナンスされてないモジュールは使わないほうが幸せになれますよ!

tokuhiromtokuhirom 2010/09/09 12:28 今回は plain text 中のをリンクにしたいって話だから URL の抽出と並行で HTML Escape する方がよくて、別段階で escape しようとしてるのがよくないかんじですね。

↓ こんなかんじで。see perldoc URI::Find

use CGI qw(escapeHTML);
use URI::Find;

my $finder = URI::Find->new(sub {
my($uri, $orig_uri) = @_;
return qq|<a href="$uri">$orig_uri</a>|;
});
$finder->find(\$text, \&escapeHTML);
print "<pre>$text</pre>";

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証