CGIKit2によるリファレンスマニュアル作り

バックナンバー

著者: speakillof <speakillof at yahoo dot co dot jp>

CGIKit2によるリファレンスマニュアル作り

皆さん RDoc を使っていますか? ご存知の方も多いでしょうが、RDoc というのは Ruby スクリプトのコメントに書かれた文章からリファレンスマニュアルを生成するツールです。RDoc は Ruby に標準添付されているので、リファレンスマニュアルを作るために多くの人が利用しています。でも、生成される HTML にはフレームがたくさん付いていて、見にくいと感じる人が多いのではないでしょうか?

RDoc のフレームがうっとおしいという意見

私も見やすいリファレンスマニュアルが欲しいと思う一人です。そこで、この記事では CGIKit と RDoc (というか ri ) を使ってオリジナルのリファレンスマニュアルを作成します。CGIKit の紹介をかねているので、この記事で作るサンプルは解説をしやすくするためシンプルなものにします。

対象読者

  • RDoc が生成する HTML を見やすくしたい人
    • Windows 用には rdoc の f オプションで chm を使った方が幸せになれます。
  • CGI の GET/POST について理解している人
  • 前回のCGIKit 2.x の記事 を読んだ人、もしくは、CGIKit の Wiki のチュートリアルを5章まで読んだ人

必要なもの

  • Ruby-1.8.2
    • 標準添付されている WEBrick、 REXML、 RDoc、 ri
  • cgikit-2.0.0-preview1
  • サンプルのソース ri2ref_sample.zip

インストールの方法については 前回の記事 や CGIKit の Wiki を参照してください。

自分で RDoc の文法を解釈するのー?

それは面倒なので、今回は ri のデータを利用することにします。

情けないなー。ところで、ri って何?

ri は refe と同じくコマンドラインからリファレンスマニュアルを検索するツールです。ri は RDoc の一部であり、ri のデータは RDoc を使って生成されます。ri について詳しく知りたい場合は http://www.ruby-lang.org/ja/man/?cmd=view;name=ri を参照してください。

ri のインストール

通常、ruby のインストールが終わると、一緒に ri がインストールされます。ri のある場所は ruby と同じディレクトリ(フォルダ)です。何も設定せずに Ruby One Click Installer をインストールした場合は

 C:\Program Files\ruby\bin\ri

に ri があります。Unix 系 OS をお使いの人は

 $ which ri

として、どこに ri がインストールされているか確かめてみてください。

ri のリファレンスマニュアルのデータのインストール

ri がインストールされていても、ri のデータが無いことがあります。

 (rubyのインストールパス)/share/ri
 (ホームディレクトリ)/.rdoc

に ri のデータがあるか確かめてみましょう(他のディレクトリにインストールされていることもあります)。

環境設定で変化しますが、Windows で Ruby One Click Installer を使っている人は上のそれぞれのディレクトリが

C:\Program Files\ruby\share\ri
C:\Documents and Settings\(ユーザー名)\.rdoc

になります。

もし、 ri のデータがない場合は Ruby のソースを取ってきて

$ tar xvzf ruby-1.8.x.tar.gz
$ cd ruby-1.8.x
$./configure --prefix=/hogehoge/
$ make
# make install
# make install-doc

として下さい($はユーザーを、#はrootを表します)。コンパイルする環境があればこれで /hogehoge/share/ 以下に ri のデータがインストールされますので、上記のディレクトリにコピーしてください。

コンパイルする環境が無い人は

$ rdoc --ri (ディレクトリ)

とすることで ri 用のリファレンスマニュアルのデータが作られます。例えば、CGIKit なら下のようにします。

$ rdoc --ri cgikit-2.0.0-preview1/lib/

これで自分のホームディレクトリの下に CGIKit の ri のデータがインストールされます。

riの動作確認

ri のデータがインストールされているか試してみましょう。

$ ri Array
$ ri Hash#[]
$ ri CGIKit::Application

ri のデータへのアクセス

今回作成するリファレンスマニュアルには ri のデータを使うわけですが、そのためには ri の データにアクセスする必要があります。RDoc には ri のデータにアクセスするためのクラス( rdoc/ri/ri_reader.rb の RI::RiReader )が用意されており、この記事ではそのクラスをラッピングしたライブラリを使用します。

ファイルの名前は ri2ref_utils.rb です。CGIKit と関係がないので、実装についてはすべて省略し、各メソッドについては後ほど紹介します。ri2ref_utils.rb

メソッドとかクラスとかの情報はこれで手に入るけど、どうやってページを作るのさ

今回はダイレクトアクションという機能を使います。ダイレクトアクションには下のような利点があります。

  • 処理が軽い
  • CGIのGET/POSTになれている人には処理の流れが分かりやすい
  • ページへのブックマークが可能になる

最後の利点は 前回の記事 で言われた文句のうち

  • URL に変な文字列がある

を解決することになります(前回のアルバム CGI で見られた URL の変な文字列というのは session ID、 context ID と呼ばれるものです)。

ダイレクトアクションを使うことで CGIKit の幾つかの機能が使いにくくなりますが、今回の記事の範囲ならダイレクトアクションを使っても困ることはありません。

ダイレクトアクションって何さー。変な言葉使うなよー。

簡単に言うと、アクセスされた URL と Ruby のメソッドを結びつけることを言います。例えば、

 http://www.hogehoge.com/hoge.cgi

が CGIKit の CGI スクリプトとします。この時に

 http://www.hogehoge.com/hoge.cgi/d/Hoge/foo

にアクセスすると、Hoge というクラスの foo_action というメソッドが実行されます。URL の /d/ というのはダイレクトアクションを使うという目印です。URL の /d/ の次の部分の Hoge はクラス名、その次の foo が呼び出されるメソッド名を示します。この URL に対して Hoge#foo_action メソッドが対応することから分かるようにaction という接尾語が付いていないメソッドは呼び出すことができません。ダイレクトアクションの詳細については CGIKit の Wiki に説明がありますので、そちらに任せます。

例がないと分かんないー

ダイレクトアクションを使った簡単な例を紹介します。

RI2Reference/hoge/hoge.rb

require 'cgikit'
require 'webrick'
require 'cgikit/webrick'

class HogePage < CGIKit::Component
  attr_accessor :title
  
  def init
    @title = 'Hoge'
  end
  
end

class FooPage < CGIKit::Component
  
  attr_accessor :body
  
  def init
    @body = 'FOO'
  end
  
end

class HogeAction < CGIKit::DirectAction
  
  def hoge_action
    a = page(HogePage)
    a.title = a.title + ' ' + Time.now.strftime('%Y%m%d') 
    a
  end
  
  def foo_action
    a = page(FooPage)
    a.body = a.body + ' ' + rand(10).to_s
    a
  end
  
  alias default_action hoge_action
  
end



if $0 == __FILE__
  port = (ARGV.shift || 8080).to_i
  
  app = CGIKit::Application.new
  app.direct_action_class = HogeAction
  
  server = WEBrick::HTTPServer.new({:Port => port})
  server.mount('/', WEBrick::CGIKitServlet::ApplicationHandler, app)
  
  trap("INT"){ server.shutdown }
  server.start  
end

各ページの HTML や ckd ファイルは省略します。この例では hoge.rb に HogePage、 FooPage というページと HogeAction というダイレクトアクションが定義されています。WEBrick用に作ってあるので、試しに RI2Reference/hoge に移動して

 ruby hoge.rb

で実行して http://localhost:8080/ にアクセスしてみてください。

…エラーになりましたね? CGIKit::Application にダイレクトアクションの設定をしていない場合、/d/を付けないと CGIKit はダイレクトアクションを使用しません。そのため http://localhost:8080/ にアクセスすると、 CGIKit::Application#main に設定されたページを表示させようとします(このサンプルの場合はMainPage)。残念ながらこのサンプルには MainPage が定義されていませんので、エラーメッセージが表示されます。

次に http://localhost:8080/d/HogeAction/hoge にアクセスしてみましょう。今度は HogePage が表示されますね。 /d/ の付いた URL にアクセスがあると、CGIKit が該当するメソッドを判定してそのメソッドを呼び出します。この場合は HogeAction#hoge_action ですね。

 def hoge_action
   a = page(HogePage)
   a.title = a.title + ' ' + Time.now.strftime('%Y%m%d')
   a
 end

HogeAction#hoge_action では CGIKit::DirectAcion#page を使って HogePage というページを生成します。このオブジェクトが HogeAction#hoge_action の返値になるので、HogePage が表示されます。ダイレクトアクションの返値は CGIKit によって表示されます。

今度は http://localhost:8080/d/HogeAction/foo にアクセスしてみましょう。予想したとおりになったでしょうか? この場合は HogeAction#foo_action が実行されて FooPage が表示されます。

では、 http://localhost:8080/d/HogeAction にアクセスしてみてください。メソッド名が指定されていないので、エラーになると考えた人が多いのではないでしょうか? メソッドの指定がない場合や該当するメソッドがない場合、CGIKit は default_action を呼びます。このサンプルでは HogeAction#default_action は HogeAction#hoge_action のエイリアスですので、 HogePage が表示されます。

最後は http://localhost:8080/d/foo にアクセスして下さい。FooPage が表示されますね。 CGIKit::Application#direct_action_class にダイレクトアクションのクラスを設定すると、CGIKit はクラス名の指定がない時に CGIKit::Application#direct_action_class に指定されたクラスを使います。

なんか前と書き方が違うよー。混乱するー。

うーん。今のところ CGIKit で URL に Session ID、 Context ID を入れない方法はこの方法しかありません。現時点では諦めてください…。

うーん、仕方がない。我慢するよー。で、ページの構成は?

主なページは 4 つあります。それぞれの名前は

  • ClassListPage
  • ClassPage
  • MethodPage
  • SearchResult

です。ClassListPage は ri にあるクラス・モジュールの一覧を表示し、 ClassPage はクラス・モジュールの情報を、MethodPage はメソッドの情報を表示させます。また、URL から検索可能にするため SearchResult に検索結果を表示させます。

ファイルはー?

大雑把に下のようになります。

RI2Reference
  │
  ├─ lib
  │    │
  │    ├─ application.rb
  │    │
  │    ├─ directaction.rb
  │    │
  │    └─ ri2ref_utils.rb
  │
  ├─ components
  │    │
  │    └─ ここに多数のファイル
  │         ClassListPage、 ClassPage、 MethodPage、 SearchResult など
  │
  ├─ www
  │    │
  │    └─ ref.css
  │
  ├─ cgikitconf.rb
  │
  ├─ RI2Reference.rb(WEBrick)
  │
  ├─ RI2Reference.cgi(CGI)
  │
  └─ RI2Reference.rbx(mod_ruby)

components 以下のファイルを省いてありますので、完全な構成を知りたい場合はサンプルをダウンロードしてください。ri2ref_sample.zip

全部手で書いていくの?

前回紹介した ckproject を使用すると、少し楽ができます。cgikit-2.x.x に CGIKit のソースがあると仮定すると、

 ruby cgikit-2.x.x/bin/ckproject RI2Reference

で、上とよく似た構成のファイル/ディレクトリが出来上がります。 足りないものは components 以下のファイルと lib/ri2ref_utils.rb、 www ディレクトリです。余分なものは lib/session.rb、 resources ディレクトリです。 余分なファイル/ディレクトリは削除しても構いません。

簡単にそれぞれのファイル/ディレクトリの紹介をします。

lib/application.rb RI2Reference::Application が定義されています。このクラスは CGIKit::Application を継承していますが、特にいじる所はありません。今回は CGIKit::Application の代わりに RI2Reference::Application を使います。
lib/directaction.rb RI2Reference::DirectAction があります。これは CGIKit::DirectAction の子クラスで、今回のダイレクトアクションのためのクラスです。これから作るページはこのクラスから呼ばれます。
lib/ri2ref_utils.rb ri のデータを扱うための RI2Reference::Utils が定義されています。
components ディレクトリ ClassListPage、 ClassPage、 MethodPage、 SearchResult などのファイルがあります。
www/ref.css 今回使用する CSS ファイルです。
cgikitconf.rb CGIKit の設定ファイルです。
RI2Reference.rb、 RI2Reference.cgi、 RI2Reference.rbx 起動用ファイルです。それぞれ WEBrick、 CGI、 mod_ruby 用です。今回は WEBrick しか使いません。ちなみに mod_ruby は CVS の CGIKit でしか使えませんので、注意して下さい。

いろいろ設定するー

実行ファイルの設定

今回も WEBrick で開発を行います(CGI 用のスクリプトは省略します。前回の内容を踏まえて御自分でいじってみて下さい)。それでは ckproject によって生成された RI2Reference.rb を変更しましょう。

#!/usr/local/bin/ruby

# RI2Reference.rb [port]

$LOAD_PATH.unshift('lib')
require 'webrick'
require 'cgikit-all'
require 'cgikit/webrick'
require 'application'
require 'directaction'

port = (ARGV.shift || 8080).to_i

app = RI2Reference::Application.new
app.load_all_components('./components')
app.load_configuration('./cgikitconf.rb')

app.direct_action_class = RI2Reference::DirectAction
app.default_request_handler = app.direct_action_request_handler

server = WEBrick::HTTPServer.new({:Port => port})
server.mount('/', WEBrick::CGIKitServlet::ApplicationHandler, app)
server.mount('/www', WEBrick::HTTPServlet::FileHandler, './www')

trap("INT"){ server.shutdown }
server.start

最初に 18・19行目の

app.direct_action_class = RI2Reference::DirectAction
app.default_request_handler = app.direct_action_request_handler

を追加します。これはダイレクトアクションの設定です。1つ目は今回使用するダイレクトアクションがどのクラスかということを CGIKit::Application に伝えます。二つ目は…おまじないと思っていて下さい。

もう一つ追加する部分は23行目です。

server.mount('/www', WEBrick::HTTPServlet::FileHandler, './www')

これは css ファイルを読み込むための設定です。変更しなくても見栄えは悪いですが、動きます。それと、ファイルの先頭にある

 require 'session'

を削っておきましょう。

修正する部分はこれでおしまいですが、見なれない部分があるので、補足しておきます。

 app.load_all_components('./components')
 app.load_configuration('./cgikitconf.rb')

15行目の CGIKit::Application#load_all_components は 指定されたディレクトリの .rb をすべて require します。これでいちいち require しなくても良くなりますが、.rb と名前が付けば何でもかんでも require するので、注意が必要です。16行目の CGIKit::Application#load_configuration は設定ファイルの読み込みです。

この修正をした RI2Reference.rb を実行すると、 http://localhost:8080/ に WEBrick のサーバーが起動します。/www 以下に アクセスがある場合はその URL に該当するファイルが www ディレクトリから探索されます。 /www 以外の URL にアクセスすると、RI2Reference::DirectAction によって 生成されたページが表示されます。

cgikitconf.rb の編集

cgikitconf.rb の最初に

 require 'ri2ref_utils'

の一行を加えます。これでどこからでも RI2Reference::Utils を使うことができます。それと @main への RI2Reference::MainPage を代入する部分をコメントにします。今回は CGIKit::Application#main を使いませんし、 RI2Reference::MainPage がないので、この行無効にしないとエラーになってしまいます。

   def configure_component

     # Main page:
     # If session ID or context ID aren't specified, this component is shown.
     #@main = RI2Reference::MainPage

他にも色々設定することが出来ますが、今回は説明しません。

肝心のダイレクトアクションってどうするの?

上で HogeAction の例を見せましたが、同じように CGIKit::DirectAction を継承したクラスを書いてそこにメソッドを定義します。今回定義するダイレクトアクションのクラスは RI2Reference::DirectAction です。そこに 4 つのメソッドを定義します。クラスを置く場所は lib/direcaction.rb です。

require 'ri2ref_utils'

module RI2Reference

  class DirectAction < CGIKit::DirectAction
    
    def list_action
      pg = page(ClassListPage)
      pg.list = RI2Reference::Utils.get_all_class_names
      
      pg
    end
    
    def class_action
      pg = page(ClassPage)
      full_name = CGIKit::Utilities.unescape_url( request.form_value('n') )
      pg.setup( RI2Reference::Utils.get_class_description(full_name) )
      
      pg 
    end
    
    def method_action
      pg = page(MethodPage)
      full_name = CGIKit::Utilities.unescape_url( request.form_value('n') )
      pg.setup( RI2Reference::Utils.get_method_description(full_name) )
      
      pg
    end
    
    # gorgeous implementation
    def search_action      
      keys = request.form_values.keys
      if keys.empty?
        list_action
      else
        keyword = CGIKit::Utilities.unescape_url( keys[0].to_s ) 
        results = RI2Reference::Utils.search_name(keyword)        
        all_classes = RI2Reference::Utils.get_all_class_names
        
        methods = results - all_classes
        classes = results & all_classes
        
        pg = page(SearchResult)
        pg.method_list = methods
        pg.class_list = classes
        pg
      end
    end
    
    alias default_action list_action
        
  end

end

ダイレクトアクションによって呼び出されるのはページではなくメソッドです。紹介した4つのページはすべてダイレクトアクションを通じて CGIKit によって表示されます。URL、 メソッド、 表示されるページの関係は下のようになります。

URL Method ページ
http://localhost:8080/d/list list_action ClassListPage
http://localhost:8080/d/class class_action ClassPage
http://localhost:8080/d/method method_action MethodPage
http://localhost:8080/d/search search_action SearchResult

各ページの遷移先は下のようになりますが、各ページの内容を見ないと下のように遷移するという事は分からないと思います。

ClassListPage ────> ClassPage ─────> MethodPage
(list_action)       (class_action)       (method_action)


 SearchResult  ─┬─> ClassPage
(search_action)  │  (class_action)
                 │
               └─> MethodPage
                  (method_action)

では、それぞれのメソッドについて見てみましょう。

list_action(default_action)

ダイレクトアクションのメソッドの指定がない場合や無効なメソッドが指定された場合に実行されるメソッドです。 CGIKit::DirectAction#page (実装は前回使った CGIKit::Component#page と同じ) を呼び出して ClassListPage を生成し、それに表示したいクラス・モジュールの一覧を ClassListPage#list に設定します。後は CGIKit が ClassListPage を表示してくれます。

表示したいクラス・モジュールの一覧は RI2Reference::Utils.get_all_class_names で取得します。実行例は下のようになります。

p RI2Reference::Utils.get_all_class_names
["Abbrev", "Array", ...  "Zlib::VersionError", "Zlib::ZStream"]

method_action、 class_action

今回は http://localhost:8080/d/class?n=Array のように GET を用いてクエリーを渡し、どのメソッド・クラス・モジュールを表示したいのかを決定します。そのためプログラム側でクエリーに指定されたメソッド・クラス・モジュール名を取得する必要があります。前回のようにダイレクトアクションを使わない場合は、この部分を CGIKit が行ってくれるのですが、今回は自分でやらなければなりません。

GET/POST の値は CGIKit::Request に格納されており、CGIKit::DirectAction (CGIKit::Component) からは request メソッドを使って CGIKit::Request のオブジェクトにアクセスすることが出来ます。取得した CGIKit::Reuqest オブジェクトに対して CGIKit::Reuqest#form_value を使うことで GET で指定された値にアクセスすることが出来ます。

例えば、http://localhost:8080/d/class?n=Array に対して request.form_value(‘n’) を実行すると、’Array’ という文字列を得ることが出来ます。この値は URL エンコードされていますので、CGIKit::Utilities.unescape_url を使って元の文字列に戻します。

次に、得られた名前に対応する ri のデータを取得します。この役目は RI2Reference::Utils.get_{class、method}_entry が担当します。取得した ri のデータは ClassPage、 MethodPage のオブジェクトの setup というメソッドで設定します。

ClassPage#setup、 MethodPage#setup については後ほど説明します。RI2Reference::Utils.get_{class、 method}_entry についてもそこで紹介します。

search_action

URL の「?」の後ろの文字列、つまり、GETによるクエリーのデータに対して検索を行います。例えば、 http://localhost:8080/d/search?push にアクセスされた時は「push」に対して検索を行います。

すでに述べたように CGIKit::Request#form_value でクエリーのデータにアクセスすることができますが、この方法ではクエリーデータのキーにはアクセスできません (上の例では push がクエリーデータのキーです)。

そこで、クエリーデータの元に近い形のオブジェクトを使うことにします。 CGIKit::Request#form_values にはクエリーデータが Hash のデータとして格納されているので、32行目で

keys = request.form_values.keys

としてクエリーデータのキーのすべてを取得します。 もし、検索ワードが指定されていれば keys は空の配列になりません。 次に36行目で

keys[0].to_s

として「push」に相当する文字列を取得します(多分 to_s はなくても大丈夫です)。37-41行でその文字列に一致するメソッド名の配列を methods に、 クラス・モジュール名の配列を classes に格納しています。

search_action で使われている RI2Reference::Utils.search_name(str) は str を含む メソッド・クラス・モジュール名を検索し、その結果を配列として返します。

 p RI2Reference::Utils.search_name('String')
 ["Kernel#String",
  "PathnameTest::AnotherStringLike",
  "String",
  "String::new",
    ...
  "StringScanner#terminate",
  "StringScanner#unscan",
  "StringScanner::Error"]

また、 RI2Reference::Utils.get_all_names は ri にある メソッド・クラス・モジュールの名前一覧です。

p RI2Reference::Utils.get_all_names
["Abbrev","Abbrev#abbrev", "ArgumentError", "Array", "Array::[]", ... , "Zlib::ZStream#total_out"]

この後の処理は CGIKit とは直接関係がないので、ふーんと思っていて下さい。ちなみにこの実装は富豪的です。検索文字列に一致する項目が見つけたら、 SearchResult にその結果を設定します。

34行目は検索する文字列が指定されていない場合の処理です。この時は ClassListPage を表示させます。list_action の返す値は ClassListPage なので、そのまま list_action を実行するだけです。

そろそろページを作ろうよ。最初は ClassListPage ?

そうですね。じゃあ、最初は ClassListPage にしましょうか。

module RI2Reference
  
  class ClassListPage < CGIKit::Component
  
    attr_accessor :i, :list
    
    def query
      {'n' => CGIKit::Utilities.escape_url(@i) }
    end
  
  end
  
end

{
  :rep => {
    :element => Repetition,
    :list => :list,
    :item => :i,  
  },
  
  :class_link => {
    :element => Link,
    :string => :i,
    :action_class => RI2Reference::DirectAction,
    :direct_action => 'class',
    :query => :query,
    :session_id => false
  },
  
}

ClassListPage.html list.jpg

このページは RI2Reference::DirectAction#list_action によって表示されるページです。ここでは ri から取得できるクラス・モジュールの一覧を表示します。ユーザーはこのページを基点として各マニュアルを参照します。

ri のクラスやモジュールの名前

ri で取得できるすべてのクラス・モジュールの名前は RI2Reference::Utils.get_all_class_names で取得できます。すでにそのリストが @list に格納されていますので、これを使って各クラス・モジュールのページへのリンクを張ります。@list のクラス・モジュール名を CGIKit::Repetition の item 属性で ClassListPage の @i に格納し、リンクを繰り返し表示させます。

今回はHTMLに目印を付けるために ckid 属性を用いています。繰り返されるのは下の HTML の <li> … </li> です。<a ckid=’class_link’> が各クラス・モジュールへのリンクになります。

<div ckid='rep'>
  <li> <a ckid='class_link' /> </li>
</div>

この辺りの処理が理解しづらい人は CGIKit の Wiki を参照してください。

クラス・モジュールへのリンク

リンクの生成には前回同様 CGIKit::Link を使います。ソースを見てみましょう。

 :class_link => {
   :element => Link,
   :string => :i,
   :action_class => RI2Reference::DirectAction,
   :direct_action => 'class',
   :query => :query,
   :session_id => false
 },
 def query
   {'n' => CGIKit::Utilities.escape_url(@i) }
 end

前回は href 属性を使いましたが、今回は action_class、 direct_action、 query、 session_id 属性が使われていますね。

action_class にはダイレクトアクションに使用するクラスを指定します。今回は RI2Reference::DirectAction ですね。direct_action は呼び出したいメソッド名です。ここにはメソッド名から action を除いた部分を指定します。ClassListPage からはクラスのマニュアルを表示させるので、指定する値は “class” となります。

query 属性は クエリーに指定する値のペア(Hashのオブジェクト)です。ここで指定した値は CGIKit::Link が生成する URL に付加されます。

例えば

 query => {'hoge' => '123'}

とすると、URL には

 ...#hoge=123

という GET のクエリーデータが付加されます。query の値は CGIKit 側で自動的に URL エンコードされます。先ほど述べたようにここで指定した値が RI2Reference::DirectAction#class_action で参照されます。

前回は一般的な CGI スクリプトで使用される GET/POST を使いませんでしたが、これは CGIKit がセッションや GET/POST の部分を自動的に処理してくれたからです。今回のようにダイレクトアクションを使用する場合はセッションや GET/POST の処理が自動的には行われませんので、query 属性等を使って自分で GET/POST を処理しなくてはなりません。

session_id 属性は URL にsession ID を付加するかどうかを決定します。これを有効にすると URL に変な文字列が付いてしまうので無効にします。無効にするには session_id 属性に false を指定します。

この設定で

 <li> <a ckid='class_link' /> </li>

の部分は

 <li> <a href='http://localhost:8080/d/class?n=Array' /> </li>

のようになります。先ほども述べましたが、/d/ の後ろにクラスの指定がない場合は CGIKit::Application#direct_action_class に指定したクラスが使用されます。今回の場合は RI2Reference::DirectAction ですね。上の URL にアクセスすると、 RI2Reference::DirectAction#class_action が実行されることになります。

次は順番どおりにいくと、ClassPageだね

ClassPage の基本構成は、

  • デザインはすべてCSSで行う
  • クラス・モジュール名を先頭に 概要・スーパークラス・includeされているモジュール・定数・インスタンスメソッド・アトリビュート・クラスメソッド を表示する
  • 各クラス・モジュール・メソッドにはリンクを張る

とします。

先にページを構成する3つのファイルをお見せします。先ほどの CGIKit::Link のダイレクトアクションの使い方が分かれば難しいところはそれほど無いと思います。

ClassPage.rb

{

  :is_class => {
    :element => Conditional,
    :condition => :class_page?,
  },
  
  :is_module => {
    :element => Conditional,
    :condition => :class_page?,
    :negate => true,
  },
  
  :class_name => {
    :element => String,
    :value => :full_name,
  },
  
  :class_comment => {
    :element => String,
    :value => :class_comment,
    :escape => false,
  },
  
  
  
  :superclasses => {
    :element => Repetition,
    :list => :superclasses,
    :item => :mod
  },
  
  :included_modules => {
    :element => Repetition,
    :list => :included_modules,
    :item => :mod,
  },
  
  :module_link => {
    :element => Link,
    :string => :module_name,
    :action_class => RI2Reference::DirectAction,
    :direct_action => 'class',
    :query => :module_query,
    :session_id => false,
    
    :class => 'module_link',
  },
  
  
  :constants => {
    :element => Repetition,
    :list => :constants,
    :item => :con,
  },
  
  :constant_name => {
    :element => String,
    :value => :constant_name
  },
  
  #:constant_value => {}
    
  
    
    
  
  :ims => {
    :element => Repetition,
    :list => :instance_methods,
    :item => :i,
  },
  
  :attrs => {
    :element => Repetition,
    :list => :attributes,
    :item => :i,
  },
  
  :cms => {
    :element => Repetition,
    :list => :class_methods,
    :item => :i,
  },
  
  
  
  :instance_method_link => {
    :element => Link,
    :string => :method_name,
    :action_class => RI2Reference::DirectAction,
    :direct_action => 'method',
    :query => :method_query,
    :session_id => false,

    :class => 'method_link',
  },
  
  :attribute_link => {
    :element => Link,
    :string => :method_name,
    :action_class => RI2Reference::DirectAction,
    :direct_action => 'method',
    :query => :method_query,
    :session_id => false,

    :class => 'method_link',
  },
  
  :class_method_link => {
    :element => Link,
    :string => :method_name,
    :action_class => RI2Reference::DirectAction,
    :direct_action => 'method',
    :query => :class_method_query,
    :session_id => false,

    :class => 'method_link',
  }
}

ClassPage.html class.jpg

RI2Reference::ClassPage#setup

RI2Reference::DirectAction で出てきた setup です。これは与えられた引数を HTML として表示させるために簡単なデータ変換を行います。変換されたデータはインスタンス変数に格納されて、データを表示するときに参照されます。setup メソッドの引数は RI2Reference::DirectAction によって決定されます。その引数の entry は full_name 対応する ri のデータです。

setup によって設定された ClassPage のインスタンス変数と ri のデータとの対応関係は下のようになります。

@full_name 表示されるページのクラス・モジュール名
@superclasses スーパークラスの名前一覧
@included_modules includeされているモジュール名一覧
@constants 定数
@instance_methods インスタンスメソッド
@attributes アトリビュート
@class_methods クラスメソッド

各変数には実際に何が格納されているのか見てみましょう。

RI2Reference::DirectAction で呼び出された RI2Reference::Utils.get_class_entry(full_name, plural = false) は full_name に対応するクラス・モジュールの情報を返します。対応する情報がない場合は nil が返ります。plural は full_name に対応するクラス・モジュールが複数あった場合にそのうちのひとつを返すのかすべてを返すのかを決めます。

p RI2Reference::Utils.get_class_entry('Array')
#<RI::ClassDescription:0x10656060 .... >

返値は RI::ClassDescription のオブジェクトです。このオブジェクトには クラスやモジュールの説明が格納されており、下のようなメソッドがあります。ClassPage のインスタンス変数に格納した値はこれらのメソッドの返値です。

full_name
クラスやモジュールの名前です。
p entry.full_name
"CGIKit::Application"
comment
クラス・モジュールの説明です。ここには HTML のタグが含まれているので、RI2Reference::Utils.struct2html でこのメソッドの返値を HTML に変換します。この記事ではcommentメソッドには直接アクセスしません。
superclass
クラスのスーパークラスです。モジュールでは空文字が返値となります。
includes
include しているモジュールの配列です。使用方法は attributes と同じです。
constants
クラス・モジュールに定義されている定数の配列です。
attributes
attr_{reader、 writer、 accessor} で定義されたメソッドが格納されています。このメソッドの返値は配列で、配列内のオブジェクトに対して name でメソッド名にアクセスできます。
p entry.attributes[0].name
"foo"
class_methods
クラス・モジュールに定義されたクラスメソッドの配列です。attributes と同様に配列内のオブジェクトに対して name を使うことでメソッド名にアクセスできます。
p entry.class_methods[0].name
"new"
instance_methods
クラス・モジュールに定義されているメソッド名の配列です。使用方法は attributes と同じです。

ClassPage.html のクラス・モジュール名の部分には余分なものが付いてるよー

表示するページによってクラス名だったりモジュール名だったりするので、それに合わせて表記を「class」や「module」に変えなくてはいけません。そのために if 文に相当する CGIKit::Conditional を使ってクラス名の場合には「class」を、モジュール名の場合には「module」を表示させます。

<div id='page_name'>
<div ckid='is_class'> <span class='title'>class</span> </div>
<div ckid='is_module'> <span class='title'>module</span> </div>
<span ckid='class_name' />
</div>
 :is_class => {
   :element => Conditional,
   :condition => :class_page?,
 },

 :is_module => {
   :element => Conditional,
   :condition => :class_page?,
   :negate => true,
 },
 def class_page?
   (not(@superclasses.empty?) or @full_name == 'Object')
 end

表示するページがクラスの場合、Object 以外にはスーパークラスがあるはず(将来は違うのかな…)なので、 RI2Reference::ClassPage#class_page? を上のように定義します。

次に ckd の設定です。is_class、 is_module の目印の付いた部分に CGIKit::Conditional を設定します。:condition が true になると目印の付いた部分は表示され、false になると表示されません。is_class、 is_module は全く正反対の動作をさせたいので、is_module の方に negate属性 を true にします。negate 属性が true だと、condition 属性が true の時に内容が表示されず、false の時に表示されるようになります。

「概要」の部分は変だー

概要の部分には RI2Reference::Utils.struct2html を使っているんですよね…。ri のデータには簡単な HTML のタグが含まれています。そのため CGIKit のエレメントを使ってその内容を表示させるのは少々難しいのです。仕方が無いので RI2Reference::Utils.struct2html を使って、CGIKitに頼らずに HTML を生成します。RI2Reference::Utils.struct2html(entry) の引数には get_method_entry、 get_class_entry の 返値を渡します。このメソッドは再帰呼び出しを使って ri から得られたデータを HTML にするので、分かりにくいかもしれません。

RI2Reference::Utils.struct2html で生成された HTML は CGIKit::String を使って ClassPage.html の中に埋め込みます。前回の記事でも書きましたが、CGIKit::String の value 属性に 「<」「>」のような文字が含まれると、CGIKit::String は自動的にその部分を escape します(つまり、「<」は「&lt;」に、「>」は「&gt;」に変換されます)。そのため HTML を埋め込む場合はその処理を無効にしておかないと意図したように表示されません。これを回避するためには CGIKit::String の escape 属性に false を指定します。

 :class_comment => {
   :element => String,
   :value => :class_comment,
   :escape => false,
 },

残り

残った部分は前回までの説明でほとんど理解できるはずです。御自分で色々考えてみて下さい。

1点だけ説明を追加しておきます。

 :instance_method_link => {
   :element => Link,
   :string => :method_name,
   :action_class => RI2Reference::DirectAction,
   :direct_action => 'method',
   :query => :method_query,
   :session_id => false,

   :class => 'method_link',
 },

この部分です。最後に class 属性が使われていますが、これは CGIKit::Link には無い属性です。CGIKit では各エレメントに定義されていない属性を使用すると、その属性は HTML の属性とみなされます。

これにより上の場合は

 <a href="http://localhost:8080/d/method?n=Mutex%2523lock" class="method_link">lock</a>

のように HTML の a タグに class 属性が設定されることになります。クラス属性は CSS でデザインする時に用いるので動作そのものには関係有りませんが、覚えておくと応用が利きます。

MethodPage は ClassPage とよく似てる?

そうですね。よく似ています。

MethodPage.rb

{
  :method_name => {
    :element => String,
    :value => :full_name,
  },
  
  :parameter => {
    :element => String,
    :value => :parameter,
  },
  
  :visibility => {
    :element => String,
    :value => :'entry.visibility',
  },
  
  :aliases => {
    :element => String,
    :value => :aliases,
  },
  
  :method_comment => {
    :element => String,
    :value => :method_comment,
    :escape => false,
  },
  
}

MethodPage.html method.jpg

MethodPage#setup は分かりますね。単にインスタンス変数に必要な情報を代入しているだけです。setup の引数である entry は RI2Reference::Utils.get_method_entry の返値です。

RI2Reference::Utils.get_method_entry(full_name, plural = false) は full_name に対応するメソッドの情報を返します。対応する情報がない場合は nil が返ります。plural は get_class_entry と同じ役割です。このメソッドの実行例を下に示します。

p RI2Reference::Utils.get_method_entry('Array#push')
#<RI::MethodDescription:0x10754e68 ... >

返値は RI::MethodDescription のオブジェクトです。RI::ClassDescription と同様にメソッドに関するいくつかの情報が含まれています。MethodPage に配置されたエレメントから RI::MethodDescription のメソッドを呼び出すことでメソッドのリファレンスを作成します。各メソッドの役割は下のようになります。

full_name
メソッド名です
p entry.full_name
"Array#push",
aliases
alias されているメソッド名の一覧です。
is_singleton
特異メソッドかどうかを示します。返値は true/false
params
メソッドの引数をあらわします。
p entry.params
"array.push(obj, ... )   -> array\n",
visibility
メソッドの可視性をあらわします。
p entry.visibility
"public"
comment
RI::ClassDescription#comment と同じです。

HTML の visibility の目印のところには見慣れない記述がありますね。

 :visibility => {
   :element => String,
   :value => :'entry.visibility',
 },

value 属性に :’entry.visibility’ という Symbol オブジェクトが設定されていますが、これは self.entry.visibility を呼び出すことを意味します( self は RI2Reference::MethodPage のオブジェクトとします)。 Ruby の文法では「:」の後ろに文字列を指定することができ、 CGIKit は Symbol オブジェクトの文字列の部分を評価して、属性の値とします。 上の場合であれば CGIKit は

 :'entry.visibility'

'entry.visibility'

に変換し、

eval('entry.visibility')

のようにして評価します。 そして、その結果が value 属性の値になります。これによって

<p>Visibility: <span ckid='visibility' /> </p>

は CGIKit::String エレメントにより self.entry.visibility の値に変換され、

<p>Visibility: public</p>

のようになります。

SearchResult…もう説明する気がないんじゃない?

…おっしゃるように説明しません。今まで見てきたページと構成はほとんど変わりませんので、御自分で考えてみて下さい。

module RI2Reference

  class SearchResult < CGIKit::Component
    
    attr_accessor :class_list, :method_list, :i
    
    def query
      {'n' => CGIKit::Utilities.escape_url(@i)}
    end
    
  end
  
end

{
  :classes => {
    :element => Repetition,
    :list => :class_list,
    :item => :i,  
  },
  

  :methods => {
    :element => Repetition,
    :list => :method_list,
    :item => :i,  
  },

  :class_link => {
    :element => Link,
    :string => :i,
    :action_class => RI2Reference::DirectAction,
    :direct_action => 'class',
    :query => :query,
    :session_id => false
  },
  
  :method_link => {
    :element => Link,
    :string => :i,
    :action_class => RI2Reference::DirectAction,
    :direct_action => 'method',
    :query => :query,
    :session_id => false
  },

}

SearchResult.html search.jpg

じゃあ、もっと違うページになるようにしろよー

うーん、この記事で使うサンプルは各ページがシンプルな構成になるように書いています。あまり複雑なことをすると、読みにくくなりますからね。

実行

今回は WEBrick だけですので、RI2Reference のディレクトリで

 ruby RI2Reference.rb

として

等にアクセスして色々試してください。

最後に

使いやすいリファレンスマニュアルが欲しいということで作ったのですが、

  • クラスのページにメソッドの簡単な概要が欲しい。
  • クラスの継承関係を一覧できるようにしたい
  • クラス一覧が無味乾燥
  • サイドメニューが欲しい
  • デザインがへちょへちょ。アイコンが欲しい(筆者は絵心無し)

などなど不満が色々とあります。この中で1つ目は何とか対処したいのですが、今の RDoc では各メソッドの概要を記述することが出来ないので対応が難しいです。

宿題

  • CGI 用のスクリプトの設定をしてみましょう。
  • CGI で実行すると CSS ファイルが上手く読み込めない場合があります。どうやって回避しますか?
  • サイドメニューを作ってみましょう。

参考文献

エレメントリファレンスやダイレクトアクションの説明などがあります。

リファレンスマニュアルの構成を考える際に以下のサイトを参考にしました。

著者について

某サービス業をしています。エンタープライズアプリケーション1を使って仕事をしています。

記事のリリース直前に Frameless RDoc template が出て、へこんだのは内緒です。

  1. 誰かこのアプリを rewrite してくれません?いやマジで。