Sinatra ベースのアプリを調べており、その一環として、あちこちで名前を聞く手頃なアプリを見てみることにする。
Table of Contents
Open Table of Contents
とってもシンプルなブログツール Scanty
「スキャンティ」と片仮名で書いてしまうと、若干いやらしくも響くが、とてもシンプルな Sinatra ベースのブログエンジンだ。
作者本人もこのツールを利用してブログを書いているようだ。
と・・・この作者の方って、Heroku の人なんだ。。
Scanty の特徴としてはサイトの記載から拾ってみると、以下のような内容になっている。
特徴
- ブログ”エンジン”とまでは言えないけれど、コンパクトでカスタマイズを入れるのは容易。
ブログツールの基礎として使える。 - ポストできる!(特徴かな。。)
- タグ付け
- 入力に使用できる記法: Markdown
- コメント機能: Disqus
- Atom feed
作者も書いているように、これを土台に自分色に染めてね(この通りには作者は言っていない)、というものである。
コメント機能に関しは、ツールの中に抱えないで、DISQUS を使用するようにしている。
原材料
- Web フレームワーク: Sinatra
- Markdown -> HTML への変換: Maruku
- コードシンタックスハイライト(Only Ruby): Syntax
- ORM: Sequel
ライセンスは、MIT License だ。
軽く動作を見てみて、ちょっと気になるところには手を入れてみることにする。
インストールと設定、そして起動
Sinatra がもしインストールされていないのであれば先に Sinatra を入れておく。
$ gem sudo install sinatra
Scanty を GitHub から clone し、自分用のブランチを用意しておく。
$ cd ~/work
$ git clone git://github.com/adamwiggins/scanty.git
$ cd scanty
$ git checkout -b taka-blog
$ git branch
master
* taka-blog
main.rb
に書いてあるデフォルトの設定を変更しておく。
Blog = OpenStruct.new(
:title => 'a scanty blog',
:author => 'John Doe',
:url_base => 'http://localhost:4567/',
:admin_password => 'changeme',
:admin_cookie_key => 'scanty_admin',
:admin_cookie_value => '51d6d976913ace58',
:disqus_shortname => nil
)
とりあえずローカルでの動作確認レベルであればこのままでもよいが、外部公開するのであれば、
:admin_password
:admin_cookie_key
:admin_cookie_value
の変更が必須だろう。cookie value はランダムな値で。
起動する。
$ ruby main.rb
先に設定した main.rb
の :usr_base
のアドレスにアクセスする。
また、rake による起動も可能。
$ rake -T
(in /Users/taka/work/sinatra/scanty)
rake start # Start the app server
rake stop # Stop the app server
Rakefile
に start
、stop
タスクが定義してある。port
番号の定義がデフォルトで 3030 になっているので、適切なポート番号に変更。
投稿してみる
サーバを起動し、初めてアクセスを行うと以下の画面が表示される。
まずは、“Log in” をクリックしログインする。
(次回以降のログインの際には、
パスワードが合っていれば、最初に開いた画面に戻るので、今度は、“create a post” をクリックする。
投稿画面は非常にシンプル。項目のラベルが何もないが、上から、タイトル、タグ、本文の入力項目となっている。
タグはスペース区切りで複数指定できるようになっている。
“save” ボタンで投稿する。
続けて投稿してみて、トップページを見てみる。
2 回目の投稿に、Ruby のソースを含めてみたのだが、少々表示がおかしいようだ。 また、入力の必須チェックなどが全く入っていない、日本語を含んだタイトルはまともに扱えない、というところがあるので、手を入れてみる。
オリジナルに変更を入れておく
起動してちょっと見ていただけで、とりあえず手を入れておきたいところが数点あった。手始めに以下の 3 点の変更を入れておくことにする。
- 入力項目の必須チェック
- タイトルで日本語の利用を可能に
- ソースコードの表示部分の変更
入力項目の必須チェックぐらいは入れておく
全く入っていないので入れておく。
この Scanty は、ORM に Sequel を使用している。Sequel は初体験で全く知識がない。ActiveRecord に似てるところもあるので、今回は深く調査せず、勘だけで書いてみる。。
検証ロジックは、ActiveRecord 同様、Model の validate メソッドに入れておいて、エラーのコレクションを追加してやればよさそうだ。
検証失敗時のハンドルは、Sequel::Model#save
時の validation チェックにが raise する Sequel::ValidationFailed
の例外でハンドルしようと思ったのだが、Sequel::ValidationFailed
がないと言われる。。
Scanty は、Sequel を vendor ディレクトリ配下に持っているのだが、取得したリポジトリに含まれているものはバージョン的に古そうだ。gem で最新のものをインストールし、そちらを使うようにする。自前で持っている vendor 配下への load path の追加は消しておく。
$ gem list sequel
*** LOCAL GEMS ***
sequel (3.2.0)
サーバの再起動をかけると、
$ ruby main.rb
./lib/post.rb:7: undefined method `table_exists?' for Post:Class (NoMethodError)
と怒られる。
新しいバージョンではお作法が変わったようだ。Sequel::Model.plugin(:schema)
を main.rb
の configure
メソッドのコードブロックに追加しておく。
main.rb
:
configure do
Sequel::Model.plugin(:schema) # add
Sequel.connect(ENV['DATABASE_URL'] || 'sqlite://blog.db')
(なんとも手探りな状態だ。。)
気を取り直して Model にバリデーションを入れておく。
lib/post.rb
:
class Post < Sequel::Model
...
# add validate method
def validate
errors[:title] << "can't be empty" if title.empty?
errors[:body] << "can't be empty" if body.empty?
end
そのハンドリングは、
main.rb
:
post '/posts' do
auth
post = Post.new(
:title => params[:title],
:tags => params[:tags],
:body => params[:body],
:created_at => Time.now,
)
# add begin - resucue - end
begin
post.save
redirect post.url
rescue Sequel::ValidationFailed
erb :edit, :locals => { :post => post, :url => '/posts' }
end
end
としておく。
エラーがあった時の表示だが、さすがに、ActionView::Helpers::ActiveRecordHelper#error_messages_for
のようなものは無い。。
適当に作っておく。
main.rb
:
helpers do
...
# add somthing like error_message_for..
def error_message_for(model)
unless model.errors.size == 0
html = ''
html << '<ul>'
model.errors.each do |k, v|
html << "<li>#{k.to_s.capitalize} #{v.first}.</li>\n"
end
html << '</ul>'
else
''
end
end
end
view に追加しておく。
lib/edit.erb
:
<%# add error_message_for call %> <%= error_message_for(post) %>
一応、これで必須チェックは入るようになる。
タイトルで日本語の表示を可能に
最近の舶来物の場合、permalink の生成に以下のようなロジックを入れてくる。
lib/post.rb
:
def self.make_slug(title)
title.downcase.gsub(/ /, '_').gsub(/[^a-z0-9_]/, '').squeeze('_')
end
これだと、マルチバイト文字は空文字になってしまう。。それがタイトルに日本語を使えない理由。
URL エンコードするのは嫌なので、対応としては、タイトルはタイトルとして入力値をそのまま残し、permalink を構成する slug 部分は、投稿者に自分で入れてもらうことにする。
- view に入力用の slug フィールドの追加
- Post インスタンス生成時のパラメータに直接 slug フィールドの入力項目を設定
- slug フィールドも必須チェック対象に
とすることにする。
1 つ目の対応。
lib/edit.erb
:
<div class="postzoom">
<h2 class="title">
<input type="text" name="title" value="<%= post.title %>" />
</h2>
<p class="meta"><input type="text" name="slug" value="<%= post.slug %>" /></p>
<!-- 追加 -->
<p class="meta"><input type="text" name="tags" value="<%= post.tags %>" /></p>
</div>
2 つ目の対応。
main.rb
:
post '/posts' do
auth
post = Post.new(
:title => params[:title],
:tags => params[:tags],
:body => params[:body],
:created_at => Time.now,
:slug => params[:slug] # add
)
3 つ目の対応。
lib/post.rb
:
class Post < Sequel::Model
...
def validate
errors[:title] << "can't be empty" if title.empty?
errors[:slug] << "can't be empty" if slug.empty? # add
errors[:body] << "can't be empty" if body.empty?
end
以上で OK。
投稿画面にラベルが無いと流石にわかり難くなってきたので、ラベルをついでにつけておく。
ソースコードの表示部分の変更
投稿された本文は、Markdown のパーサ(Maruku) を通って HTML 化され、さらに Syntax を通ってコードハイライト用のタグが補足される。
ソースの該当箇所は、以下のようになっている。
lib/post.rb:
class Post < Sequel::Model
....
def to_html(markdown)
h = Maruku.new(markdown).to_html
h.gsub(/<code>([^<]+)<\/code>/m) do
convertor = Syntax::Convertors::HTML.for_syntax "ruby"
highlighted = convertor.convert($1)
"<code>#{highlighted}</code>"
end
end
....
で、表示は以下の通り。
まず、ソースコードの枠の部分が 2 重になっているのは、<pre>...</pre>
の中に、<code><pre>...</pre><pre>...</pre></code>
と <pre>
タグが入れ子になってしまうからのようだ。作者のブログのページ を見てみたが、そうはなっていない。HTML の表記として何が正しいのかをキチンと確認する必要もあるが、
<pre>
が入れ子になっている- ソースコード部分での実体参照への勝手な置き換えが行なわれている
という点に修正を入れないと表示がおかしくなってしまう。
まずは現在のソースでどのように動作するか、状況を把握する。
$ irb
>> require 'rubygems'
=> true
>> require 'maruku'
=> true
>> require 'syntax/convertors/html'
=> true
>> str = File.read('t.md')
=> "second!\n\nclass Hello\n def hello\n puts 'hello!'\n end\nend\n"
>> org_reg = Regexp.new('<code>([^<]+)</code>', Regexp::MULTILINE)
=> code[]codem
>> org_syn_convert_proc = lambda { "<code>#{Syntax::Convertors::HTML.for_syntax('ruby').convert($1)}</code>" }
=> #<Proc:0x0110169c@(irb):8>
>> org_h = Maruku.new(str).to_html
=> "<p>second!</p>\n\n
<pre><code>class Hello\n def hello\n puts 'hello!'\n end\nend</code></pre>"
>> org_h.gsub(org_reg, &org_syn_convert_proc)
=> "<p>second!</p>\n\n
<pre>
<code>
<pre>
<span class="keyword">class </span>
<span class="class">Hello</span>\n
<span class="keyword">def </span>
<span class="method">hello</span>\n
<span class="ident">puts</span>
<span class="punct">&</span>
<span class="comment">#39;hello!&#39;</span>\n
<span class="keyword">end</span>\n
<span class="keyword">end</span>
</pre>
</code>
</pre>"
確かに問題となっている状況の通りだ。
この状況に対して、
- シンタックスハイライトを適用させるための Convert 処理を変更する
<pre>
が入れ子になっている件への対応
- Markdown から HTML への変換には、Maruku の代わりに、BlueCloth を使う
- ソースコード部分での実体参照への勝手な置き換えが行なわれている件への対応
の 2 点を行ってみた。
>> require 'bluecloth'
=> true
>> changed_reg = Regexp.new('<pre><code>([^<]+)</code></pre>', Regexp::MULTILINE)
=> precode[]codeprem
>> changed_syn_convert_proc = lambda { Syntax::Convertors::HTML.for_syntax('ruby').convert($1) }
=> #<Proc:0x0108f27c@(irb):15>
>> b_h = BlueCloth.new(str).to_html
=> "<p>second!</p>\n\n<pre><code>class Hello\n def hello\n puts 'hello!'\n end\nend\n</code></pre>"
>> b_h.gsub(changed_reg, &changed_syn_convert_proc)
=> "<p>second!</p>\n\n
<pre>
<span class="keyword">class </span>
<span class="class">Hello</span>\n
<span class="keyword">def </span>
<span class="method">hello</span>\n
<span class="ident">puts</span>
<span class="punct">'</span>
<span class="string">hello!</span>
<span class="punct">'</span>\n
<span class="keyword">end</span>\n
<span class="keyword">end</span>\n
</pre>"
一応、これで表示上は問題なくなる。
(後で、もう少しちゃんと考えよう。)
最終的な Post#to_html
の変更箇所は以下の通り。
$ git diff
diff --git a/lib/post.rb b/lib/post.rb
index 3acb149..9f5c20b 100644
--- a/lib/post.rb
+++ b/lib/post.rb
@@ -1,4 +1,4 @@
-require File.dirname(__FILE__) + '/../vendor/maruku/maruku'
+require 'bluecloth'
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../vendor/syntax'
require 'syntax/convertors/html'
@@ -56,11 +56,10 @@ class Post < Sequel::Model
########
def to_html(markdown)
- h = Maruku.new(markdown).to_html
- h.gsub(/<code>([^<]+)<\/code>/m) do
+ h = BlueCloth.new(markdown).to_html
+ h.gsub(/<pre><code>([^<]+)<\/code><\/pre>/m) do
convertor = Syntax::Convertors::HTML.for_syntax "ruby"
- highlighted = convertor.convert($1)
- "<code>#{highlighted}</code>"
+ convertor.convert($1)
end
end
感想
- 機能が最小限ということもあるが、Sinatra ベースのアプリは非常にシンプルでわかり易い。
- 「HTTP メソッド + パス」がダイレクトに処理コードに紐付くのは気持ちいい。
- Sequel って結構いいかも。
Sinatra の作者が言うように Sinatra はフレームワークというよりは、ライブラリ的なものだ。 Rack に対して非常にセンスのよいデコレーションを行っている。
Rails はフルセットのフレームワークであり、非常に有用で、その設計は勉強にもなる。 ただ、その内部は複雑化しており、何か問題のあった時などには知っておくべきことが多すぎて、思わぬところで時間をとられる場合がある。
適材適所で、シンプルで扱い易い Sinatra を使ってみたいと思えてくる。