ruby-slippers 0.0.1

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.
Files changed (42) hide show
  1. data/.rvmrc +2 -0
  2. data/Gemfile +16 -0
  3. data/Gemfile.lock +90 -0
  4. data/Guardfile +16 -0
  5. data/LICENSE +20 -0
  6. data/README.md +114 -0
  7. data/Rakefile +5 -0
  8. data/VERSION +1 -0
  9. data/lib/ext/ext.rb +57 -0
  10. data/lib/ruby_slippers/app.rb +41 -0
  11. data/lib/ruby_slippers/archives.rb +22 -0
  12. data/lib/ruby_slippers/article.rb +84 -0
  13. data/lib/ruby_slippers/config.rb +35 -0
  14. data/lib/ruby_slippers/context.rb +38 -0
  15. data/lib/ruby_slippers/engine.rb +21 -0
  16. data/lib/ruby_slippers/repo.rb +21 -0
  17. data/lib/ruby_slippers/site.rb +103 -0
  18. data/lib/ruby_slippers/template.rb +28 -0
  19. data/lib/ruby_slippers.rb +28 -0
  20. data/lib/tasks/gemspec.rake +24 -0
  21. data/lib/tasks/test.rake +17 -0
  22. data/test/fixtures/articles/2010-05-17-the-wonderful-wizard-of-oz.txt +6 -0
  23. data/test/fixtures/articles/2010-05-18-the-marvelous-land-of-oz.txt +6 -0
  24. data/test/fixtures/articles/2010-05-20-dorothy-and-the-wizard-of-oz.txt +6 -0
  25. data/test/fixtures/articles/2011-05-18-ozma-of-oz.txt +8 -0
  26. data/test/fixtures/images/ozma.png +0 -0
  27. data/test/fixtures/pages/about.rhtml +1 -0
  28. data/test/fixtures/pages/archives.rhtml +5 -0
  29. data/test/fixtures/pages/article.rhtml +4 -0
  30. data/test/fixtures/pages/index.rhtml +13 -0
  31. data/test/fixtures/pages/tagged.rhtml +9 -0
  32. data/test/fixtures/templates/index.builder +21 -0
  33. data/test/fixtures/templates/layout.rhtml +4 -0
  34. data/test/fixtures/templates/repo.rhtml +1 -0
  35. data/test/integration/article_test.rb +44 -0
  36. data/test/integration/atom_test.rb +19 -0
  37. data/test/integration/ruby_slippers_test.rb +77 -0
  38. data/test/support/test_helper.rb +46 -0
  39. data/test/unit/archives_test.rb +8 -0
  40. data/test/unit/article_test.rb +118 -0
  41. data/test/unit/ruby_slippers_test.rb +75 -0
  42. metadata +318 -0
data/.rvmrc ADDED
@@ -0,0 +1,2 @@
1
+ rvm_gemset_create_on_use_flag=1
2
+ rvm gemset use slippers
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gem 'rake'
4
+ gem 'rack'
5
+ gem 'hpricot'
6
+ gem 'builder'
7
+ gem 'rdiscount'
8
+ gem 'maruku'
9
+
10
+ group :test, :development do
11
+ gem 'guard'
12
+ gem 'guard-bundler'
13
+ gem 'guard-test'
14
+ gem 'riot'
15
+ gem 'jeweler'
16
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,90 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ builder (3.0.0)
5
+ capybara (1.0.0)
6
+ mime-types (>= 1.16)
7
+ nokogiri (>= 1.3.3)
8
+ rack (>= 1.0.0)
9
+ rack-test (>= 0.5.4)
10
+ selenium-webdriver (~> 0.2.0)
11
+ xpath (~> 0.1.4)
12
+ childprocess (0.1.9)
13
+ ffi (~> 1.0.6)
14
+ cucumber (1.0.0)
15
+ builder (>= 2.1.2)
16
+ diff-lcs (>= 1.1.2)
17
+ gherkin (~> 2.4.1)
18
+ json (>= 1.4.6)
19
+ term-ansicolor (>= 1.0.5)
20
+ diff-lcs (1.1.2)
21
+ ffi (1.0.9)
22
+ gherkin (2.4.1)
23
+ json (>= 1.4.6)
24
+ git (1.2.5)
25
+ guard (0.4.2)
26
+ thor (~> 0.14.6)
27
+ guard-bundler (0.1.3)
28
+ bundler (>= 1.0.0)
29
+ guard (>= 0.2.2)
30
+ guard-test (0.3.0)
31
+ guard (>= 0.2.2)
32
+ test-unit (~> 2.2)
33
+ hpricot (0.8.4)
34
+ jeweler (1.6.2)
35
+ bundler (~> 1.0)
36
+ git (>= 1.2.5)
37
+ rake
38
+ json (1.5.3)
39
+ json_pure (1.5.3)
40
+ maruku (0.6.0)
41
+ syntax (>= 1.0.0)
42
+ mime-types (1.16)
43
+ nokogiri (1.4.6)
44
+ rack (1.3.0)
45
+ rack-test (0.6.0)
46
+ rack (>= 1.0)
47
+ rake (0.9.2)
48
+ rdiscount (1.6.8)
49
+ riot (0.12.4)
50
+ rr
51
+ rr (1.0.2)
52
+ rspec (2.6.0)
53
+ rspec-core (~> 2.6.0)
54
+ rspec-expectations (~> 2.6.0)
55
+ rspec-mocks (~> 2.6.0)
56
+ rspec-core (2.6.4)
57
+ rspec-expectations (2.6.0)
58
+ diff-lcs (~> 1.1.2)
59
+ rspec-mocks (2.6.0)
60
+ rubyzip (0.9.4)
61
+ selenium-webdriver (0.2.1)
62
+ childprocess (>= 0.1.7)
63
+ ffi (>= 1.0.7)
64
+ json_pure
65
+ rubyzip
66
+ syntax (1.0.0)
67
+ term-ansicolor (1.0.5)
68
+ test-unit (2.3.0)
69
+ thor (0.14.6)
70
+ xpath (0.1.4)
71
+ nokogiri (~> 1.3)
72
+
73
+ PLATFORMS
74
+ ruby
75
+
76
+ DEPENDENCIES
77
+ builder
78
+ capybara
79
+ cucumber
80
+ guard
81
+ guard-bundler
82
+ guard-test
83
+ hpricot
84
+ jeweler
85
+ maruku
86
+ rack
87
+ rake
88
+ rdiscount
89
+ riot
90
+ rspec
data/Guardfile ADDED
@@ -0,0 +1,16 @@
1
+ guard 'bundler' do
2
+ watch('Gemfile')
3
+ watch('Gemfile.lock')
4
+ end
5
+
6
+ guard 'test' do
7
+ watch(%r{^test/fixtures/articles/(.+)\.txt$})
8
+ watch(%r{^test/fixtures/templates/(.+)$})
9
+ watch(%r{^test/fixtures/pages/(.+)$})
10
+
11
+ watch(%r{^lib/(.+)\.rb$}) { |m| "test/#{m[1]}_test.rb" }
12
+ watch(%r{^lib/ruby_slippers/(.+)\.rb$}) { |m| ["test/unit/#{m[1]}_test.rb", "test/integration/#{m[1]}_test.rb"] }
13
+ watch(%r{^test/integration/.+_test\.rb$})
14
+ watch(%r{^test/unit/.+_test\.rb$})
15
+ watch(%r{^test/support/(.+)\.rb$}) { "test" }
16
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 dreamr
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,114 @@
1
+ RubySlippers, the smartest blogging engine in all of Oz!
2
+ ========================================================
3
+
4
+ [website](http://ruby-slippers.heroku.com)
5
+
6
+ Introduction
7
+ ------------
8
+
9
+
10
+ For Developers
11
+ --------------
12
+
13
+ #### To set up a new blog
14
+
15
+ $ git clone git://github.com/dreamr/ruby-slippers.git myblog
16
+ $ cd myblog
17
+ $ gem install bundler
18
+ $ bundle
19
+ $ rake install
20
+ $ -> Blog name: My new blog
21
+ $ Installing your blog to my-new-blog
22
+ $ Blog installed!
23
+
24
+
25
+ #### To create an article
26
+
27
+ $ rake new
28
+ $ -> Title: My new blog post!
29
+ $ Creating and opening my-new-blog-post (opens in your text editor!)
30
+ $ rake publish (commits, pushes, publishes then opens in your browser!)
31
+
32
+
33
+ Philosophy
34
+ ----------
35
+
36
+ RubySlippers is based on [Toto](http://github.com/cloudhead/toto) and aims to achieve their goals as well as our own. Hosting a ruby based free blog shouldn't be hard. We want to take that a step further and say it should be easy. Easy as pie. Easy as my best friend's Mom. Easy as a 1 click installer.
37
+
38
+ Oh yeah, MRI, bytecode whatever. If it is Ruby, it should run.
39
+
40
+ How it works
41
+ ------------
42
+
43
+ - Article management is done with a text editor and git
44
+ * stored as _.txt_ files, with embeded metadata (in yaml format).
45
+ * processed through a markdown converter (rdiscount) by default.
46
+ * can have tags
47
+ * can have images
48
+ * can be browsed by date, or tags
49
+ * comments are handled by [disqus](http://disqus.com)
50
+ - built for easy use with _ERB_.
51
+ - built right on top of _Rack_.
52
+ - built to take advantage of _HTTP caching_.
53
+ - built with _heroku_ in mind.
54
+
55
+
56
+ RubySlippers comes with a basic default theme for you to mangle. I hope to release more themes shortly and will accept your submitted themes.
57
+
58
+ Deployment
59
+ ==========
60
+
61
+ #### on heroku
62
+
63
+ RubySlippers comes with a basic rackup file. To start it up locally do:
64
+
65
+ $ cd myblog
66
+ $ bundle
67
+ $ shotgun
68
+ [2011-06-20 17:04:46] INFO WEBrick::HTTPServer#start: pid=61628 port=9393
69
+
70
+ #### on your own server
71
+
72
+ Once you have created the remote git repo, and pushed your changes to it, you can run RubySlippers with any Rack compliant web server, such as **thin**, **mongrel** or **unicorn**.
73
+
74
+ With thin, you would do something like:
75
+
76
+ $ thin start -R config.ru
77
+
78
+ With unicorn, you can just do:
79
+
80
+ $ unicorn
81
+
82
+
83
+ ### Configuration
84
+
85
+ You can configure ruby-slippers, by modifying the _config.ru_ file. For example, if you want to set the blog author to 'John Galt',
86
+ you could add `set :author, 'John Galt'` inside the `RubySlippers::Engine::App.new` block. Here are the defaults, to get you started:
87
+
88
+ set :author, ENV['USER'] # blog author
89
+ set :title, Dir.pwd.split('/').last # site title
90
+ set :url, 'http://example.com' # site root URL
91
+ set :prefix, '' # common path prefix for all pages
92
+ set :root, "index" # page to load on /
93
+ set :date, lambda {|now| now.strftime("%d/%m/%Y") } # date format for articles
94
+ set :markdown, :smart # use markdown + smart-mode
95
+ set :disqus, false # disqus id, or false
96
+ set :summary, :max => 150, :delim => /~\n/ # length of article summary and delimiter
97
+ set :ext, 'txt' # file extension for articles
98
+ set :cache, 28800 # cache site for 8 hours
99
+
100
+ set :to_html do |path, page, ctx| # returns an html, from a path & context
101
+ ERB.new(File.read("#{path}/#{page}.rhtml")).result(ctx)
102
+ end
103
+
104
+ set :error do |code| # The HTML for your error page
105
+ "<font style='font-size:300%'>A large house has landed on you. You cannot continue because you are dead. <a href='/'>try again</a> (#{code})</font>"
106
+ end
107
+
108
+ Thanks
109
+ ------
110
+
111
+ * To heroku for making this easy as pie.
112
+ * To the developers of [Toto](http://github.com/cloudhead/toto), for making such an awesome minimal blog engine in Ruby.
113
+
114
+ Copyright (c) 2011 dreamr. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ Dir.glob('lib/tasks/*.rake').each { |file| import file }
4
+
5
+ task :default => 'test:all'
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
data/lib/ext/ext.rb ADDED
@@ -0,0 +1,57 @@
1
+ class Object
2
+ def meta_def name, &blk
3
+ (class << self; self; end).instance_eval do
4
+ define_method(name, &blk)
5
+ end
6
+ end
7
+ end
8
+
9
+ class String
10
+ def slugize
11
+ self.downcase.gsub(/&/, 'and').gsub(/\s+/, '-').gsub(/[^a-z0-9-]/, '')
12
+ end
13
+
14
+ def humanize
15
+ self.capitalize.gsub(/[-_]+/, ' ')
16
+ end
17
+ end
18
+
19
+ class Fixnum
20
+ def ordinal
21
+ # 1 => 1st
22
+ # 2 => 2nd
23
+ # 3 => 3rd
24
+ # ...
25
+ case self % 100
26
+ when 11..13; "#{self}th"
27
+ else
28
+ case self % 10
29
+ when 1; "#{self}st"
30
+ when 2; "#{self}nd"
31
+ when 3; "#{self}rd"
32
+ else "#{self}th"
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ class Date
39
+ # This check is for people running RubySlippers::Enginewith ActiveSupport, avoid a collision
40
+ unless respond_to? :iso8601
41
+ # Return the date as a String formatted according to ISO 8601.
42
+ def iso8601
43
+ ::Time.utc(year, month, day, 0, 0, 0, 0).iso8601
44
+ end
45
+ end
46
+ end
47
+
48
+ module Kernel
49
+ def silence_stream(stream)
50
+ old_stream = stream.dup
51
+ stream.reopen(RUBY_PLATFORM =~ /mswin/ ? 'NUL:' : '/dev/null')
52
+ stream.sync = true
53
+ yield
54
+ ensure
55
+ stream.reopen(old_stream)
56
+ end
57
+ end
@@ -0,0 +1,41 @@
1
+ module RubySlippers
2
+ module Engine
3
+ class App
4
+ attr_reader :config, :site
5
+
6
+ def initialize config = {}, &blk
7
+ @config = config.is_a?(Config) ? config : Config.new(config)
8
+ @config.instance_eval(&blk) if block_given?
9
+ @site = RubySlippers::Engine::Site.new(@config)
10
+ end
11
+
12
+ def call env
13
+ @request = Rack::Request.new env
14
+ @response = Rack::Response.new
15
+
16
+ return [400, {}, []] unless @request.get?
17
+
18
+ path, mime = @request.path_info.split('.')
19
+ route = (path || '/').split('/').reject {|i| i.empty? }
20
+
21
+ response = @site.go(route, env, *(mime ? mime : []))
22
+
23
+ @response.body = [response[:body]]
24
+ @response['Content-Length'] = response[:body].length.to_s unless response[:body].empty?
25
+ @response['Content-Type'] = Rack::Mime.mime_type(".#{response[:type]}")
26
+
27
+ # Set http cache headers
28
+ @response['Cache-Control'] = if RubySlippers::Engine.env == 'production'
29
+ "public, max-age=#{@config[:cache]}"
30
+ else
31
+ "no-cache, must-revalidate"
32
+ end
33
+
34
+ @response['ETag'] = %("#{Digest::SHA1.hexdigest(response[:body])}")
35
+
36
+ @response.status = response[:status]
37
+ @response.finish
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,22 @@
1
+ module RubySlippers
2
+ module Engine
3
+ class Archives < Array
4
+ include Template
5
+
6
+ def initialize articles, config
7
+ self.replace articles
8
+ @config = config
9
+ end
10
+
11
+ def [] a
12
+ a.is_a?(Range) ? self.class.new(self.slice(a) || [], @config) : super
13
+ end
14
+
15
+ def to_html
16
+ super(:archives, @config)
17
+ end
18
+ alias :to_s to_html
19
+ alias :archive archives
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,84 @@
1
+ require 'ruby_slippers/template'
2
+
3
+ module RubySlippers
4
+ module Engine
5
+ class Article < Hash
6
+ include Template
7
+
8
+ def initialize obj, config = {}
9
+ @obj, @config = obj, config
10
+ self.load if obj.is_a? Hash
11
+ end
12
+
13
+ def load
14
+ data = if @obj.is_a? String
15
+ meta, self[:body] = File.read(@obj).split(/\n\n/, 2)
16
+
17
+ # use the date from the filename, or else ruby-slippers won't find the article
18
+ @obj =~ /\/(\d{4}-\d{2}-\d{2})[^\/]*$/
19
+ ($1 ? {:date => $1} : {}).merge(YAML.load(meta))
20
+ elsif @obj.is_a? Hash
21
+ @obj
22
+ end.inject({}) {|h, (k,v)| h.merge(k.to_sym => v) }
23
+
24
+ self.taint
25
+ self.update data
26
+ self[:date] = Date.parse(self[:date].gsub('/', '-')) rescue Date.today
27
+ self
28
+ end
29
+
30
+ def [] key
31
+ self.load unless self.tainted?
32
+ super
33
+ end
34
+
35
+ def slug
36
+ self[:slug] || self[:title].slugize
37
+ end
38
+
39
+ def summary length = nil
40
+ config = @config[:summary]
41
+ sum = if self[:body] =~ config[:delim]
42
+ self[:body].split(config[:delim]).first
43
+ else
44
+ self[:body].match(/(.{1,#{length || config[:length] || config[:max]}}.*?)(\n|\Z)/m).to_s
45
+ end
46
+ markdown(sum.length == self[:body].length ? sum : sum.strip.sub(/\.\Z/, '&hellip;'))
47
+ end
48
+
49
+ def url
50
+ "http://#{(@config[:url].sub("http://", '') + self.path).squeeze('/')}"
51
+ end
52
+ alias :permalink url
53
+
54
+ def body
55
+ markdown self[:body].sub(@config[:summary][:delim], '') rescue markdown self[:body]
56
+ end
57
+
58
+ def path
59
+ "/#{@config[:prefix]}#{self[:date].strftime("/%Y/%m/%d/#{slug}/")}".squeeze('/')
60
+ end
61
+
62
+ def tags
63
+ return [] unless self[:tags]
64
+ self[:tags].split(',').collect do |tag|
65
+ "#{tag.strip.humanize.downcase}"
66
+ end.join(@config[:tag_separator])
67
+ end
68
+
69
+ def tag_links
70
+ return [] unless self[:tags]
71
+ self[:tags].split(',').collect do |tag|
72
+ "<a href=\"/tagged/#{tag.strip.slugize}\">#{tag.strip.humanize.downcase}</a>"
73
+ end.join(@config[:tag_separator])
74
+ end
75
+
76
+ def title() self[:title] || "an article" end
77
+ def date() @config[:date].call(self[:date]) end
78
+ def author() self[:author] || @config[:author] end
79
+ def image_src() self[:image] end
80
+ def to_html() self.load; super(:article, @config) end
81
+ alias :to_s to_html
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,35 @@
1
+ class RubySlippers::Engine::Config < Hash
2
+ Defaults = {
3
+ :author => ENV['USER'], # blog author
4
+ :title => Dir.pwd.split('/').last, # site title
5
+ :root => "index", # site index
6
+ :url => "http://127.0.0.1", # root URL of the site
7
+ :prefix => "", # common path prefix for the blog
8
+ :date => lambda {|now| now.strftime("%d/%m/%Y") }, # date function
9
+ :markdown => :smart, # use markdown
10
+ :disqus => false, # disqus name
11
+ :summary => {:max => 150, :delim => /~\n/}, # length of summary and delimiter
12
+ :ext => 'txt', # extension for articles
13
+ :cache => 28800, # cache duration (seconds)
14
+ :tag_separator => ', ', # tag separator for articles
15
+ :github => {:user => "dreamr", :repos => [], :ext => 'md'}, # Github username and list of repos
16
+ :to_html => lambda {|path, page, ctx| # returns an html, from a path & context
17
+ ERB.new(File.read("#{path}/#{page}.rhtml")).result(ctx)
18
+ },
19
+ :error => lambda {|code| # The HTML for your error page
20
+ "<font style='font-size:300%'>A large house has landed on you. You cannot continue because you are dead. <a href='/'>try again</a> (#{code})</font>"
21
+ }
22
+ }
23
+ def initialize obj
24
+ self.update Defaults
25
+ self.update obj
26
+ end
27
+
28
+ def set key, val = nil, &blk
29
+ if val.is_a? Hash
30
+ self[key].update val
31
+ else
32
+ self[key] = block_given?? blk : val
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,38 @@
1
+ module RubySlippers
2
+ module Engine
3
+ class Context
4
+ include Template
5
+ attr_reader :env
6
+
7
+ def initialize ctx = {}, config = {}, path = "/", env = {}
8
+ @config, @context, @path, @env = config, ctx, path, env
9
+ @articles = Site.articles(@config[:ext]).reverse.map do |a|
10
+ Article.new(a, @config)
11
+ end
12
+
13
+ ctx.each do |k, v|
14
+ meta_def(k) { ctx.instance_of?(Hash) ? v : ctx.send(k) }
15
+ end
16
+ end
17
+
18
+ def title
19
+ @config[:title]
20
+ end
21
+
22
+ def render page, type
23
+ content = to_html page, @config
24
+ type == :html ? to_html(:layout, @config, &Proc.new { content }) : send(:"to_#{type}", page)
25
+ end
26
+
27
+ def to_xml page
28
+ xml = Builder::XmlMarkup.new(:indent => 2)
29
+ instance_eval File.read("#{Paths[:templates]}/#{page}.builder")
30
+ end
31
+ alias :to_atom to_xml
32
+
33
+ def method_missing m, *args, &blk
34
+ @context.respond_to?(m) ? @context.send(m, *args, &blk) : super
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,21 @@
1
+ module RubySlippers
2
+ module Engine
3
+ Paths = {
4
+ :templates => "templates",
5
+ :pages => "templates/pages",
6
+ :articles => "articles"
7
+ } unless defined?(Paths)
8
+
9
+ def self.gem_root
10
+ File.expand_path("../../", __FILE__)
11
+ end
12
+
13
+ def self.env
14
+ ENV['RACK_ENV'] || 'production'
15
+ end
16
+
17
+ def self.env= env
18
+ ENV['RACK_ENV'] = env
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ module RubySlippers
2
+ module Engine
3
+ class Repo < Hash
4
+ include Template
5
+
6
+ README = "https://github.com/%s/%s/raw/master/README.%s"
7
+
8
+ def initialize name, config
9
+ self[:name], @config = name, config
10
+ end
11
+
12
+ def readme
13
+ markdown open(README %
14
+ [@config[:github][:user], self[:name], @config[:github][:ext]]).read
15
+ rescue Timeout::Error, OpenURI::HTTPError => e
16
+ "This page isn't available."
17
+ end
18
+ alias :content readme
19
+ end
20
+ end
21
+ end