ruby-slippers 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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