#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のシンボリックリンクを置くという運用も考慮されているのがすばらしいところです。))が置かれているディレクトリを登録することでカレントディレクトリがどこから実行されてもindex.rbと同じディレクトリにあるtdiary.rbを読み込めるようにしています。
 $:はファイルのロードパスです。そこに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で処理しています。
 
 

トップ   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS