Archive for November 2007
メールはどうするかっていうのもあるけど,そこまで国際化ツールに期待すると機能が肥大化してしまいそう。
その前段階としてフォームをどうするかで悩み中。
まず,現状のNucleusはNucleusのパーサーを使ってフォームを生成しています。これをどうするか。
今のRails版のアプローチでは,そのまま移植するのが一番楽ですが,フォーム部分はスキンと独立しているので,Nucleusの<%,%>を使う必然性はありません。なので,この部分はRailsのビュー機能を使い,Partial Renderingで入れていくのがいいのかなと思っています(それで動くかどうかは未テスト)。
それともう1つ,国際化をどうするかという問題があります。RailsのWikiの国際化方法の比較ページを見ていたのですが,決めかねています。世間的にはgettextの評価が高いのかと思っていますが,gemでインストールしなければいけないのがちょっと面倒なのと,「gettextでなくちゃ」というのがどこなのか,よく分かっていないというのがあります。
Railsのプラグインとして使える中では,DBに言語データを入れていくGlobalizeは面倒なので却下。なんとなくよさげかなあと思うのがGloc。ただ,Simple Localizationというのも導入のしきいが低そうなところは好感が持てます。とりあえずSimple Localizationを入れておいて,後から変えることを検討するというのが妥当なところかもしれません。
えーと,これだけでは何なので,今まで書いていなかった,基本的な処理の流れを紹介(といっても大したことはありませんが)。
まず,入ってきたURLを振り分けるのはroutes.rbの役割。ここはこんな感じです(デフォルトブログの場合)。
名前付きのルーティングもできるように,map.connectではなくて,各ルートに名前を付けています。
これを見て分かるように,このあたりは全部applicationというメインのコントローラが処理します。actionのところでスキンタイプごとに振り分ける形。
このほか,ポスト系を処理するactionのコントローラと管理用のadminコントローラ,rss用のコントローラを別途作っていく予定です。
これを受けるapplication.rbは今のところこんな感じ。グローバル変数を多用しているところがちょっと格好悪いのですが。
今,actionはindexとitemしかありませんが,itemの最後には「render :action=>'index'」というのが入っています。これは表示用のビュー・ファイルはindex.rhtmlを使うということ。全スキンで共通にしています。といってもビュー・ファイルの中身は「<%= parse %>」だけ。これを処理するのはapplication_helper.rbで,この中身もいたってシンプル。
スキン変数とテンプレート変数をちょこちょこと実装しています。相変わらずRubyのnilの扱いにはちょっととまどいがちですが,間違えるパターンが分かってきたので,バグ取りの時間は短くなってきたようです。
URL生成もできているのですが,気になるのはRails開発ブログでDeprecated Controller Methodsの中にurl_for(:#{options}) - use url_for with a named route directlyと書かれていること。
しかし,名前付きの呼び出し方 ...._url はprotectedなので直接コントローラから呼べないのと,_send_を使って無理やり送ってもちょっと動作が違うという問題があります。url_forを使って名前を指定する方法があるのかと,探しているのですが,見つかりません。
それなのにRailsでは文字化け。
$KCODEもdatabase.ymlのencodingも指定しているのになあ…
ほかに何かあったっけ?
全体の進捗を書いておくと,パーサの基本部分はできていて,後はスキンやテンプレートでparse_●●というのを書いていけばいい状態。次に与えられたURLに対して動作を振り分けるところを作っていて,それに対応するURL生成をテストしているところ。これがうまく行ってcreateLink系の部分ができると基本的にブラウズはできる形になります(検索除く)。
その後は,認証系を作り,コメントなどの更新系の作業に移る計画です。
●プログラムを書くのは夜中12時くらいから2時くらいまでなのですが,デバグなどしているとついつい時間オーバーしてしまいがち。朝7時に起きることを考えると3時までには寝るようにしないときついのですが。さらに,夜1回1時間くらい寝てしまってから,起きて作業をすると,目が冴えてしまって,今度は眠りに着くのが大変。夕べも結局睡眠時間が3時間くらいに。こうなると昼間の仕事に影響が出てしまうので,一回寝てしまった日はプログラム書かないとか,デバッグしないとか,ルールを決めないと体壊しそうです。
●Subversionの使い方が,相変わらずよく分かっていません。今はローカルにtrunkだのbranchだののツリーを作って,サーバーのリポジトリと同期させ,trunkの下だけをInstantRailsの下にコピーして作業しています。作業内容をツリーに入れるときに,どこを更新したかちゃんと覚えていないので,更新忘れのファイルなどが出現しがちです。それから,他の場所で更新したものを反映するときも,ローカルのツリーでどこが変わったのかを忘れてしまい,作業ディレクトリに反映し忘れてしまうことがあります。RadRails(Eclipse)にはSubversionの機能があるというのですが,使い方が分かっていません。作業ディレクトリから直接リポジトリと同期させるのでしょうか。
●このブログ,カテゴリーもちゃんと作っていないいい加減な構成。基本的に作業メモだからいいんだけど,実際には動作チェックもここのデータを使っているので,もうちょっとまじめに作っておかないとちょっと大変。MCやTagEXも入れようかなあ。あ,既に入っているのだけどつかってないだけでした。
それと並行して,各スキンタイプによる処理を加え始めているのですが,ちょっと迷い。
今のNucleusの場合,スキンタイプごとに,サポートしているスキン変数を配列に持っていて,それを参照して実行するかどうかを決めています。その部分の実装をどうするか。Nucleusと同じにしてもいいのですが,あまり格好よくありません。スキンタイプごとにサブクラスを作ってそこで対応するのがスマートですが,今度は複数のスキンタイプでサポートしているときにどうするか,という問題があります。
全部のタイプでサポートしているならば親クラスで定義してしまえばそれで済むのですが,そうでない場合,各スキンタイプに書くのは無駄です。Railsにはdelegateという機能があって,特定のメソッドをインスタンス変数に実行させることができますが,そのためだけに,他のスキンタイプ用のオブジェクトをインスタンス変数に持つのもどうなのか。まあ,それでもこれが一番きれいな気がするので,多分それにするでしょう。
これに関連してもう1つの実装として考えているのがパーサの処理部分をクラスではなくモジュールにしてしまって,コントローラにmix-inすること。これから先,Railsのコントローラで提供されている様々な機能(例えばキャッシュ)を利用していくためには,処理をなるべくコントローラ内でするのがいいのではないか,という発想です。
モジュールにした場合に機能的に問題があるのかないのか。その場合,サブクラスとして考えている分も,単なる別のモジュールという扱いになって,それぞれmix-inすることになります。一応それでも動きそうな気はするのですが,検証はしていません。まだ,モジュールの扱い方には慣れていないので,ちょっと危険な感じもします。
少し調べていたら,この問題の解決にはつながっていないのですが,そもそもこのPaginationの機能自体がなくなって,will_paginateあるいはpaginating_findプラグインを使うとのこと。will_paginateの方が標準推奨のようですが,吐き出すSQLがいけてないらしく,どちらを使うかちょっと悩ましいところです。
どちらもモデルに組み込む形なので,使うのは比較的簡単そうな気がします。ということで,これは先送り。
ということで,一応進捗はここまで。
画像サンプル
テンプレート変数など,まだ少ししか実装していないので<%,%>だらけですが,それでもタイトルや本文が表示されると,すこし格好が付きました。
リンク系をちゃんとしないと,トップページを表示する以外,何もできないので,次はコントローラのディスパッチの設定をして,それにあわせてURL生成をするという作業に移ります。
コードは暫定的なのばかりなので,特に載せませんが,テンプレートのパーツにアクセスする部分を変えたので,そこだけ書いておきます。
これでテンプレート・オブジェクトに対してtemplate['ITEM']みたいな形でパーツを取ってこれます。ブログを展開するとき,連続していくつかのパーツを使うため,一回のDBアクセスでそのテンプレートの内容を全部取り込むようにしました。Rubyは例外処理が簡単に書けるので,こういうのはやりやすいです(これくらいなら例外処理使わなくても構いませんが,この方が読みやすいと思います)。スキンの場合は,一回のアクセスで使うパーツはたいてい1つなので,今のところこの処理は入れていません。ただ,スペシャル・スキン・パーツを多用するようになってきたら,入れた方がいいかもしれません。
それから,パートのデータを返すところにわざわざto_sを付けているのは,そのパートのデータがないときに,nilの代わりに空文字列を返すためです。こうしておかないと,文字列にnilを連結させてしまいエラーを起こすことになります。
ちなみに
show variables like 'char%'
すると
character_set_client utf8
character_set_connection utf8
character_set_database utf8
character_set_filesystem binary
character_set_results utf8
character_set_server latin1
character_set_system utf8
とでます。サーバーのlatin1というのが怪しいのかなあ。
(default-character-set=utf8を設定したらutf8になりましたが文字化けは変わらず)
PHPにしろMySQLにしろ,文字コードの自動変換ほど余計なお世話な機能はないと思います。
こういうので作業が止まるのが一番嫌です。
RadRailsのエディタは括弧やクォーテーション・マークなど補完してくれるのですが,間違ってクォーテーション・マークを入れてしまい,すぐに戻って消そうとするとそこでハングします。昨日編集していたファイルの特殊事情なのかどうかは不明です。
後から考えたら2回ハングして時点で,RadRails使わず,テキスト・エディタに切り替えればよかったです。
知らなかったりして(^^;)。
スキンタイプと関連するパラメータ周りをまとめるとこのようになります(多分)。一番右のページングは,ページスイッチを付ける意味があるかどうか。ページスイッチの機能はコアに持たせる予定なので。
これ見ると,archivelistのところだけブログの指定方法が変則ですね。
URL周りはNucleusコアとの互換性はそれほど重視していません。FancyURLに似た形にはすると思いますが違うところも出てくるかもしれません(要は実装しやすい形でということで)。最終的にはCustomURLと互換にするつもりです。
注:archiveとarchivelistで逆になっているところなどがあったので修正しています。
テンプレートなどの細かい実装に移っているので,ようやく今のNucleusのコードを調べたりしています。
パーサ書いているときに気付いたのですが,今のNucleusのパーサーの作りってかなりいい加減。
実は<%と%>って全く区別されていないってご存知でしょうか。試しにスキン中で
%>parsedinclude(head.inc)<%
などと書いてしまっても問題なく動きます。今回のパーサはネストを実現したかったこともあり,そのあたりはきちんと見ています。
あと,一般にはNucleusのパーサってスキンとテンプレートの2段階だと思われていると思いますが,実際にはその下に,アイテム・レベルとイメージ,ポップアップ,メディア処理の2段階が入り,計4段階で動いています。Nucleus3.3ではプラグインのアイテム変数が利用できるようになっていますが,アイテムにはもともと<%image%>などを処理するためにパーサを適用していたので,そこを少し変えるだけでこの機能は追加できました(記憶になかったけど,この部分は僕が書いたコードが採用されているようです)。
最後の段階のイメージなどの処理はテンプレートの中のIMAGE_CODEなどの内容がパースされます。ただし,他の段階のようなフルレベルのパースではなく,同じ名前の変数と置き換えるくらいの単純なやり方です。ちなみにこのテンプレートを書き換えるとプラグインを使わなくてもLightBox.jsを呼び出す程度のものは実現できます。このあたりの自由度は,あまり活用されていない気がしますが,どうでしょう。
というわけで,このあたりの処理を,今は書いています。
ああっと,まだプラグイン呼び出すとこ書いてないけど,parse_●●がなかったときの処理としてrescue節に入れてあげればいいでしょう。そもそもまだプラグインのアーキテクチャを考えていないので,そこは後から加えます。
parser_base/lib/の下に/parser/base.rbを作ってそこに入れたところ,どこにもrequireなしでちゃんと動くようになりました。テストもリネームしないといけないけどそちらは未検証。
それと,parsedincludeの中からのparseが動かないというのもバグでした。
括弧とパラメータがないときの処理がおかしかったのが原因。Rubyは暗黙の型変換がないのでnilには結構悩まされます。
config.plugins = [ "prefixed", "parser_base", "default_parser" ]
などと書くことで動くことが判明。ここまでかなり不毛な時間を過ごしました。パーサーにもいくつかバグがあったので,それを取りつつ,いちおう動くようになってきました。
例えばDefaultParserのクラスは
としているのですが,ここでなぜかparsedincludeの中でパースしてくれません。self.parseの行を取るとincludeはできているのですが。ちょっと不思議。
aがnilの場合にa += b といった書き方ができないのが痛い。PHPだったら勝手に空文字列扱いにしてくれるのに。
パーサはネストありのものしか今のところ作っていませんが,ネストなしは簡単なので,まあいいでしょう。ただ,ちゃんとデバッグしてないです。正しくないフォーマットのものが来たら,問題起こす可能性大。無駄な検索をしていないので,実行速度は悪くないのではないかと思っています(ベンチマークはしてないけど)。
こういった書き方が正しいのかどうかはよく分かりませんが,これでConfigurations['DefaultBlog']といった形でアクセスできるようになりました。
まだ,プラグインなど残っているテーブルはありますが,すぐには必要ないので,コントローラとパーサに移りたいと思います。
PS. クラス名が長いのでCONFに修正しようと思います。
国際化プラグイン
Simple Localizationは,プラグインにも利用できることがわかったので,これを採用することにしました。あまり進んでいませんが
render :partialでフォームを貼り込めることは確認できました。国際化について補足
プラグインを国際化するのにも使えるかどうか。メールはどうするかっていうのもあるけど,そこまで国際化ツールに期待すると機能が肥大化してしまいそう。
さて,フォームをどうしようか
前に,認証,更新系の順に作業する予定と書きましたが,先に更新系をやろうかと思っています。その前段階としてフォームをどうするかで悩み中。
まず,現状のNucleusはNucleusのパーサーを使ってフォームを生成しています。これをどうするか。
今のRails版のアプローチでは,そのまま移植するのが一番楽ですが,フォーム部分はスキンと独立しているので,Nucleusの<%,%>を使う必然性はありません。なので,この部分はRailsのビュー機能を使い,Partial Renderingで入れていくのがいいのかなと思っています(それで動くかどうかは未テスト)。
それともう1つ,国際化をどうするかという問題があります。RailsのWikiの国際化方法の比較ページを見ていたのですが,決めかねています。世間的にはgettextの評価が高いのかと思っていますが,gemでインストールしなければいけないのがちょっと面倒なのと,「gettextでなくちゃ」というのがどこなのか,よく分かっていないというのがあります。
Railsのプラグインとして使える中では,DBに言語データを入れていくGlobalizeは面倒なので却下。なんとなくよさげかなあと思うのがGloc。ただ,Simple Localizationというのも導入のしきいが低そうなところは好感が持てます。とりあえずSimple Localizationを入れておいて,後から変えることを検討するというのが妥当なところかもしれません。
テンプレートのコメントの処理がよく分からない
Nucleusのテンプレートにおけるコメントの処理がよく理解できなくて,先に進めないでいます。なんだかややこしいことをやっているのは分かるのですが。えーと,これだけでは何なので,今まで書いていなかった,基本的な処理の流れを紹介(といっても大したことはありませんが)。
まず,入ってきたURLを振り分けるのはroutes.rbの役割。ここはこんな感じです(デフォルトブログの場合)。
# rules for the index page
map.blog '', :controller => "application", :action => 'index'
map.blogcategory 'catid/:catid', :controller => "application", :action => 'index'
# rules for the item page
map.item 'item/:itemid', :controller => 'application', :action => 'item'
map.itemcategory 'item/:itemid/catid/:catid', :controller => 'application', :action => 'item'
# rules for the archivelist page
map.archivelist 'archivelist', :controller => 'application', :action => 'archivelist'
# rules for the member page
map.member 'member/:memberid', :controller => 'application', :action => 'member'
# rules for the archive page
map.archive 'archive/:daymonth', :controller=> 'application', :action => 'archive', \
:requirements=>{:daymonth => /\d{4}-d{1,2}(-d{1,2})?/}
map.archivecategory 'archive/:daymonth/catid/:catid', :controller=> 'application', \
:action => 'archive', \
:requirements=>{:daymonth => /\d{4}-d{1,2}(-d{1,2})?/}
名前付きのルーティングもできるように,map.connectではなくて,各ルートに名前を付けています。
これを見て分かるように,このあたりは全部applicationというメインのコントローラが処理します。actionのところでスキンタイプごとに振り分ける形。
このほか,ポスト系を処理するactionのコントローラと管理用のadminコントローラ,rss用のコントローラを別途作っていく予定です。
これを受けるapplication.rbは今のところこんな感じ。グローバル変数を多用しているところがちょっと格好悪いのですが。
class ApplicationController < ActionController::Base
private
def set_charset
headers['Content-Type'] = "text/html; charset=utf-8"
end
# Pick a unique cookie name to distinguish our session data from others'
session :session_key => '_nonror_session_id'
private
def setblog
if !params['blogname']
$NCGlobals['blog'] = Blog.find(CONF['DefaultBlog'])
else
$NCGlobals['blog'] = Blog.find_by_bname(params['blogname']) || Blog.find(CONF['DefaultBlog'])
end
end
def setcatid
if params['catid']
$NCGlobals['catid'] = params['catid'].to_i
end
end
public
def index
setblog
setcatid
$NCGlobals['skin'] = $NCGlobals['blog'].skin
$NCGlobals['skintype'] = 'index'
$NCGlobals['item'] = Array.new
$NCGlobals['controller'] = self
@contents = $NCGlobals['skin'].part($NCGlobals['skintype'])
@controller = NCParser::DefaultSkinIndex.new $NCGlobals['controller']
end
def item
setblog
setcatid
$NCGlobals['skin'] = $NCGlobals['blog'].skin
$NCGlobals['skintype'] = 'item'
$NCGlobals['item'] = [Item.find(params['itemid'].to_i)]
$NCGlobals['controller'] = self
@contents = $NCGlobals['skin'].part($NCGlobals['skintype'])
@controller = NCParser::DefaultSkinItem.new $NCGlobals['controller']
render :action=>'index'
end
end今,actionはindexとitemしかありませんが,itemの最後には「render :action=>'index'」というのが入っています。これは表示用のビュー・ファイルはindex.rhtmlを使うということ。全スキンで共通にしています。といってもビュー・ファイルの中身は「<%= parse %>」だけ。これを処理するのはapplication_helper.rbで,この中身もいたってシンプル。
# Methods added to this helper will be available to all templates in the application.
module ApplicationHelper
def parse
@controller.parse(@contents)
end
end
ここで@controllerというのがパーサのオブジェクトで,それがスキンの中身をパースするという仕組みです。これでパーサが実行した結果が出力されます。これが基本的な流れです。どうもきたない
文字化け問題,Railsのコンソールでは化けているものの,Webの出力は化けていないことが分かり,ますますよく分かりませんが,とりあえず今のままでも困らないので,このまま進めます。スキン変数とテンプレート変数をちょこちょこと実装しています。相変わらずRubyのnilの扱いにはちょっととまどいがちですが,間違えるパターンが分かってきたので,バグ取りの時間は短くなってきたようです。
URL生成もできているのですが,気になるのはRails開発ブログでDeprecated Controller Methodsの中にurl_for(:#{options}) - use url_for with a named route directlyと書かれていること。
しかし,名前付きの呼び出し方 ...._url はprotectedなので直接コントローラから呼べないのと,_send_を使って無理やり送ってもちょっと動作が違うという問題があります。url_forを使って名前を指定する方法があるのかと,探しているのですが,見つかりません。
文字化けわけ分からん
DBをmysqldumpして,エディタで見てみたらちゃんとUTF-8でデータが入っています。それなのにRailsでは文字化け。
$KCODEもdatabase.ymlのencodingも指定しているのになあ…
ほかに何かあったっけ?
そんなわけで
RadRailsのSubversion対応はできました(Thanks to hsurさん)。で,結局夕べは眠くて,コントローラをローカル変数で渡したときに動かないというバグだけ取って寝ました。昨日は睡眠時間4時間オーバー。全体の進捗を書いておくと,パーサの基本部分はできていて,後はスキンやテンプレートでparse_●●というのを書いていけばいい状態。次に与えられたURLに対して動作を振り分けるところを作っていて,それに対応するURL生成をテストしているところ。これがうまく行ってcreateLink系の部分ができると基本的にブラウズはできる形になります(検索除く)。
その後は,認証系を作り,コメントなどの更新系の作業に移る計画です。
もろもろ
●パーサをクラスでなくてモジュールにするかという件。パーサが複数になって連動したりすると名前のコンフリクトとか絶対に起きるだろうなと思い,やっぱり却下。今はコントローラをグローバル変数に入れているのが,事実上は問題なくてもちょっと気持ち悪いところ。そこをパラメータで渡してインスタンス変数にしようかと。でも,なぜかエラーが出ます。グローバル変数はあまり使いたくないのだけど,プラグインで様々な機能を入れていくときに結局必要になるのではないかという気もします。●プログラムを書くのは夜中12時くらいから2時くらいまでなのですが,デバグなどしているとついつい時間オーバーしてしまいがち。朝7時に起きることを考えると3時までには寝るようにしないときついのですが。さらに,夜1回1時間くらい寝てしまってから,起きて作業をすると,目が冴えてしまって,今度は眠りに着くのが大変。夕べも結局睡眠時間が3時間くらいに。こうなると昼間の仕事に影響が出てしまうので,一回寝てしまった日はプログラム書かないとか,デバッグしないとか,ルールを決めないと体壊しそうです。
●Subversionの使い方が,相変わらずよく分かっていません。今はローカルにtrunkだのbranchだののツリーを作って,サーバーのリポジトリと同期させ,trunkの下だけをInstantRailsの下にコピーして作業しています。作業内容をツリーに入れるときに,どこを更新したかちゃんと覚えていないので,更新忘れのファイルなどが出現しがちです。それから,他の場所で更新したものを反映するときも,ローカルのツリーでどこが変わったのかを忘れてしまい,作業ディレクトリに反映し忘れてしまうことがあります。RadRails(Eclipse)にはSubversionの機能があるというのですが,使い方が分かっていません。作業ディレクトリから直接リポジトリと同期させるのでしょうか。
●このブログ,カテゴリーもちゃんと作っていないいい加減な構成。基本的に作業メモだからいいんだけど,実際には動作チェックもここのデータを使っているので,もうちょっとまじめに作っておかないとちょっと大変。MCやTagEXも入れようかなあ。あ,既に入っているのだけどつかってないだけでした。
実装方針で迷い
URLによってディスパッチ先を決めていくのは,割と簡単な作業なので,淡々と進めています。それと並行して,各スキンタイプによる処理を加え始めているのですが,ちょっと迷い。
今のNucleusの場合,スキンタイプごとに,サポートしているスキン変数を配列に持っていて,それを参照して実行するかどうかを決めています。その部分の実装をどうするか。Nucleusと同じにしてもいいのですが,あまり格好よくありません。スキンタイプごとにサブクラスを作ってそこで対応するのがスマートですが,今度は複数のスキンタイプでサポートしているときにどうするか,という問題があります。
全部のタイプでサポートしているならば親クラスで定義してしまえばそれで済むのですが,そうでない場合,各スキンタイプに書くのは無駄です。Railsにはdelegateという機能があって,特定のメソッドをインスタンス変数に実行させることができますが,そのためだけに,他のスキンタイプ用のオブジェクトをインスタンス変数に持つのもどうなのか。まあ,それでもこれが一番きれいな気がするので,多分それにするでしょう。
これに関連してもう1つの実装として考えているのがパーサの処理部分をクラスではなくモジュールにしてしまって,コントローラにmix-inすること。これから先,Railsのコントローラで提供されている様々な機能(例えばキャッシュ)を利用していくためには,処理をなるべくコントローラ内でするのがいいのではないか,という発想です。
モジュールにした場合に機能的に問題があるのかないのか。その場合,サブクラスとして考えている分も,単なる別のモジュールという扱いになって,それぞれmix-inすることになります。一応それでも動きそうな気はするのですが,検証はしていません。まだ,モジュールの扱い方には慣れていないので,ちょっと危険な感じもします。
メモ
後で,クラスごとの役割分担を考えるときに必要かと思って,「Ruby on Rails入門」をみながらページスイッチの機能を一応入れてみようとしてみたのですが,うまく動かず。少し調べていたら,この問題の解決にはつながっていないのですが,そもそもこのPaginationの機能自体がなくなって,will_paginateあるいはpaginating_findプラグインを使うとのこと。will_paginateの方が標準推奨のようですが,吐き出すSQLがいけてないらしく,どちらを使うかちょっと悩ましいところです。
どちらもモデルに組み込む形なので,使うのは比較的簡単そうな気がします。ということで,これは先送り。
Ruby on Rails入門―優しいRailsの育て方
西 和則
秀和システム
2006/08
¥ 2,940 (定価)
¥ 2,940 (Amazon価格)
29pt (Amazonポイント)
(私のおすすめ度)
★★★☆ (Amazonおすすめ度)
単行本
通常24時間以内に発送
(価格・在庫状況は12月4日 23:22現在)
西 和則
秀和システム
2006/08
¥ 2,940 (定価)
¥ 2,940 (Amazon価格)
29pt (Amazonポイント)
(私のおすすめ度)
★★★☆ (Amazonおすすめ度)
単行本
通常24時間以内に発送
(価格・在庫状況は12月4日 23:22現在)
一応パースはしています
文字化け問題は完全には解決していませんが,とりあえずいくつかのMySQLのうちちゃんと動くものもあったので,問題は当面たなあげということで進めます。ということで,一応進捗はここまで。
画像サンプル
テンプレート変数など,まだ少ししか実装していないので<%,%>だらけですが,それでもタイトルや本文が表示されると,すこし格好が付きました。
リンク系をちゃんとしないと,トップページを表示する以外,何もできないので,次はコントローラのディスパッチの設定をして,それにあわせてURL生成をするという作業に移ります。
コードは暫定的なのばかりなので,特に載せませんが,テンプレートのパーツにアクセスする部分を変えたので,そこだけ書いておきます。
def [] partname
begin
@parts[partname].to_s
rescue
if !@parts
@parts = Hash.new
partsarray = TemplatePart.find(:all, :conditions=>["tdesc = ?", self.tdnumber])
partsarray.each do |part|
@parts[part['tpartname']]=part['tcontent']
end
end
@parts[partname].to_s
end
end
これでテンプレート・オブジェクトに対してtemplate['ITEM']みたいな形でパーツを取ってこれます。ブログを展開するとき,連続していくつかのパーツを使うため,一回のDBアクセスでそのテンプレートの内容を全部取り込むようにしました。Rubyは例外処理が簡単に書けるので,こういうのはやりやすいです(これくらいなら例外処理使わなくても構いませんが,この方が読みやすいと思います)。スキンの場合は,一回のアクセスで使うパーツはたいてい1つなので,今のところこの処理は入れていません。ただ,スペシャル・スキン・パーツを多用するようになってきたら,入れた方がいいかもしれません。
それから,パートのデータを返すところにわざわざto_sを付けているのは,そのパートのデータがないときに,nilの代わりに空文字列を返すためです。こうしておかないと,文字列にnilを連結させてしまいエラーを起こすことになります。
文字化けorz
パースはなんとかしてくれるようになってきたものの,文字化け問題が再発していることが判明しました。phpMyAdmin上ではちゃんと見えているのですが。ちなみに
show variables like 'char%'
すると
character_set_client utf8
character_set_connection utf8
character_set_database utf8
character_set_filesystem binary
character_set_results utf8
character_set_server latin1
character_set_system utf8
とでます。サーバーのlatin1というのが怪しいのかなあ。
(default-character-set=utf8を設定したらutf8になりましたが文字化けは変わらず)
PHPにしろMySQLにしろ,文字コードの自動変換ほど余計なお世話な機能はないと思います。
こういうので作業が止まるのが一番嫌です。
「落ちる」RadRails
Railsの開発はRadRailsを使っているのですが,昨晩は編集中に3回もハングアップしてへこみました。しかも毎回,マシンをリセットするしかないという派手な落ち方。RadRailsのエディタは括弧やクォーテーション・マークなど補完してくれるのですが,間違ってクォーテーション・マークを入れてしまい,すぐに戻って消そうとするとそこでハングします。昨日編集していたファイルの特殊事情なのかどうかは不明です。
後から考えたら2回ハングして時点で,RadRails使わず,テキスト・エディタに切り替えればよかったです。
URL周り
今日はちょっとコードは書くのを休んで,NucleusのURL周りを調べていました。実はあんまりちゃんと知らなかったりして(^^;)。
| スキンタイプ | パラメータ | blogidパラメータ | category修飾 | ページング |
| index | なし | 指定可(省略時デフォルト) | ○ | ○ |
| item | itemid | 指定可(省略時デフォルト) | ○ | × |
| archivelist | blogidあるいはなし | 指定なし | ○ | × |
| search | 検索文字列 | 指定可(省略時デフォルト) | × | ○ |
| member | memberid | 指定可(省略時デフォルト) | × | × |
| archive | 年月 | 指定可(省略時デフォルト) | ○ | ○ |
| error | URLとしては使われない | 指定なし | × | × |
| imagepopup | mediaファイルの位置 | 指定可(省略時デフォルト) | × | × |
スキンタイプと関連するパラメータ周りをまとめるとこのようになります(多分)。一番右のページングは,ページスイッチを付ける意味があるかどうか。ページスイッチの機能はコアに持たせる予定なので。
これ見ると,archivelistのところだけブログの指定方法が変則ですね。
URL周りはNucleusコアとの互換性はそれほど重視していません。FancyURLに似た形にはすると思いますが違うところも出てくるかもしれません(要は実装しやすい形でということで)。最終的にはCustomURLと互換にするつもりです。
注:archiveとarchivelistで逆になっているところなどがあったので修正しています。
<%image%>と<%popup%>
コードは書いていますが,まだ動作テストしていないので,あらましは明日あたり。テンプレートなどの細かい実装に移っているので,ようやく今のNucleusのコードを調べたりしています。
パーサ書いているときに気付いたのですが,今のNucleusのパーサーの作りってかなりいい加減。
実は<%と%>って全く区別されていないってご存知でしょうか。試しにスキン中で
%>parsedinclude(head.inc)<%
などと書いてしまっても問題なく動きます。今回のパーサはネストを実現したかったこともあり,そのあたりはきちんと見ています。
あと,一般にはNucleusのパーサってスキンとテンプレートの2段階だと思われていると思いますが,実際にはその下に,アイテム・レベルとイメージ,ポップアップ,メディア処理の2段階が入り,計4段階で動いています。Nucleus3.3ではプラグインのアイテム変数が利用できるようになっていますが,アイテムにはもともと<%image%>などを処理するためにパーサを適用していたので,そこを少し変えるだけでこの機能は追加できました(記憶になかったけど,この部分は僕が書いたコードが採用されているようです)。
最後の段階のイメージなどの処理はテンプレートの中のIMAGE_CODEなどの内容がパースされます。ただし,他の段階のようなフルレベルのパースではなく,同じ名前の変数と置き換えるくらいの単純なやり方です。ちなみにこのテンプレートを書き換えるとプラグインを使わなくてもLightBox.jsを呼び出す程度のものは実現できます。このあたりの自由度は,あまり活用されていない気がしますが,どうでしょう。
というわけで,このあたりの処理を,今は書いています。
ifの処理つきパーサーのコード
ほぼ完全版(のつもり)。これで,各パーサーのクラスはスキン変数やテンプレート変数に相当する部分のparse_●●を書いていけばいいはずです。ああっと,まだプラグイン呼び出すとこ書いてないけど,parse_●●がなかったときの処理としてrescue節に入れてあげればいいでしょう。そもそもまだプラグインのアーキテクチャを考えていないので,そこは後から加えます。
# ParserBase
require 'strscan'
module NCParser
class Base
yaml = YAML.load_file("config/database.yml")
ENV['RAILS_ENV'] ||= 'development'
@@execmode = ENV['RAILS_ENV']
def initialize (left = '<%', right = '%>', nest = false)
@left = Regexp.new left
@leftlen = left.length
@right = Regexp.new right
@rightlen = right.length
@nest = nest
end
def parse(str)
if @nest
parse_with_nest(str)
else
parse_without_nest(str)
end
end
def parse_with_nest(str)
parenleft = StringScanner.new(str)
parenright = StringScanner.new(str)
length = str.length
nestlevel = 0
result = Array.new
ifstack = Array.new
ifstack[0] = Array.new
@ifsituation = ifstack[0]
currentpos = 0
if parenleft.search_full(@left,true,false)
leftpos = parenleft.pos
else
leftpos = length+1
end
if parenright.search_full(@right,true,false)
rightpos = parenright.pos
else
rightpos = length+1
end
while (leftpos <= length) || (rightpos <= length)
if nestlevel == 0 || leftpos < rightpos
if @ifsituation.size == 0 || @ifsituation.last == 1
if ! result[nestlevel]
result[nestlevel] = str[currentpos , (leftpos-@leftlen-currentpos)]
else
result[nestlevel] += str[currentpos , (leftpos-@leftlen-currentpos)]
end
end
ifstack.push Array.new
@ifsituation = ifstack.last
nestlevel += 1
currentpos = leftpos
if parenleft.search_full(@left,true,false)
leftpos = parenleft.pos
else
leftpos = length+1
end
else
if @ifsituation.size == 0 || @ifsituation.last == 1
if ! result[nestlevel]
result[nestlevel] = str[currentpos , (rightpos-@rightlen-currentpos)]
else
result[nestlevel] += str[currentpos , (rightpos-@rightlen-currentpos)]
end
end
result[nestlevel-1] += self.exec result[nestlevel]
result.pop
nestlevel -= 1
ifstack.pop
@ifsituation = ifstack.last
currentpos = rightpos
if parenright.search_full(@right,true,false)
rightpos = parenright.pos
else
rightpos = length+1
end
end
end
result[0] += str[currentpos .. length]
end
def parse_without_nest(str)
parenleft = StringScanner.new(str)
parenright = StringScanner.new(str)
length = str.length
result = ''
currentpos = 0
leftpos = 0
rightpos = 0
@ifsituation = Array.new
while (leftpos <= length) || (rightpos <= length)
if parenleft.search_full(@left,true,false)
leftpos = parenleft.pos
else
leftpos = length+1
end
begin
if parenright.search_full(@right,true,false)
rightpos = parenright.pos
else
rightpos = length+1
end
end while leftpos > rightpos
if leftpos<=length && rightpos<=length
if @ifsituation.size == 0 || @ifsituation.last == 1
result += str[currentpos,(leftpos-@leftlen-currentpos)].to_s
end
result += self.exec str[leftpos..rightpos-@rightlen-1]
currentpos = rightpos
parenleft.pos = rightpos-1
else
result += str[currentpos,length-currentpos]
end
end
result
end
def exec str
str.rstrip!
str.lstrip!
str =~ /^(\w+)(?:\((.*)\))?$/m
command = $1
paramall = $2 ? $2 : ""
params = paramall.split(/[\t\r\n\f\v]*,[\t\r\n\f\v]*/)
params.unshift paramall
begin
send('do_'+command, params)
rescue
begin
if @ifsituation.size == 0 || @ifsituation.last == 1
send('parse_'+command, params)
else
''
end
rescue
debugout('<%'+command+'('+paramall+')%>')
end
end
end
def debugout str
if @@execmode == 'production'
''
else
str
end
end
def do_if(params)
if self.check_conditions(params)
@ifsituation.push(1)
else
@ifsituation.push(0)
end
''
end
def do_ifnot(params)
if self.check_conditions(params)
@ifsituation.push(0)
else
@ifsituation.push(1)
end
''
end
def do_else(params)
case @ifsituation.last
when 0
@ifsituation[-1,1] = 1
when 1
@ifsituation[-1,1] = 2
end
''
end
def do_elseif(params)
case @ifsituation.last
when 0
if self.check_conditions(params)
@ifsituation[-1,1] = 1
else
@ifsituation[-1,1] = 0
end
when 1
@ifsituation[-1,1] = 2
end
''
end
def do_elseifnot(params)
case @ifsituation.last
when 0
if self.check_conditions(params)
@ifsituation[-1,1] = 0
else
@ifsituation[-1,1] = 1
end
when 1
@ifsituation[-1,1] = 2
end
''
end
def do_endif(params)
@ifsituation.pop
''
end
end
end
前の記事は大分うそでした
「Ruby on Rails入門~優しいRailsの育て方」を読んでいたら,プラグインの作り方が少し載っていて,モジュールにする際にはモジュールをディレクトリ名にすると書いてありました。先日の動かなかった原因もそこにあったようです。つまりRailsの規約に合っていなかったわけ。parser_base/lib/の下に/parser/base.rbを作ってそこに入れたところ,どこにもrequireなしでちゃんと動くようになりました。テストもリネームしないといけないけどそちらは未検証。
それと,parsedincludeの中からのparseが動かないというのもバグでした。
括弧とパラメータがないときの処理がおかしかったのが原因。Rubyは暗黙の型変換がないのでnilには結構悩まされます。
遅々とした進行
先日作ったParser::Baseを継承してデフォルト・パーサーのクラスを作ったのはいいものの,問題はプラグインのロード順。Baseの方を先に読み込んでもらわないといけないのに,どうしてもうまくrequireできない。いろいろ調べるうちに,ようやくRailsのフォーラムでenvironment.rbの中にconfig.plugins = [ "prefixed", "parser_base", "default_parser" ]
などと書くことで動くことが判明。ここまでかなり不毛な時間を過ごしました。パーサーにもいくつかバグがあったので,それを取りつつ,いちおう動くようになってきました。
例えばDefaultParserのクラスは
class DefaultParser <Parser::Base
def exec str
str.rstrip!
str.lstrip!
str =~ /^(\w+)(?:\((.*)\))?$/m
command = $1
paramall = $2
params = paramall.split(/[\t\r\n\f\v]*,[\t\r\n\f\v]*/)
params.unshift paramall
begin
self.send(command, params)
rescue
command+'('+paramall+')'
end
end
def parsedinclude fname
contents = $skin.file(fname[1])
self.parse contents
end
endとしているのですが,ここでなぜかparsedincludeの中でパースしてくれません。self.parseの行を取るとincludeはできているのですが。ちょっと不思議。
パーサのベース部分
あんまりRubyっぽくない書き方だけど,今のところほかに思いつかないのでそのまま。aがnilの場合にa += b といった書き方ができないのが痛い。PHPだったら勝手に空文字列扱いにしてくれるのに。
パーサはネストありのものしか今のところ作っていませんが,ネストなしは簡単なので,まあいいでしょう。ただ,ちゃんとデバッグしてないです。正しくないフォーマットのものが来たら,問題起こす可能性大。無駄な検索をしていないので,実行速度は悪くないのではないかと思っています(ベンチマークはしてないけど)。
module Parser
class Base
def initialize (left = '<%', right = '%>', nest = true)
@left = Regexp.new left
@leftlen = left.length
@right = Regexp.new right
@rightlen = right.length
@nest = nest
end
def parse(str)
if @nest
parse_with_nest(str)
else
parse_without_nest(str)
end
end
def parse_with_nest(str)
parenleft = StringScanner.new(str)
parenright = StringScanner.new(str)
length = str.length
nestlevel = 0
result = Array.new
currentpos = 0
parenleft.search_full(@left,true,false)
parenright.search_full(@right,true,false)
leftpos = parenleft.pos ? parenleft.pos : length
rightpos = parenright.pos ? parenright.pos : length
while (leftpos != length) || (rightpos != length)
if nestlevel == 0 || leftpos < rightpos
if ! result[nestlevel]
result[nestlevel] = str[currentpos .. (leftpos-@leftlen-1)]
else
result[nestlevel] += str[currentpos .. (leftpos-@leftlen-1)]
end
nestlevel += 1
currentpos = leftpos
if parenleft.search_full(@left,true,false)
leftpos = parenleft.pos
else
leftpos = length
end
else
if ! result[nestlevel]
result[nestlevel] = str[currentpos .. (rightpos-@rightlen-1)]
else
result[nestlevel] += str[currentpos .. (rightpos-@rightlen-1)]
end
result[nestlevel-1] += self.exec result[nestlevel]
result.pop
nestlevel -= 1
currentpos = rightpos
if parenright.search_full(@right,true,false)
rightpos = parenright.pos
else
rightpos = length
end
end
end
result[0] += str[currentpos .. length]
end
def exec str
'exec(' + str + ')'
end
end
end
設定テーブルへのアクセス
nucleus_configテーブルには数値の主キーがないので,ActiveRecordによる自動処理は期待できません。ただ,ここはもう項目が決まっているので,一気に読み込んでハッシュとしてアクセスできるようにしてしまいます。
class Configurations < ActiveRecord::Base
set_base_name :config
@@configs = Hash.new
self.find_by_sql(["SELECT * FROM "+ self.table_name + " WHERE 1"]).each { |pair|
@@configs[pair['name']]=pair['value']
}
def self.[] name
@@configs[name]
end
endこういった書き方が正しいのかどうかはよく分かりませんが,これでConfigurations['DefaultBlog']といった形でアクセスできるようになりました。
まだ,プラグインなど残っているテーブルはありますが,すぐには必要ないので,コントローラとパーサに移りたいと思います。
PS. クラス名が長いのでCONFに修正しようと思います。
