feedcellar 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 616ba2dcc5f71090f0cf437817a18182af508d37
4
- data.tar.gz: 02438faff33f59e0e6b6c7ac529bdcb1e24ba191
3
+ metadata.gz: 043688575adc2059f4f67363860f3f200efd2008
4
+ data.tar.gz: 15b35303185ebc4c4267d2b2125dc71a3393e948
5
5
  SHA512:
6
- metadata.gz: ba9a911ae57ad0758ae5652caf90edcabfc3d630703d5a7cf9bdd08af9b9dea87c776887eaef6136e286dcf04a62ffce07ac8bf3a3cfc590d3ed9f9b95467721
7
- data.tar.gz: ef820e614e5373c8af03734ca4af31283b4fd7c81c47ebfa224a3ddf29cf73c99423c32ff6c22fad8f9bc32c6486baf54309fe144ffc898ff9ec5103f9a4afd6
6
+ metadata.gz: f6dacd8a9f1b500b211038399634153ee7fcd0066d6d0300f3efbd7b655620d2fc0b8a4e7add5430c0075222e8b9eeaf37edda0db55ceab8e442df4f69f3aea0
7
+ data.tar.gz: cac123ca7f2c0bf5d2c1a317bc36f068df440d77209718f7b8ae114004e9cf8ba51cda4079bb3aca0e3ab6cbe6fdeae20824f679e634da25744bf35b32d5cd11
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ /var/
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ rvm:
2
+ - 1.9.3
3
+ - 2.0.0
4
+ - 2.1
5
+ before_install:
6
+ - curl --silent --location https://github.com/groonga/groonga/raw/master/data/travis/setup.sh | sh
data/NEWS.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # NEWS
2
2
 
3
+ ## 0.4.0: 2014-08-29
4
+
5
+ Browser and GUI support!
6
+
7
+ ### Changes
8
+
9
+ * Improvements
10
+ * Added an interface for a web browser.
11
+ * Added show command and GUI window (experimental).
12
+ * Use LGPLv2.1 or later to license.
13
+
3
14
  ## 0.3.2: 2013-07-04
4
15
 
5
16
  A bug fix release of 0.3.1.
data/README.md CHANGED
@@ -1,8 +1,14 @@
1
- # feedcellar - Searchable Storage
1
+ # feedcellar - a feed reader
2
2
 
3
- Searchable storage for RSS feed reader.
3
+ [![Build Status](https://secure.travis-ci.org/myokoym/feedcellar.png?branch=master)](http://travis-ci.org/myokoym/feedcellar)
4
4
 
5
- Powered by rroonga with groonga.
5
+ Feedcellar is a full-text searchable RSS feed reader and data store.
6
+
7
+ Powered by [Groonga][] (via [Rroonga][]) with [Ruby][].
8
+
9
+ [Groonga]:http://groonga.org/
10
+ [Rroonga]:http://ranguba.org/#about-rroonga
11
+ [Ruby]:https://www.ruby-lang.org/
6
12
 
7
13
  ## Installation
8
14
 
@@ -20,48 +26,74 @@ Or install it yourself as:
20
26
 
21
27
  ## Usage
22
28
 
23
- Show help
29
+ ### Show help
24
30
 
25
31
  $ feedcellar
26
32
 
27
- Register URL
33
+ ### Register URL
28
34
 
29
35
  $ feedcellar register http://example.net/rss
30
36
 
31
- Import URL from OPML
37
+ ### Import URL from OPML
32
38
 
33
39
  $ feedcellar import registers.xml
34
40
 
35
- Export registerd resources to OPML to STDOUT
41
+ ### Export registerd resources to OPML to STDOUT
36
42
 
37
43
  $ feedcellar export
38
44
 
39
- Show registers
45
+ ### Show registers
40
46
 
41
47
  $ feedcellar list
42
48
 
43
- Collect feeds (It takes several minutes)
49
+ ### Collect feeds (It takes several minutes)
44
50
 
45
51
  $ feedcellar collect
46
52
 
47
- Word search from titles and descriptions
53
+ ### Word search from titles and descriptions
48
54
 
49
55
  $ feedcellar search ruby
50
56
 
51
- Rich view by curses (set as default since 0.4.0)
57
+ ### Show feeds in a web browser
58
+
59
+ $ feedcellar web [--silent]
60
+
61
+ Or
62
+
63
+ $ rackup
64
+
65
+ #### Enable cache (using Racknga)
66
+
67
+ $ FEEDCELLAR_ENABLE_CACHE=true rackup
52
68
 
69
+ ### Show feeds on GUI window (experimental)
70
+
71
+ $ gem install gtk2
72
+ $ feedcellar show [--lines=N]
73
+
74
+ ### Rich view by curses (experimental)
75
+
76
+ $ gem install curses # for Ruby 2.1
53
77
  $ feedcellar search ruby --curses
54
78
 
55
79
  Keybind:
56
80
  j: down
57
81
  k: up
58
- f, ENTER: open link on firefox
82
+ f, ENTER: open the link on Firefox
59
83
  q: quit
60
84
 
61
- Delete database
85
+ ### Delete database
62
86
 
63
87
  $ rm -r ~/.feedcellar
64
88
 
89
+ ## License
90
+
91
+ Copyright (c) 2013-2014 Masafumi Yokoyama `<myokoym@gmail.com>`
92
+
93
+ LGPLv2.1 or later.
94
+
95
+ See 'license/lgpl-2.1.txt' or 'http://www.gnu.org/licenses/lgpl-2.1' for details.
96
+
65
97
  ## Contributing
66
98
 
67
99
  1. Fork it
data/Rakefile CHANGED
@@ -1 +1,9 @@
1
1
  require "bundler/gem_tasks"
2
+
3
+ desc "Run test"
4
+ task :test do
5
+ Bundler::GemHelper.install_tasks
6
+ ruby("test/run-test.rb")
7
+ end
8
+
9
+ task :default => :test
data/bin/feedcellar CHANGED
File without changes
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "racknga"
4
+ require "racknga/cache_database"
5
+
6
+ bin_dir = File.expand_path(File.dirname(__FILE__))
7
+ base_dir = File.join(bin_dir, "..")
8
+
9
+ cache_database_path = File.join(base_dir, "var", "cache", "db")
10
+ cache_database = Racknga::CacheDatabase.new(cache_database_path)
11
+ cache_database.purge_old_responses
data/config.ru ADDED
@@ -0,0 +1,16 @@
1
+ base_dir = File.expand_path(File.dirname(__FILE__))
2
+ lib_dir = File.join(base_dir, "lib")
3
+ $LOAD_PATH.unshift(lib_dir)
4
+ require "feedcellar/web"
5
+
6
+ ENV["FEEDCELLAR_HOME"] ||= File.join(base_dir, ".feedcellar")
7
+
8
+ if ENV["FEEDCELLAR_ENABLE_CACHE"]
9
+ require "racknga"
10
+ require "racknga/middleware/cache"
11
+
12
+ cache_database_path = File.join(base_dir, "var", "cache", "db")
13
+ use Racknga::Middleware::Cache, :database_path => cache_database_path
14
+ end
15
+
16
+ run Feedcellar::Web
data/feedcellar.gemspec CHANGED
@@ -8,22 +8,27 @@ Gem::Specification.new do |spec|
8
8
  spec.version = Feedcellar::VERSION
9
9
  spec.authors = ["Masafumi Yokoyama"]
10
10
  spec.email = ["myokoym@gmail.com"]
11
- spec.description = %q{Searchable storage for RSS feed reader by rroonga with groonga.}
12
- spec.summary = %q{Searchable Storage for Feed Reader}
11
+ spec.description = %q{Feedcellar is a full-text searchable RSS feed reader and data store by Groonga (via Rroonga) with Ruby.}
12
+ spec.summary = %q{Full-Text Searchable RSS Feed Reader by Groonga}
13
13
  spec.homepage = "http://myokoym.net/feedcellar/"
14
- spec.license = "MIT"
14
+ spec.license = "LGPLv2.1 or later"
15
15
 
16
16
  spec.files = `git ls-files`.split($/)
17
17
  spec.executables = spec.files.grep(%r{^bin/}) {|f| File.basename(f) }
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_runtime_dependency("rroonga")
21
+ spec.add_runtime_dependency("rroonga", ">= 3.0.4")
22
22
  spec.add_runtime_dependency("thor")
23
+ #spec.add_runtime_dependency("gtk2")
24
+ spec.add_runtime_dependency("sinatra")
25
+ spec.add_runtime_dependency("haml")
26
+ spec.add_runtime_dependency("launchy")
27
+ spec.add_runtime_dependency("racknga")
23
28
 
24
29
  spec.add_development_dependency("test-unit")
25
30
  spec.add_development_dependency("test-unit-notify")
26
31
  spec.add_development_dependency("test-unit-rr")
27
- spec.add_development_dependency("bundler", "~> 1.3")
32
+ spec.add_development_dependency("bundler")
28
33
  spec.add_development_dependency("rake")
29
34
  end
@@ -10,9 +10,12 @@ module Feedcellar
10
10
  class Command < Thor
11
11
  map "-v" => :version
12
12
 
13
+ attr_reader :database_dir
14
+
13
15
  def initialize(*args)
14
16
  super
15
- @base_dir = File.join(File.expand_path("~"), ".feedcellar")
17
+ default_base_dir = File.join(File.expand_path("~"), ".feedcellar")
18
+ @base_dir = ENV["FEEDCELLAR_HOME"] || default_base_dir
16
19
  @database_dir = File.join(@base_dir, "db")
17
20
  end
18
21
 
@@ -57,6 +60,21 @@ module Feedcellar
57
60
  end
58
61
  end
59
62
 
63
+ desc "show", "Show feeds on GUI."
64
+ option :lines, :type => :numeric, :aliases => "-n", :desc => "Number of lines"
65
+ def show
66
+ # TODO: Can we always require gtk2 gem?
67
+ begin
68
+ require "feedcellar/window"
69
+ rescue LoadError => e
70
+ $stderr.puts("#{e.class}: #{e.message}")
71
+ return false
72
+ end
73
+
74
+ window = Window.new(@database_dir, options)
75
+ window.run
76
+ end
77
+
60
78
  desc "list", "Show registered resources list of title and URL."
61
79
  def list
62
80
  GroongaDatabase.new.open(@database_dir) do |database|
@@ -90,16 +108,10 @@ module Feedcellar
90
108
  desc "latest", "Show latest feeds by resources."
91
109
  def latest
92
110
  GroongaDatabase.new.open(@database_dir) do |database|
93
- feeds = database.feeds
94
- # TODO: I want to use the groonga method for grouping.
95
- feeds.group_by {|feed| feed.resource.xmlUrl }.each do |url, group|
96
- latest_feed = group.sort_by {|feed| feed.date }.last
97
- next unless latest_feed
98
-
99
- title = latest_feed.title.gsub(/\n/, " ")
100
- next unless title
101
- date = latest_feed.date.strftime("%Y/%m/%d")
102
- puts "#{date} #{title} - #{latest_feed.resource.title}"
111
+ GroongaSearcher.latest(database).each do |feed|
112
+ title = feed.title.gsub(/\n/, " ")
113
+ date = feed.date.strftime("%Y/%m/%d")
114
+ puts "#{date} #{title} - #{feed.resource.title}"
103
115
  end
104
116
  end
105
117
  end
@@ -110,6 +122,7 @@ module Feedcellar
110
122
  option :mtime, :type => :numeric, :desc => "feed's data was last modified n*24 hours ago."
111
123
  option :resource, :type => :string, :desc => "search of partial match by feed's resource url"
112
124
  option :curses, :type => :boolean, :desc => "rich view for easy web browse"
125
+ option :grouping, :type => :boolean, :desc => "group by resource"
113
126
  def search(*words)
114
127
  if words.empty? &&
115
128
  (options["resource"].nil? || options["resource"].empty?)
@@ -123,6 +136,10 @@ module Feedcellar
123
136
  if options[:curses]
124
137
  require "feedcellar/curses_view"
125
138
  CursesView.run(sorted_feeds)
139
+ elsif options[:grouping]
140
+ sorted_feeds.group("resource").each do |group|
141
+ puts "#{group.key.title} (#{group.n_sub_records})"
142
+ end
126
143
  else
127
144
  sorted_feeds.each do |feed|
128
145
  title = feed.title.gsub(/\n/, " ")
@@ -138,5 +155,15 @@ module Feedcellar
138
155
  end
139
156
  end
140
157
  end
158
+
159
+ desc "web", "Show feeds in a web browser"
160
+ option :silent, :type => :boolean, :desc => "Don't open in browser"
161
+ def web
162
+ require "feedcellar/web"
163
+ require "launchy"
164
+ web_server_thread = Thread.new { Feedcellar::Web.run! }
165
+ Launchy.open("http://localhost:4567") unless options[:silent]
166
+ web_server_thread.join
167
+ end
141
168
  end
142
169
  end
@@ -8,12 +8,11 @@ module Feedcellar
8
8
  Curses.noecho
9
9
  Curses.nonl
10
10
 
11
- feeds.each_with_index do |feed, i|
12
- Curses.setpos(i, 0)
13
- title = feed.title.gsub(/\n/, " ")
14
- date = feed.date.strftime("%Y/%m/%d")
15
- Curses.addstr("#{date} #{title}")
16
- end
11
+ # TODO
12
+ feeds = feeds.to_a
13
+ feeds.reject! {|feed| feed.title.nil? }
14
+
15
+ render_feeds(feeds)
17
16
  Curses.setpos(0, 0)
18
17
 
19
18
  pos = 0
@@ -28,8 +27,24 @@ module Feedcellar
28
27
  Curses.setpos(pos, 0)
29
28
  when "f", 13
30
29
  spawn("firefox",
31
- feeds[pos + 1].link,
30
+ feeds[pos].link,
32
31
  [:out, :err] => "/dev/null")
32
+ when "d"
33
+ mainwin = Curses.stdscr
34
+ mainwin.clear
35
+ subwin = mainwin.subwin(mainwin.maxy, mainwin.maxx, 0, 0)
36
+ subwin.setpos(0, 0)
37
+ subwin.addstr(feeds[pos].title)
38
+ subwin.setpos(3, 0)
39
+ subwin.addstr(feeds[pos].resource.title)
40
+ subwin.setpos(6, 0)
41
+ subwin.addstr(feeds[pos].description)
42
+ subwin.refresh
43
+ Curses.getch
44
+ subwin.clear
45
+ subwin.close
46
+ render_feeds(feeds)
47
+ Curses.setpos(pos, 0)
33
48
  when "q"
34
49
  break
35
50
  end
@@ -38,5 +53,17 @@ module Feedcellar
38
53
  Curses.close_screen
39
54
  end
40
55
  end
56
+
57
+ module_function
58
+ def render_feeds(feeds)
59
+ feeds.each_with_index do |feed, i|
60
+ Curses.setpos(i, 0)
61
+ title = feed.title.gsub(/\n/, " ")
62
+ date = feed.date.strftime("%Y/%m/%d")
63
+ Curses.addstr("#{date} #{title}")
64
+ end
65
+ end
66
+
67
+ private_class_method :render_feeds
41
68
  end
42
69
  end
@@ -43,6 +43,10 @@ module Feedcellar
43
43
  :date => date)
44
44
  end
45
45
 
46
+ def delete(id)
47
+ feeds.delete(id)
48
+ end
49
+
46
50
  def unregister(title_or_url)
47
51
  resources.delete do |record|
48
52
  (record.title == title_or_url) |
@@ -3,38 +3,50 @@ module Feedcellar
3
3
  class << self
4
4
  def search(database, words, options)
5
5
  feeds = database.feeds
6
- feeds = feeds.select do |feed|
7
- expression = nil
8
- words.each do |word|
9
- sub_expression = (feed.title =~ word) |
10
- (feed.description =~ word)
11
- if expression.nil?
12
- expression = sub_expression
13
- else
14
- expression &= sub_expression
6
+
7
+ if (!words.nil? && !words.empty?) || options[:resource_id]
8
+ feeds = feeds.select do |feed|
9
+ expression = nil
10
+ words.each do |word|
11
+ sub_expression = (feed.title =~ word) |
12
+ (feed.description =~ word)
13
+ if expression.nil?
14
+ expression = sub_expression
15
+ else
16
+ expression &= sub_expression
17
+ end
15
18
  end
16
- end
17
19
 
18
- if options[:mtime]
19
- base_date = (Time.now - (options[:mtime] * 60 * 60 * 24))
20
- mtime_expression = feed.date > base_date
21
- if expression.nil?
22
- expression = mtime_expression
23
- else
24
- expression &= mtime_expression
20
+ if options[:mtime]
21
+ base_date = (Time.now - (options[:mtime] * 60 * 60 * 24))
22
+ mtime_expression = feed.date > base_date
23
+ if expression.nil?
24
+ expression = mtime_expression
25
+ else
26
+ expression &= mtime_expression
27
+ end
25
28
  end
26
- end
27
29
 
28
- if options[:resource]
29
- resource_expression = feed.resource =~ options[:resource]
30
- if expression.nil?
31
- expression = resource_expression
32
- else
33
- expression &= resource_expression
30
+ if options[:resource]
31
+ resource_expression = feed.resource =~ options[:resource]
32
+ if expression.nil?
33
+ expression = resource_expression
34
+ else
35
+ expression &= resource_expression
36
+ end
34
37
  end
35
- end
36
38
 
37
- expression
39
+ if options[:resource_id]
40
+ resource_expression = feed.resource._id == options[:resource_id]
41
+ if expression.nil?
42
+ expression = resource_expression
43
+ else
44
+ expression &= resource_expression
45
+ end
46
+ end
47
+
48
+ expression
49
+ end
38
50
  end
39
51
 
40
52
  order = options[:reverse] ? "ascending" : "descending"
@@ -42,6 +54,20 @@ module Feedcellar
42
54
 
43
55
  sorted_feeds
44
56
  end
57
+
58
+ def latest(database)
59
+ latest_feeds = []
60
+
61
+ feeds = database.feeds
62
+ feeds.group("resource.xmlUrl", :max_n_sub_records => 1).each do |group|
63
+ latest_feed = group.sub_records[0]
64
+ next unless latest_feed
65
+ next unless latest_feed.title
66
+ latest_feeds << latest_feed
67
+ end
68
+
69
+ latest_feeds
70
+ end
45
71
  end
46
72
  end
47
73
  end
@@ -0,0 +1,131 @@
1
+ # Copyright (C) 2014 Masafumi Yokoyama <myokoym@gmail.com>
2
+ #
3
+ # This library is free software; you can redistribute it and/or
4
+ # modify it under the terms of the GNU Lesser General Public
5
+ # License as published by the Free Software Foundation; either
6
+ # version 2.1 of the License, or (at your option) any later version.
7
+ #
8
+ # This library is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11
+ # Lesser General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU Lesser General Public
14
+ # License along with this library; if not, write to the Free Software
15
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
+
17
+ require "gtk2"
18
+ require "erb"
19
+
20
+ module Feedcellar
21
+ class TreeView < Gtk::TreeView
22
+ KEY_COLUMN, TITLE_COLUMN, LINK_COLUMN, DESCRIPTION_COLUMN, DATE_COLUMN, STRFTIME_COLUMN, RESOURCE_AND_TITLE_COLUMN = *0..6
23
+
24
+ def initialize(records)
25
+ super()
26
+ @model = Gtk::ListStore.new(String, String, String, String, Time, String, String)
27
+ create_tree(@model, records)
28
+ end
29
+
30
+ def next
31
+ move_cursor(Gtk::MovementStep::DISPLAY_LINES, 1)
32
+ end
33
+
34
+ def prev
35
+ move_cursor(Gtk::MovementStep::DISPLAY_LINES, -1)
36
+ end
37
+
38
+ def remove_selected_record
39
+ return nil unless selected_iter
40
+ @model.remove(selected_iter)
41
+ end
42
+
43
+ def get_link(path)
44
+ @model.get_iter(path).get_value(LINK_COLUMN)
45
+ end
46
+
47
+ def selected_key
48
+ return nil unless selected_iter
49
+ selected_iter.get_value(KEY_COLUMN)
50
+ end
51
+
52
+ def selected_title
53
+ return nil unless selected_iter
54
+ selected_iter.get_value(TITLE_COLUMN)
55
+ end
56
+
57
+ def selected_link
58
+ return nil unless selected_iter
59
+ selected_iter.get_value(LINK_COLUMN)
60
+ end
61
+
62
+ def selected_description
63
+ return nil unless selected_iter
64
+ selected_iter.get_value(DESCRIPTION_COLUMN)
65
+ end
66
+
67
+ def selected_date
68
+ return nil unless selected_iter
69
+ selected_iter.get_value(DATE_COLUMN)
70
+ end
71
+
72
+ def selected_iter
73
+ selection.selected
74
+ end
75
+
76
+ def update_model(records)
77
+ model = Gtk::ListStore.new(String, String, String, String, Time, String, String)
78
+ records.each do |record|
79
+ load_record(model, record)
80
+ end
81
+ set_model(model)
82
+ @model = model
83
+ end
84
+
85
+ private
86
+ def create_tree(model, records)
87
+ set_model(model)
88
+ self.search_column = TITLE_COLUMN
89
+ self.enable_search = false
90
+ self.rules_hint = true
91
+ self.tooltip_column = DESCRIPTION_COLUMN
92
+
93
+ selection.set_mode(:browse)
94
+
95
+ records.each do |record|
96
+ load_record(model, record)
97
+ end
98
+
99
+ column = create_column("Date", STRFTIME_COLUMN)
100
+ append_column(column)
101
+
102
+ column = create_column("Title", RESOURCE_AND_TITLE_COLUMN)
103
+ append_column(column)
104
+
105
+ expand_all
106
+ end
107
+
108
+ def create_column(title, index)
109
+ column = Gtk::TreeViewColumn.new
110
+ column.title = title
111
+ renderer = Gtk::CellRendererText.new
112
+ column.pack_start(renderer, :expand => false)
113
+ column.add_attribute(renderer, :text, index)
114
+ column.set_sort_column_id(index)
115
+ column
116
+ end
117
+
118
+ def load_record(model, record)
119
+ iter = model.append
120
+ iter.set_value(KEY_COLUMN, record._key)
121
+ iter.set_value(TITLE_COLUMN, record.title)
122
+ iter.set_value(LINK_COLUMN, record.link)
123
+ escaped_description = ERB::Util.html_escape(record.description)
124
+ iter.set_value(DESCRIPTION_COLUMN, escaped_description)
125
+ iter.set_value(DATE_COLUMN, record.date)
126
+ iter.set_value(STRFTIME_COLUMN, record.date.strftime("%Y-%m-%d\n%H:%M:%S"))
127
+ text = [record.resource.title, record.title].join("\n")
128
+ iter.set_value(RESOURCE_AND_TITLE_COLUMN, text)
129
+ end
130
+ end
131
+ end
@@ -1,3 +1,3 @@
1
1
  module Feedcellar
2
- VERSION = "0.3.2"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -0,0 +1,13 @@
1
+ %h1 feedcellar
2
+ %p= "Powered by Groonga #{groonga_version} and Rroonga #{rroonga_version}."
3
+ %form{:action => url("/search", false, true), :method => :get}
4
+ %input{:type => "textarea", :name => "word", :size => 20, :value => params[:word]}
5
+ %input{:type => "submit", :value => "Search"}
6
+ - if @feeds
7
+ %p
8
+ - grouping(@feeds).each do |resource|
9
+ = markup_drilled_item(resource)
10
+ %ul
11
+ - @feeds.each do |feed|
12
+ %li
13
+ %a{:href => feed.link}= "#{feed.title} - #{feed.resource.title}"
@@ -0,0 +1,6 @@
1
+ !!!
2
+ %html
3
+ %head
4
+ %title feedcellar
5
+ %body
6
+ = yield