feedcellar 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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