Skip to content

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

Posted on:2009年7月25日 at 02:04

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

Table of Contents

Open Table of Contents

とってもシンプルなブログツール Scanty

「スキャンティ」と片仮名で書いてしまうと、若干いやらしくも響くが、とてもシンプルな Sinatra ベースのブログエンジンだ。

作者本人もこのツールを利用してブログを書いているようだ。

と・・・この作者の方って、Heroku の人なんだ。。

Scanty の特徴としてはサイトの記載から拾ってみると、以下のような内容になっている。

特徴

作者も書いているように、これを土台に自分色に染めてね(この通りには作者は言っていない)、というものである。

コメント機能に関しは、ツールの中に抱えないで、DISQUS を使用するようにしている。

原材料

ライセンスは、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
)

とりあえずローカルでの動作確認レベルであればこのままでもよいが、外部公開するのであれば、

の変更が必須だろう。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 << '<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) %>

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

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>
</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。

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

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 の表記として何が正しいのかをキチンと確認する必要もあるが、

という点に修正を入れないと表示がおかしくなってしまう。

まずは現在のソースでどのように動作するか、状況を把握する。

$ 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 &#39;hello!&#39;\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">&amp;</span>
          <span class="comment">#39;hello!&amp;#39;</span>\n
          <span class="keyword">end</span>\n
          <span class="keyword">end</span>
        </pre>
      </code>
    </pre>"

確かに問題となっている状況の通りだ。

この状況に対して、

の 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 の作者が言うように Sinatra はフレームワークというよりは、ライブラリ的なものだ。 Rack に対して非常にセンスのよいデコレーションを行っている。

Rails はフルセットのフレームワークであり、非常に有用で、その設計は勉強にもなる。 ただ、その内部は複雑化しており、何か問題のあった時などには知っておくべきことが多すぎて、思わぬところで時間をとられる場合がある。

適材適所で、シンプルで扱い易い Sinatra を使ってみたいと思えてくる。