#contents *はじめに [#lda23308] 今回はtDiaryのプラグインの仕組みを読んでみたいと思います。といっても今までtDiaryを読んだことがないので1から読んでいきたいと思います。 なお、今回読んだtDiaryのバージョンは2.1.4です。 * index.rb [#hc454ba3] 何はなくともindex.rbを見てみましょう。早速興味深いコードがあります。 #pre{{ if FileTest::symlink?( __FILE__ ) then org_path = File::dirname( File::readlink( __FILE__ ) ) else org_path = File::dirname( __FILE__ ) end $:.unshift( org_path.untaint ) require 'tdiary' }} $:はファイルのロードパスです。そこにindex.rb((例えば複数の日記を書く場合に実体はどこかに置いといて各日記のディレクトリにはindex.rbとupdate.rbのシンボリックリンク、とtdiary.confを置くという運用も考慮されているのがすばらしいところです))が置かれているディレクトリを登録することでカレントディレクトリがどこから実行されてもindex.rbと同じディレクトリにあるtdiary.rbをロードできるようにしています。((カレントディレクトリにtdiary.confがない場合エラーになりますが)) 次にTDiary::Configオブジェクトを作ることでtdiary.confのロードを行っています。さらっと長そうと思ったのですがまた興味深いコードがありました。TDiary::Config.initializeメソッドにおいて次のコードがあります。 #pre{{ instance_variables.each do |v| v.sub!( /@/, '' ) instance_eval( <<-SRC def #{v} @#{v} end def #{v}=(p) @#{v} = p end SRC ) end }} インスタンス変数からgetterメソッドとsetterメソッドを動的に生成しています。う〜ん、Rubyならではですね。 その後、渡された引数に応じてTDiary::TDiaryBaseから派生したクラスのオブジェクトを作成、eval_rhtmlメソッドを呼ぶことで出力を作っているようです。 *TDiary::Plugin [#e66cb375] さて、eval_rhtmメソッドl(実体はprotectedなdo_eval_rhtmlメソッド)ですが初めにプラグインをロードしているようです。というわけでその部分を見てみましょう。 #pre{{ Dir::glob( "#{plugin_path}/*.rb" ).sort.each do |file| plugin_file = file load_plugin( file ) @plugin_files << plugin_file end }} pluginディレクトリは00default.rb, 05referer.rb, 10spamfilter.rbという「数字2桁+名前.rb」でファイルが置かれているわけですがsortをかけることで必ず番号の小さいファイルから読まれるようにしています。globが番号の小さい順にしてくれる保証はありませんからね。参考になります。 個々のプラグインは次のコードで読み込まれています。 #pre{{ def load_plugin( file ) @resource_loaded = false begin res_file = File::dirname( file ) + "/#{@conf.lang}/" + File::basename( file ) open( res_file.untaint ) do |src| instance_eval( src.read.untaint, "(plugin/#{@conf.lang}/#{File::basename( res_file )})", 1 ) end @resource_loaded = true rescue IOError, Errno::ENOENT end File::open( file.untaint ) do |src| instance_eval( src.read.untaint, "(plugin/#{File::basename( file )})", 1 ) end end }} 興味深いところは2つあります。言語名ディレクトリにあるプラグインファイル名と同名のリソースファイルを読み込んでいるところとプラグインファイルの内容をinstance_evalしているところです。表示メッセージなどの文字列を別ファイルにすることで言語を切り替えやすくなります。また、instance_evalすることで各プラグインはいちいち #pre{{ module TDiary class Plugin def foo ... end end end }} としなくても #pre{{ def foo ... end }} とすればよいことになります。 *50sp.rb [#v07a2e0f] さて、上のコードではtdiaryディレクトリ直下のpluginディレクトリにあるプラグインしか読み込まれません。しかし、現在のtDiaryにはmisc/pluginディレクトリにあるプラグインから使いたいプラグインを選択するという機能があります。これはどのように実現されているのでしょうか?どうやらその仕事をしているのは50sp.rbのようです。50sp.rbの最後に以下のコードがあります。 #pre{{ # Finally, we can eval the selected plugins as tdiary.rb does if sp_option( 'selected' ) then sp_option( 'selected' ).untaint.split( /\n/ ).collect{ |p| File.basename( p ) }.sort.each do |filename| @sp_path.each do |dir| path = "#{dir}/#{filename}" if File.readable?( path ) then begin load_plugin( path ) @plugin_files << path rescue Exception raise PluginError::new( "Plugin error in '#{path}'.\n#{$!}" ) end break end end end end }} @data_path/tdiary.confを見るとoptions2配列にsp.selectedというキー名で選択したプラグインが列挙されていることがわかると思います。@sp_pathは通常[misc/plugin]なので、これで選択したプラグインが読み込まれることになります。 ところで、いつ@data_path/tdiary.confを読んでいるんだろう?と疑問に思っていたのですが、カレントディレクトリのtdiary.confの最終行に、 #pre{{ load_cgi_conf }} という行があります。これにより、カレントディレクトリのtdiary.confを読むと同時に@data_path/tdiary.confも読むということを行ってるようです。 それではdo_eval_rhtmlメソッドに戻りましょう。do_eval_htmlメソッドはプラグインを読み込んだ後、HTMLを生成するrhtmlをERBで処理しています。