― Web Technology and Life ―

PerlのテンプレートエンジンText::XslateにおけるINCLUDEの取り扱いの注意

2011-11-09
久しぶりに2時間以上もはまったので愚痴がてら共有しておきます。具体的には、CPANモジュールガイドにも出てくるText::Markdownとの連携と、INCLUDEしたファイル内の「:」の取り扱いについてです。

概要 ~Markdownを使う理由とサンプルアプリの概要~

Markdownっていまいち文法がわからないし、tableとかdlとかいまいちぐぐっても出てこないから使っていなかった。だけど、最近、「Markdownいいよ」って言われること増えたし、そろそろ自分のブログも生HTMLを書くのに飽きたし、仕事でもHTMLだけ書く人に発注するもの馬鹿馬鹿しいし、Webにアップする前提の文章書くならやっぱり最初からMarkdownで書いてしまったほうがいいという結論に至った。それで、いろいろ使い方確認したいし、周りの人の啓蒙も兼ねて、Markdownで書いた文章をHTMLに変換するアプリを「さくっと」書こうとしたら、「MarkdownとText::Xslateとの連携ではまったよ」というのがきっかけ。ちなみに動作確認の、Text::Markdownのバージョンは「1.000031」で、Text::Xslateのバージョンは「1.5002」です。

Text::Markdownをincludeで連携するときの注意

問題

早速ですが、CPANモジュールガイドにも出てくるText::XslateにおけるファイルのincludeによるText::Markdownとの連携ですが、以下のようなコードはうまくいきません。

use strict;
use warnings;
use utf8;
use File::Spec;
use File::Basename;
use lib File::Spec->catdir(dirname(__FILE__), 'extlib', 'lib', 'perl5');
use lib File::Spec->catdir(dirname(__FILE__), 'lib');
use Plack::Builder;
use Amon2::Lite;

use Text::Xslate qw/html_builder mark_raw/;
use Text::Markdown qw/markdown/;

sub config {
    +{
        'Text::Xslate' => {
            'function' => {
                markdown     => html_builder { Text::Markdown::markdown(@_) },
            },
        }
    }

}

get '/markdown_sample' => sub { $_[0]->render('markdown_sample.tx')  };

builder {
    enable 'Plack::Middleware::Static',
        path => qr{^(?:/static/|/robot\.txt$|/favicon\.ico$)},
        root => File::Spec->catdir(dirname(__FILE__));
    enable 'Plack::Middleware::ReverseProxy';
    enable 'Plack::Middleware::Session';

    __PACKAGE__->to_app();
};

__DATA__

@@ markdown_sample.tx
<!doctype html>
<html>
<body>

<div style="color:red;">markdown文章を生表示</div>

<pre>
    [% INCLUDE 'docs.txt' %]
</pre>

<div style="color:red;">markdown文章をmarkdownに通してHTML化</div>

<pre>
[% FILTER markdown %]
    [% INCLUDE 'docs.txt' %]
[% END %]
</pre>

<div style="color:red;">markdownのサンプルをdump</div>
<pre>
[% FILTER dump %]
    [% INCLUDE 'docs.txt' %]
[% END %]
</pre>
</body>
</html>

@@ docs.txt
#見出しです(h1)
##二番目の見出しです(h2)
###三番目の見出しです(h3)
####四番目の見出しです(h4)

「plackup」で実行すると、[% FILTER markdown %]のほうは何も出力されません。そして、以下のようなエラーが表示されます。[% FILTER dump %]の方は正常に出力されますので、TTの書き方が間違っているわけではないはずです。

Text::Xslate: Not a HASH reference at /home/hirobanex/perl5/perlbrew/perls/perl-5.10.1/lib/site_perl/5.10.1/Text/Markdown.pm line 202.

以下のようにText::Markdownの該当のコード見るとおかしいことに気づきます。Text::Markdownはnewしていないので、「unless (ref $self) {」の中の処理に入るはずです。$selfに実際のテキストが入っているはずです。

#Text::Markdownの該当のコード

sub markdown {
    my ( $self, $text, $options ) = @_;

    # Detect functional mode, and create an instance for this run
    unless (ref $self) {
        if ( $self ne __PACKAGE__ ) {
            my $ob = __PACKAGE__->new();
                                # $self is text, $text is options
            return $ob->markdown($self, $text);
        }
        else {
            croak('Calling ' . $self . '->markdown (as a class method) is not supported.');
        }
    }

    $options ||= {};

    %$self = (%{ $self->{params} }, %$options, params => $self->{params});

    $self->_CleanUpRunData($options);

    return $self->_Markdown($text);
}

「あれー、おかしいな」と「warn $self」すると、これが不思議なことに[% INCLUDE 'docs.txt' %]で引っ張ってきた中身が表示されるのです。ここで、ちょっと気を抜いたために(実際に私がはまったときは別の要因もあったために)、本質的な問題に気づかなかったのですが、これを「use Data::Dumper; warn Dumper $self;」すると以下のように表示されることで問題に気がづきます。

Text::Xslate: $VAR1 = bless( do{\(my $o = "
#\x{898b}\x{51fa}\x{3057}\x{3067}\x{3059}\x{ff08}h1\x{ff09}
##\x{4e8c}\x{756a}\x{76ee}\x{306e}\x{898b}\x{51fa}\x{3057}\x{3067}\x{3059}(h2)
###\x{4e09}\x{756a}\x{76ee}\x{306e}\x{898b}\x{51fa}\x{3057}\x{3067}\x{3059}(h3)
####\x{56db}\x{756a}\x{76ee}\x{306e}\x{898b}\x{51fa}\x{3057}\x{3067}\x{3059}(h4)
")}, 'Text::Xslate::Type::Raw' );

オーバーロード系のオブジェクトになっていたんですねー。Text::Markdownの内部のトリックあいまって、非常に不都合なことが起こっていたのです。

ここだけの結論

この問題は、以下のようにText::Markdownをオブジェクトにしてから使うことで解決できるのできます。

'function' => {
    markdown     => html_builder { my $m = Text::Markdown->new; $m->markdown(@_) },
},

ホントはもうひとつの方の解決方法がいいのですが、それは下で書きます。

【追記】
はてぶとツイートでコメントいただいておりますが。本件、トミールさんにCPAN本の正誤表に載せて頂きました。→CPANモジュールガイドの正誤表。また、tokuhiromさんがText::Markdownのパッチを送ったということで、2011-11-23現在まだ取り込まれていませんが、そのうち取り込まれるとうれしいですねー。ブログに書いたことで、ちょっとでも役に立ったようで、うれしいです。(ちなみに、本件を発見するきっかけになった『Text::MultiMarkdownにdecodeさせた文字列を投げ込むと定義リストの解釈に失敗する』という問題も、makamakaさんにヒントを頂き、思い切ってパッチを送ってみました、取り込まれなくてもきっかけに該当の問題が解決されればうれしいなーと思います。)

Kolonシンタックスでincludeファイルの中に「:」が入っているとエラーに

上記との変化

Text::Markdownだとtable,dlタグ使えないことが分かったので、Text::MultiMarkdownを使うことにしました。それと、同時に、やっぱりシンタックスはKolonだよねーっていう感じでhide_o_55さんに言われてTTerseからKolonに変更しました。なので、TTerseだとどうなるかは知りませんが、Kolonが災いに。。。

問題

とりあえず、上記の変化を反映させたの以下ですが、これが正常に動きません。

use strict;
use warnings;
use utf8;
use File::Spec;
use File::Basename;
use lib File::Spec->catdir(dirname(__FILE__), 'extlib', 'lib', 'perl5');
use lib File::Spec->catdir(dirname(__FILE__), 'lib');
use Plack::Builder;
use Amon2::Lite;
use Text::Xslate qw/html_builder mark_raw/;
use Text::MultiMarkdown qw/markdown/;

sub config {
    +{
        'Text::Xslate' => {
            'syntax'   => 'Kolon',
            'function' => {
                markdown     => html_builder { my $m = Text::MultiMarkdown->new; $m->markdown(@_) },
            },
        }
    }
}

get '/markdown_sample' => sub { $_[0]->render('markdown_sample.tx')  };

builder {
    enable 'Plack::Middleware::Static',
        path => qr{^(?:/static/|/robot\.txt$|/favicon\.ico$)},
        root => File::Spec->catdir(dirname(__FILE__));

    enable 'Plack::Middleware::ReverseProxy';
    enable 'Plack::Middleware::Session';

    __PACKAGE__->to_app();
};

__DATA__

@@ markdown_sample.tx
<!doctype html>
<html>
<body>

<div style="color:red;">markdown文章を生表示</div>

<pre>
    :include docs
</pre>

<div style="color:red;">markdown文章をmarkdownに通してHTML化</div>

<pre>
: block source_markdown | markdown -> {
    : include docs
: }
</pre>

</body>
</html>

@@ docs.tx

#見出しです(h1)
##二番目の見出しです(h2)
###三番目の見出しです(h3)
####四番目の見出しです(h4)

定義リスト
:    ほげほげ

住友工業
:    そんな企業はありません

三菱物産
:    さんな企業もありません

ゲーム
:    子供だけが遊ぶものではありません

「plackup」で実行すると、Error traceに以下のように表示されます。

Text::Xslate: Text::Xslate::Syntax::Kolon: Unknown operator 'ほ', while parsing templates (:7) at /home/hirobanex/perl5/perlbrew/perls/perl-5.10.1/lib/site_perl/5.10.1/Amon2/Web.pm line 159
----------------------------------------------------------------------------
定義リスト
:    ほげほげ
----------------------------------------------------------------------------

MultiMarkdownの定義リスト(dlとか)を書くときに、「:」を使うのがまずいんですねー。。。こちらはエスケープする方法がありそうな気がするので、ご存知のかたいたらコッソリでもいいので教えて欲しいです。

結論

「:」の解釈が問題になるのは、templateファイル上の場合が問題なので、そうじゃなくすればいいのです。以下でまとめて詳細に述べます。

最終的な教訓

要するに、includeはインジェクションにまつわる繊細な対応がテンプレートエンジンに施されているから、ファイルをincludeするんじゃなくて、いったんコードサイドで変数にもって、それでからテンプレートに流し込むのが総じて吉だと思います。以下、最終的な吉なコードです。

use strict;
use warnings;
use utf8;
use File::Spec;
use File::Basename;
use lib File::Spec->catdir(dirname(__FILE__), 'extlib', 'lib', 'perl5');
use lib File::Spec->catdir(dirname(__FILE__), 'lib');
use Plack::Builder;
use Amon2::Lite;
use Text::Xslate qw/html_builder mark_raw/;
use Text::MultiMarkdown qw/markdown/;
use Path::Class qw/file/;

sub config {
    +{
        'Text::Xslate' => {
            'syntax'   => 'Kolon',
            'function' => {
                markdown     => html_builder { my $m = Text::MultiMarkdown->new; $m->markdown(@_) },
            },
        }
    }
}

get '/markdown_sample' => sub {
    my $text = file('./markdown.txt')->slurp;

    $_[0]->render('markdown_sample.tx',+{ text => $text });
};

builder {
    enable 'Plack::Middleware::Static',
        path => qr{^(?:/static/|/robot\.txt$|/favicon\.ico$)},
        root => File::Spec->catdir(dirname(__FILE__));

    enable 'Plack::Middleware::ReverseProxy';
    enable 'Plack::Middleware::Session';

    __PACKAGE__->to_app();
};

__DATA__
@@ markdown_sample.tx

<!doctype html>
<html>
<body>

<div style="color:red;">markdown文章を生表示</div>
<pre><: $text :></pre>

<div style="color:red;">markdown文章をmarkdownに通してHTML化</div>

<pre><: $text | markdown :></pre>

</body>
</html>
#./markdown.txt

#見出しです(h1)
##二番目の見出しです(h2)
###三番目の見出しです(h3)
####四番目の見出しです(h4)

定義リスト
:    ほげほげ

住友工業
:    そんな企業はありません

三菱物産
:    さんな企業もありません

ゲーム
:    子供だけが遊ぶものではありません

【追記】
「:」を「<: ":" :>」と書けばインクルード元でもエラーにならないというのを、はてぶにコメント頂きましたbayashiさん、ありがとうございます!その手があったかという感じですw本件の文脈では適切ではありませんが、ケースバイケースで使える(現場では臨機応変にしなくいけない)ので、覚えておいて損はないですね!

最後に

いやー、慣れないテンプレートエンジンを使うのが大変ですねー。業務上重要なところで、詳しくないけど使ってみたい新しいものを入れるのは難しいですけど、こういう軽いもので試していくことはとてもいいなーっと思っていたのですが、まさかここまで連続ではまるとは。。。まぁ、これからはXslateの時代ということで、Xslate使っていきたいところですが、XslateのKolonを使ったベストプラクティス的なのがあると、とっかかり的にはいいんだけどなーと思ったりもしました。あー、hachioji.pmで発表した内容をまとめなおしていなかったなー。

Perl update_at : 2011-11-23T22:12:43
hirobanex.netの更新情報の取得
 RSSリーダーで購読する   
blog comments powered by Disqus