gem戦記

2010-11-26

Rails3のビューでブロックを伴なうHelperを使う時の注意 16:31

globalize3というgemを評価していて、Rails3のおかしな挙動に気がつきました。

ビュー内で、Globalize.with_locale というメソッド使うと、一部の内容がダブって出力されるというものです。

最初、globalize3の問題かと思いましたが、調べてみると、Rails3の問題のようです。ビュー内でブロックを使うHelperメソッドを使うと、そのブロックまでの内容が二重に出力されます。

以下の手順で再現できます。(Rails 3.0.3で確認)

新規アプリケーション作成

$ rails new just_yield
$ cd just_yield

コントローラを追加

$ rails g controller welcome index
$ rm public/index.html

config/routes.rbに以下を追加

root :to => "welcome#index"

app/helpers/application_helper.rb に、yieldを行なうだけのHelperメソッドを追加

module ApplicationHelper
  def just_yield
    yield
  end
end

ビューを以下のように変更

<h1>Welcome#index</h1>

A
<% just_yield do %>
B
<% end %>
C

サーバを起動して、http://localhost:3000/にアクセスすると、以下の出力になります。

<!DOCTYPE html>
<html>
<head>
  <title>JustYield</title>
  
  <script src="/javascripts/prototype.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/effects.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/dragdrop.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/controls.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/rails.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/application.js?1290752743" type="text/javascript"></script>
  <meta name="csrf-param" content="authenticity_token"/>
<meta name="csrf-token" content="Dow8TXbSPVQh9azlacoLXSTrlOTnfsdJIuJvgfJP8EQ="/>
</head>
<body>

<h1>Welcome#index</h1>

A
B
<h1>Welcome#index</h1>

A
B
C




</body>
</html>

つまり、ビューの先頭から、ブロックの終わりまでが二重に出力されます。

ビューを以下ように修正して、just_yieldの評価結果を変更すると正常に出力されます。

<h1>Welcome#index</h1>

A
<% just_yield do %>
B
<% 1; end %>
C

出力結果

<!DOCTYPE html>
<html>
<head>
  <title>JustYield</title>

  <script src="/javascripts/prototype.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/effects.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/dragdrop.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/controls.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/rails.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/application.js?1290752743" type="text/javascript"></script>
  <meta name="csrf-param" content="authenticity_token"/>
<meta name="csrf-token" content="Dow8TXbSPVQh9azlacoLXSTrlOTnfsdJIuJvgfJP8EQ="/>
</head>
<body>

<h1>Welcome#index</h1>

A
B
C




</body>
</html>

つまり、ビュー内で使用するHelperメソッドが、yieldの結果をそのままリターンすると、二重の出力になるようです。

さらにこれの原因を調べてみると、do ... end を含むERBソースは、次のようにコンパイルされます。

@output_buffer.append_if_string=  just_yield do
  @output_buffer.safe_concat('B\n');
end

つまり、<%= ... %> ではなくて、<% ... %>を使っているのに、その式の評価結果を出力結果として使用するようです。このメソッドの実装は以下の所です。

gems/actionpack-3.0.3/lib/action_view/template/handlers/erb.rb

    def append_if_string=(value)
      if value.is_a?(String) && !value.is_a?(NonConcattingString)
        ActiveSupport::Deprecation.warn("<% %> style block helpers are deprecated. Please use <%= %>", caller)
        self << value
      end
    end

この Deprecation.warn は、確かに上記の問題が発生する時に、出力されています。

後方互換性のために、ブロックを伴う式の評価結果が文字列だと、それが出力されるようにしてある、ということのようです。

結論として、ブロックを伴う Helperメソッドの実装を変更できる場合には、

module ApplicationHelper
  def just_yield
    yield
    1 # 文字列を返却するとテンプレートに出力されてしまうので、文字列以外を返す
  end
end

とすればよくて、実装が変更しにくい場合はビューの方を下記のように変更すれば、良いと思います。

<h1>Welcome#index</h1>

A
<% just_yield do %>
B
<% 1 # ブロックが文字列を返却するとテンプレートに出力されてしまうので、文字列以外を返す
   end %>
C

2009-05-02

[][] 世界のナベアツ問題をMoose::Roleで解く 16:02

最近、Perlの勉強を始めて Moose というモジュールが面白かったので、これを使って「世界のナベアツ問題」を解いてみました。

「世界のナベアツ問題」とはこれです。

1 から 40 までの数をプリントするプログラムを書け。 ただし 3 の倍数または数に 3 が含まれる時はその数に続けて、「〜〜〜」をプリントし、8 の倍数の時はその数に続けて「ぅぅういえぇえあ」をプリントすること。

世界のナベアツ問題

これを普通にPerlで書くと、こんな感じになると思います。

use strict ;

sub print_number {
    my ($n) = @_ ;

    print $n ;

    print "〜〜〜" if (($n%3 == 0) || ($n =~ /3/)) ;
    print "ぅぅういえぇえあ" if ($n%8 == 0) ;

    print "\n" ;
}

print_number($_) for 1..40 ;

これだと、print_number という関数の中に、以下のような仕様が全部埋め込まれています。

  • まず数字を表示する
  • アホになる条件(3 の倍数または数に 3 が含まれる時)
  • アホになった時に付加する文字列(「〜〜〜」)
  • 快感になる条件(8 の倍数の時)
  • 快感になった時に付加する文字列(「ぅぅういえぇえあ」)

もちろんこんな短いプログラムでは問題になりませんが、実際のプログラムでは、ひとつの関数の中に多くの処理を詰めこんでしまうと、解読、修正、再利用が難しくなります。また、多人数で同時に開発する場合に、コミュニケーションのミスが発生します。

そこで、仕様をなるべく分割し、かつ、ドメイン(適用業務)に近い記述で表現するのが、オブジェクト指向です。Perlにもその機能があるし、Mooseも基本はそれを拡張したものです。

しかし、一般的なオブジェクト指向言語では、クラスという形で分割された仕様を実装し、継承という形でそれらをつなげることが必要です。そして、継承に関連して、プログラミングのレベルでいろいろ難しい問題が発生して、個々のクラスを独立させることが難しくなってしまいます。

Mooseには「ポストモダンなオブジェクト指向」というスローガンがあって、Role という記述の単位によって、継承のわずらわしさを無くそうとしています。

たとえば、「ある条件の時だけ、数字の出力後に特定の文字列を付加する」という処理ををMoose::Roleで書くとこうなります。

package Nabeatu::Role::Aho ;
use Moose::Role ;

has 'aho_string' => (
    is => 'rw' ,
    isa => 'Str',
    required => 1,
    ) ;

requires 'aho_condition' ;

after 'print_number' => sub {
    my ($self, $n) = @_ ;

    print $self->aho_string if $self->aho_condition($n) ;
};

Roleは単独では動作しません。必ず、一般のクラスに「混ぜて」使います。その使う側のクラスに対して、「〜〜〜」を付加するというような、特定の機能を提供します。

MooseのRoleが優れていると私が考えるのは、その使う側のクラスに対して、「このRoleを使いたかったら、こういう機能を用意しろ」ということを表現できることです。この Role では、以下のことを要求しています。

  • aho_stringという文字列の属性に値を設定すること
  • aho_conditionというメソッドを用意すること
  • print_numberというメソッドを用意すること

そして、print_numberが実行された時に、クラス側の処理の後に、aho_conditionが成立しているかどうかをチェックして、それが真の時にだけ、aho_stringを表示します。

同様に、別の条件で「気持ちいい」みたいな文字列を付加する Role を書くとこうなります。

package Nabeatu::Role::Ecstasy ;
use Moose::Role ;

has 'ecstasy_string' => (
    is => 'rw' ,
    isa => 'Str',
    required => 1,
    ) ;

requires 'ecstasy_condition' ;

after 'print_number' => sub {
    my ($self, $n) = @_ ;

    print $self->ecstasy_string if $self->ecstasy_condition($n) ;
};

それで、この二つのRoleされた要求を満たすクラスを書けばいいわけですが、ここでは、さらにもう少し工夫して、この要求を満たすRoleを作成してみます。

package Nabeatu::Role::Conditions ;
use Moose::Role ;

has 'aho_number' => (
    is => 'rw' ,
    isa => 'Int',
    required => 1,
    ) ;

has 'ecstasy_number' => (
    is => 'rw' ,
    isa => 'Int',
    required => 1,
    ) ;


sub dividable_by {
    my ($m, $n) = @_ ;
    return ($m%$n) == 0 ; 
}

sub has_number_character {
    my ($m, $n) = @_ ;

    return $m =~ /$n/ ;
}

sub aho_condition {
    my ($self, $n) = @_ ;

    return dividable_by($n, $self->aho_number) || has_number_character($n, $self->aho_number) ;
}

sub ecstasy_condition {
    my ($self, $n) = @_ ;

    return dividable_by($n, $self->ecstasy_number) ;
}

この Roleは、aho_conditionとecstasy_conditionという二つのメソッドを提供しますが、その代わりに、aho_numberとecstasy_numberという二つの属性を要求します。つまり、この「○で割りきれる」という基準となる数字を、使うクラスの側で設定できるようにします。

そして、上記の3つのRoleを使って、ナベアツを表現するクラスを書くとこうなります。

package Nabeatu ;
use Moose; 
with qw(Nabeatu::Role::Aho Nabeatu::Role::Ecstasy Nabeatu::Role::Conditions) ;

sub BUILDARGS {
    my ($class, %args) = @_ ;

    $args{aho_string} ||= "〜〜〜" ;
    $args{ecstasy_string} ||= "ぅぅういえぇえあ" ;
    $args{aho_number} ||= 3 ;
    $args{ecstasy_number} ||= 8 ;

    return $class->SUPER::BUILDARGS(%args) ;
}

sub print_number {
    my ($self, $n) = @_ ;

    print $n ;
}

after 'print_number' => sub {
    my ($self, $n) = @_ ;

    print "\n" ;
};

my $nabeatu = Nabeatu->new ;
$nabeatu->print_number($_) for (1..40) ;

実行結果

1
2
3〜〜〜
4
5
6〜〜〜
7
8ぅぅういえぇえあ
9〜〜〜
10
11
12〜〜〜
13〜〜〜
14
15〜〜〜
16ぅぅういえぇえあ
17
18〜〜〜
19
20
21〜〜〜
22
23〜〜〜
24〜〜〜ぅぅういえぇえあ
25
26
27〜〜〜
28
29
30〜〜〜
31〜〜〜
32〜〜〜ぅぅういえぇえあ
33〜〜〜
34〜〜〜
35〜〜〜
36〜〜〜
37〜〜〜
38〜〜〜
39〜〜〜
40ぅぅういえぇえあ

BUILDARGSは特別なメソッドで、Mooseによるインスタンス生成の途中で呼ばれるメソッドです。ここで適切な初期値を設定します。

インスタンス生成(new)の後で初期値を設定しようとすると、Roleから「○○の属性が無い」と言って怒られてしまいます。それで、生成が完了する前に、属性値を設定する必要があります。

無駄に長くなっているだけのように思われるかもしれませんが、本体のクラスとRoleを別の人が作成していると考えてみてください。両者に行き違いがあると、動きだす前に、Mooseがチェックしてくれます。

特に、requiresで要求したメソッドが無い時は、コンパイル時にチェックされます。たとえば、Nabeatuクラスの

with qw(Nabeatu::Role::Aho Nabeatu::Role::Ecstasy Nabeatu::Role::Conditions) ;

を次のように変更するとエラーになります。

with qw(Nabeatu::Role::Aho Nabeatu::Role::Ecstasy) ;

このエラーは次のように表示されます。

'Nabeatu::Role::Aho|Nabeatu::Role::Ecstasy' requires the methods 'aho_condition' and 'ecstasy_condition' to be implemented by 'Nabeatu' at /usr/local/share/perl/5.10.0/Moose/Meta/Role/Application.pm line 59
        Moose::Meta::Role::Application::apply('Moose::Meta::Role::Application::ToClass=HASH(0x8bfda68)', 'Moose::Meta::Role::Composite=HASH(0x8bfda38)', 'Moose::Meta::Class=HASH(0x8bb7158)') called at /usr/local/share/perl/5.10.0/Moose/Meta/Role/Application/ToClass.pm line 18
        (中略)
        Moose::with('Nabeatu::Role::Aho', 'Nabeatu::Role::Ecstasy') called at moose_nabeatu.pl line 79

「'aho_condition' と 'ecstasy_condition'という二つのメソッドが実装されてない」と言っていますが、重要なことは、この最後の 「line 79」 が、今、変更した withの行を指していることです。

このエラーは、print_numberが呼ばれる前、Nabeatuのインスタンスが生成される前に検出され、そこでプログラムの実行が止まるのです。

ということは、Nabeatu->new が実行された時点で、Nabeatuが使用している 3つのRoleが要求している条件は(Mooseで記述可能できちんと書かれている限りは)全て満たされていることが保証されるわけです。

多人数で大規模なプログラムを開発したり、オープンソースのモジュールを多く使うような場合は、全部組み合わせた時に、整合性が取れてないという問題が起こりがちです。Perlのように動的な言語では、特にそれが問題になりますが、Mooseを使うと、静的型言語に近いレベルで、そういう問題をチェックできます。

処理系に全ソースを読ませた時点でかなりのことがわかり、必要なインスタンスを生成した時点でほとんどのエラーがチェックされます。

それともう一つ面白いことは、Role を個別のインスタンスに対して動的にアサインできることです。

たとえば、アホになったりしない真面目なナベアツを作ってみます。

package Nabeatu::Dynamic ;
use Moose; 
with qw(Nabeatu::Role::Conditions) ;

has 'aho_string' => (
    is => 'rw' ,
    isa => 'Str',
    ) ;

has 'ecstasy_string' => (
    is => 'rw' ,
    isa => 'Str',
    ) ;

sub BUILDARGS {
    my ($class, %args) = @_ ;

    $args{aho_number} ||= 3 ;
    $args{ecstasy_number} ||= 8 ;

    return $class->SUPER::BUILDARGS(%args) ;
}

sub print_number {
    my ($self, $n) = @_ ;

    print $n ;
}

my $nabeatu_d = Nabeatu::Dynamic->new ;
$nabeatu_d->print_number(32) ; # ただの 「32」

これは、「〜〜〜」とか「ぅぅういえぇえあ」とか、ふざけたことを言わずに、数字だけを表示する真面目なナベアツです。

そして、このナベアツに Aho の Roleをアサインします。

$nabeatu_d->aho_string('〜〜〜') ;
Nabeatu::Role::Aho->meta->apply($nabeatu_d) ;

$nabeatu_d->print_number(32) ; # 32〜〜〜

さらに、EcstasyのRoleをアサインします。

$nabeatu_d->ecstasy_string('ぅぅういえぇえあ') ;
Nabeatu::Role::Ecstasy->meta->apply($nabeatu_d) ;

$nabeatu_d->print_number(32) ; # 32〜〜〜ぅぅういえぇえあ

つまり、このナベアツは、インスタンスごとに違うふるまいをさせることができるわけです。Aho抜きで「32ぅぅういえぇえあ」と言うナベアツを作ることもできます。

これは、JavaScriptのような Prototypeベースのプログラミングに近いと思いますが、この場合でも、Roleが要求している条件のチェックは行なわれます。

Roleが必要としているメソッドや属性が無いと、applyの時点でエラーになります。JavaScriptで同様のことを行なった場合には、(よほど工夫しないと)実際にprint_numberが実行されるまで、その不整合は検出されないと思います。

Mooseは、普通のクラスベースの使い方もできるし、「ポストモダン」なRoleによって、継承の木構造を浅くする(無くする)ような設計もできるし、プロトタイプベースで動的にふるまいを変えるようなインスタンスもできます。AOPっぽい機能についても他にもいろいろあるようです。

Mooseは、この本の理論的枠組みを元にしているそうです。

The Art of the Metaobject Protocol (MIT Press)

The Art of the Metaobject Protocol (MIT Press)

この考え方は、CLOSの基盤にもなっていて、かなり歴史のあるものです。

こういうメタプログラミングで言語を拡張してどんどん便利にしていこうという試みは、他にも多く行なわれていますが、理論的基盤がしっかりしてないと、メタな機能をあれこれ使った時に矛盾が生じて破綻していまいます。Moose はそういう意味でも安心して使える感じがしました。

それと、、Mooseとは関係ないですが、Perlの練習としてついでに、関数的なナベアツも書いてみました。こちらはあんまりうまく行かなかったけど、おまけとして付けておきます。

use strict;
use utf8;

my $3の倍数の時 = の倍数の時(3) ;
my $3がつく数の時 = がつく数の時(3);
my $8の倍数の時 = の倍数の時(8);
my $あほ =  の時だけ(または($3の倍数の時,  $3がつく数の時), "〜〜〜");
my $気持ちいい = の時だけ($8の倍数の時, "ぅぅういえぇえあ");

my $数字をそのまま = sub  { shift };
my $改行 = sub  { "\n" };

print map {
    my $n = $_ ;
    map {
        $_->($n) # 一つの数字に4つの関数を順番に適用して配列を作る
    } (
        $数字をそのまま,
        $あほ,
        $気持ちいい,
        $改行
    ) 
} 1..40; # ...という処理を1から40まで繰り返して配列の配列を作る

sub の倍数の時 { 
    my ($n) = @_ ;
    return sub {  
        my ($i) = @_ ;
        return $i%$n == 0;
    }
};

sub がつく数の時 { 
    my ($n) = @_ ;
    return sub {  
        my ($i) = @_ ;
        return $i =~ /$n/ ;
    }
};

sub または {
    my ($a, $b) = @_ ;
    return sub {
        my ($i) = @_ ;
        $a->($i) or $b->($i) ;
    }
};

sub の時だけ {
    my ($condfunc, $text) = @_ ;
    return sub {
        $condfunc->(shift) ? $text : "" ;
    }
}

2008-03-04

[] Amrita2-2.0.2リリース 15:47

Amrita2-2.0.2をリリースしました。

プログラムの変更は、GetTextに関する小さなバグ修正二点だけですが、合わせて ザリガニが見ていた...。さんが書いてくださった以下の Amrita2 に関するエントリを英訳して、Amrita2のWikiに掲載させていただきました。

開発者の視点と実際のユーザの視点はズレてしまいがちなので、実際に使用された方の紹介というのは、ユーザにとって非常にわかりやすいものになるのではないかと思います。

id:zariganitosh さんのブログは、以前から私も、Railsについての勉強で参考にさせていただいていましたが、これで少しでもAmrita2を使う人が増えたらいいな、と思っています。ただ、いつもながら私の英語がヒドいので、そこが心配な所ですが。

なお、今回のリリースで修正した、Gettext回りのバグについての指摘は省かせていただきました。

2008-02-27

リポジトリに謎の障害 15:47

rmindを公開しているリポジトリでおかしな現象が発生しました。

環境、構成

  • debian etch
  • subversion バージョン 1.4.2 (r22196)
  • apache 2.2.3-4+etch1, ldap認証
  • リポジトリへのアクセスはhttp

リポジトリのサーバは、Xen + drbd + OpenVPNという仮想だらけの環境ですが、数十日間連続稼動しており、他には特に問題がないので、この件には関係ないと思います。

予兆

しばらく前から、commitの時に以下のようなエラーが出ていました

  • general server errorというようなメッセージで異常終了
  • checksum errorというようなメッセージで異常終了

出るタイミングは不定で、再度(そのまま)、commitをするとそのまま直る場合と、何度やっても同じエラーが出る場合がありました。

この時は次の手順で復旧しました。

  1. ソースを退避
  2. 問題のソースを含むディレクトリを削除
  3. svn update
  4. 退避したソースを上書き
  5. svn commit

本来はここでしっかり調査すべきでしたが、この手順で復旧できていたので、リポジトリでなくワークファイル側の問題だと判断していました。

現象の確認

本日、リポジトリ(内の特定のソース)が完全におかしくなっていることに気がつきました。

  1. svn checkout
  2. ソースを修正 commit
  3. 別ディレクトリにcheckout
  4. 2の内容が反映されていない

問題となっているソースは、rmind/app/model/user.rb です。

原因

今の所、全く不明です。

対策

現状のリポジトリを保存後、新しいリポジトリを作成し、最新のソースをチェックインします。

2008-02-14

[] 「ザリガニが見ていた...。」さんのAmrita2紹介記事 15:47

「ザリガニが見ていた...。」さんでAmrita2についての解説記事を書いていただきました。

「ザリガニが見ていた...。」さんのRailsについてのエントリは参考になるものが多くて、私もレビュアブルマインドを開発する時や、現在行なっているRails2.0対応の作業で参考にさせていただいています。

そういうブログでAmrita2について書いていただけたことは、本当にありがたいことです。特に、Rails(ERB)に慣れている方からの視点でAmrita2について書いてもらうと、開発している側にはわからないことがいろいろ見えてきます。

そこで、この3つの記事に対して、補足、感想等を書かせていただきます。

インストールについて

id:zariganitoshさんは、tarボールを vendor/plugin に展開して使用されているようですが、UsingWithRailsに書いてあるように、gemでインストールすることもできます。

$ sudo gem install amrita2
$ cd (アプリケーションルート)
$ mkdir vendor/plugins/amrita2
$ cp /usr/lib/ruby/gems/1.8/gems/amrita2-2.0.1/init.rb vendor/plugins/amrita2/

/usr/lib/ruby/gems/1.8/gemsは、環境によって違いますが、gemsがインストールされているディレクトリです。

ERbからの段階的な移行

最初からAmrita2のすべてを理解する必要はなく、便利だと思う機能から利用していけば良いのだ。

Amrita2でビューはさらにシンプルに! - ザリガニが見ていた...。

これは、まさに私が意図していたことで、従来のERbテンプレートから少しづつ段階的に移行していけるように工夫しています。

次の例のように(これはanalyticsのjsコードを挿入している所)、<<%< というマークから段を下げた部分は、ERbテンプレートと同じように扱われます。

    <<%<
      <script type="text/javascript">
        var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
        document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
      </script>
      <script type="text/javascript">
        var pageTracker = _gat._getTracker("<%= $rmind_config[:google_analytics_account] %>");
        pageTracker._initData();
        pageTracker._trackPageview();
      </script>

CDATAで囲んだ所も同様にERbとして扱われます。

だから、Amrita2の仕様やエラーメッセージがよくわからない場合は、すぐに、ERbに戻ってそこだけERbで書くことができます。

出力ソースのインデント

ブラウザの描画結果はまったく同じなのだが、やはりHTMLソースも改行されて欲しい...。

Amrita2でビューはさらにシンプルに! - ザリガニが見ていた...。

Amtita2の出力結果は、はっきり言ってインデントがおかしくて、人間が見るには見づらいものになっています。

後の記事にありますが、これを補うには、tidy等の外部コマンドをフィルターとして組みこんでインデントしてください。

次のようにすると、デバッグ時のみインデントされた出力を行なうことができます。プロダクション環境では、余分なコマンド呼び出しを行ないません。

(config/environment.rb)
if ENV['RAILS_ENV'] == "development"
  IndentByTidyIfDebug = Amrita2::Filters::CommandFilter['tidy -q -xml -indent -utf8']
else
  IndentByTidyIfDebug = nil
end
(layout/application.html.a2)
<<html :| IndentByTidyIfDebug <
  <<head<
    <title>Reviewable Mind</title>

ただし、tidyのインデントは、railsの javascript_tag の出力を壊してしまうようで、javascriptが正しく実行されないことがあります。(他に適当なHTMLのプリティプリンタが無いか物色中です)

表記述の仕様

ここは(ここも)、説明が完全に不足していた所ですが、仕様を読み取ってわかりやすくまとめていただけました。

  • 「|」で区切った範囲が、tdタグの1セルと解釈される。
  • 「||」で区切った左側で属性を指定すると...
    • その行はtdタグの属性値の指定になる。
    • 属性値の指定が無ければ、半角スペースで埋めておけば良い。
  • 上下のハイフンの連続は、無くてもOK。
  • 「|」の間隔も最低1スペースあればOK。(インデントを揃えておかないと、この書式の意味は無いが。)
  • ちなみに、「||」の右側の「|」を「||」に置き換えるとthタグと解釈された。
  • td、th以外の目的で「|」を利用するときは、エスケープ「\|」しておく必要がある。

まったくその通りです。

この記法は激しく好みが分かれる所のような気がしますが、次のようにフォームと組み合わせた場合等は、有効ではないかと思います。

次の例は、レビュアブルマインドのサインアップフォームです。

  <%
      signup_form = amrita_define_form(:user, :action=>:login) do |f|
        f.text_field :login
        f.password_field :password
        f.add_field_element :password_confirm, password_field_tag(:password_confirm, "")
        f.text_field :email, :size=>50
        f.add_field_element :terms_of_use, check_box_tag(:terms_of_use, false)
        f.add_field_element :link_to_terms_of_use, link_to('terms of use', '/term.html')
      end
   %>

  << :signup_form | AcceptData[:hook] <
    <<two_columns_form<
      <<<---------------------------------------------------------------
        ||| Login ID:            | <<:login>>                          |
      <<<---------------------------------------------------------------
        ||| Password:            | <<:password>>                       |
      <<<---------------------------------------------------------------
        ||| Password(confirm):   | <<:password_confirm>>               |
      <<<---------------------------------------------------------------
        ||| E-Mail:              | <<:email>>                          |
      <<<---------------------------------------------------------------
        ||| Terms of use:        | <<:terms_of_use>> &#160;            |
        |||                      | I have read and agree to the &#160; |
        |||                      | <<:link_to_terms_of_use>>           |
      <<button_bar  <---------------------------------------------------
        <<:| Join[:nbsp]<
          %= submit_tag _('Signup')
          %= link_to _('Login'), :action => 'login'

two_columns_formは、マクロという機能を使用して、tableタグに置き代わりますが、ここに直接 table タグを指定してもかまいません。

ちなみに、この例では、lableタグを後から埋めこむためにマクロを使用しています。


Rubyブロック内のルール

# ブロック内にRuby式以外が存在するとエラー。
# NG
% 3.times do
  <p>
    index.html.a2
  </p>
% end

# ブロック内がすべてRuby式なら大丈夫。
# OK
% 3.times do
  %= "<p>"
    %= "index.html.a2"
  %= "</p>"
% end

行頭の%の記述ですが、ここは、ちょっと特殊な処理を行なっています。

  • 行頭に%が続く限り、(インデントを無視して)一つのCDATAブロックとしてまとめる(後にERbとして一括処理)
  • 行頭が%=だったら、その1行分の結果を出力する処理に変換

この記法は単発の短い処理を書くのには適していますが、ブロックを使う処理や、複数行にまたがる処理をERbで書く時は、次の書き方の方がおすすめです。

<<%<
  <% 3.times do %>
    <p>
      index.html.a2
    </p>
  <% end %>

[] GetTextへの自動対応 11:07

これは、Amrita2の最も重要な機能と言えるかもしれません。Amrita2では、ほとんど自動的にテンプレートを国際化することができます。

Gettext用のフィルターがあってこれを組み込むと、テンプレート内の文字列に対して、自動的に_()を呼び出す処理を行います。

require "amrita2/template"
require "amrita2/gettext"
include Amrita2

tmpl_text =  <<-END
<<html<
  <<body<
    <<h1 :title |Attr[:title, :body]>>
    <<p <
      Amrita2 is a html template libraly for Ruby.
END

tmpl = Amrita2::Template.new(tmpl_text) do |e, src, filters|
  filters << Amrita2::Filters::GetTextFilter.new
end

tmpl.set_trace(STDOUT)
tmpl.text_domain = 'test'
puts tmpl.render_with(:title=>{:title=>"hello world", :body=>"hello world" })

このサンプルから出力されたコンパイル済みソースを抜粋すると、次のようになっています。

__stream__.concat("<html><body>")
__stream__.concat(XXTitleInstance.render_with($_.amrita_value("title"), __binding__))
__stream__.concat("<p>")
__stream__.concat(_("Amrita2 is a html template libraly for Ruby.") % $_)
__stream__.concat("</p></body></html>")

"Amrita2 is a ..."という文字列が、_()呼び出し後に出力されています。全ての文字列に対して自動的にこの処理が組み込まれます。

Rails用のブリッジでは、自動的にこのフィルターを組み込んでいますので、Rails上では、何もしないで国際化対応になります。

次のRakeタスクを実行すると、テンプレート上の全ての静的な文字列を国際化対象の文字列としてpoファイルを生成します。

task :updatepo do
  $: << 'vendor/plugins/amrita2/lib'
  require 'gettext/utils'
  require 'amrita2/gettext'
  require 'amrita2/macro'

  GetText.update_pofiles("rmind", 
                         Dir.glob("{app,config,components,lib}/**/*.{rb,rhtml,a2html,a2}"),
                         "rmind 1.0.0" 
                         )
end

レビュアブルマインドは、この機能を利用して国際化しています。

zariganitoshzariganitosh 2008/02/15 18:45 私の拙い日記の疑問にタイムリーに回答を頂きまして、ありがとうございます。
Tidyの件、<<%<記法、素晴らしいです。今までの悩みが一気に解決しました。これで簡単に.a2ファイルに変更することができます。(form_forについては、Railsのbuilderオプションを活用したいと考えていました。)

GetTextへの自動対応の件ですが、自分のMacBookのRails2.0.2環境では、以下の現象に悩んでいます。
-.a2ファイルが翻訳文字列の抽出対象にならないようです。
→「|| File.extname(file) == ’.a2’」を追記すると、抽出されるようになりました。
-<!-- -->コメントタグがあると、エラーが発生します。
→分かりませんでした。悩み中です。
  rake aborted!
  undefined method `get_erb_source’ for #<Amrita2::Core::CommentNode:0x2138b00>

(ちゃんと調べきれていないので、自分の環境設定に問題がある可能性も...。)
既に抽出済みの翻訳文字列については、自動対応で処理されます。とても快適です!
(すいません、本来はサポートページに書くべきことかもしれませんが、ここにコメントさせて頂きました。)

amrita2amrita2 2008/02/27 20:25 せっかくのご報告を長い間放置してすみませんでした。
レビュアブルマインドの開発が一段落したら対応しようと思っていた所に、2/27の所に書いた問題が発生してしまいました。
問題が解決したら、この件について調査して回答します。おそらくバグだと思います。

amrita2amrita2 2008/03/04 17:56 Gettextの件は二点ともバグで、2.0.2で修正しました。
ご指摘ありがとうございました。