Sinatra ベースの軽量アプリいろいろ Scanty 編

Written by @dr_taka_n at 2009/07/25 02:04 [, ]

Sinatra ベースのアプリを調べており、その一環として、あちこちで名前を聞く手頃なアプリを見てみることにする。

とってもシンプルなブログツール 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

Rakefilestartstop タスクが定義してある。port 番号の定義がデフォルトで 3030 になっているので、適切なポート番号に変更。

投稿してみる

サーバを起動し、初めてアクセスを行うと以下の画面が表示される。

scanty-first-access

まずは、“Log in” をクリックしログインする。

(次回以降のログインの際には、/auth のパスを直打ちする)

scanty-admin-login

パスワードが合っていれば、最初に開いた画面に戻るので、今度は、“create a post” をクリックする。

scanty-post

投稿画面は非常にシンプル。項目のラベルが何もないが、上から、タイトル、タグ、本文の入力項目となっている。
タグはスペース区切りで複数指定できるようになっている。

“save” ボタンで投稿する。

scanty-posted

続けて投稿してみて、トップページを見てみる。

scanty-blog-list

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.rbconfigure メソッドのコードブロックに追加しておく。

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 << '
    ' model.errors.each do |k, v| html << "
  • #{k.to_s.capitalize} #{v.first}.
  • \n" end html << '
' else '' end end end

view に追加しておく。

lib/edit.erb:

<%# add error_message_for call %>
<%= error_message_for(post) %>

一応、これで必須チェックは入るようになる。

scanty-error-check1

タイトルで日本語の表示を可能に

最近の舶来物の場合、permalink の生成に以下のようなロジックを入れてくる。

lib/post.rb:

  def self.make_slug(title)
    title.downcase.gsub(/ /, '_').gsub(/[^a-z0-9_]/, '').squeeze('_')
  end

これだと、マルチバイト文字は空文字になってしまう。。それがタイトルに日本語を使えない理由。

URL エンコードするのは嫌なので、対応としては、タイトルはタイトルとして入力値をそのまま残し、permalink を構成する slug 部分は、投稿者に自分で入れてもらうことにする。

  1. view に入力用の slug フィールドの追加
  2. Post インスタンス生成時のパラメータに直接 slug フィールドの入力項目を設定
  3. 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>

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。

投稿画面にラベルが無いと流石にわかり難くなってきたので、ラベルをついでにつけておく。

scanty-edit-view

ソースコードの表示部分の変更

投稿された本文は、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
....

で、表示は以下の通り。

scanty-codeview

まず、ソースコードの枠の部分が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>"

一応、これで表示上は問題なくなる。
(後で、もう少しちゃんと考えよう。)

scanty-codeview-fixed

最終的な 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 を使ってみたいと思えてくる。

blog comments powered by Disqus