ruby_slippers 0.2.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.
Files changed (51) hide show
  1. data/.rvmrc +2 -0
  2. data/Gemfile +16 -0
  3. data/Gemfile.lock +45 -0
  4. data/Guardfile +16 -0
  5. data/LICENSE +20 -0
  6. data/README.md +122 -0
  7. data/Rakefile +5 -0
  8. data/TODO +32 -0
  9. data/VERSION +1 -0
  10. data/lib/ext/ext.rb +164 -0
  11. data/lib/ruby_slippers.rb +28 -0
  12. data/lib/ruby_slippers/app.rb +47 -0
  13. data/lib/ruby_slippers/archives.rb +22 -0
  14. data/lib/ruby_slippers/article.rb +104 -0
  15. data/lib/ruby_slippers/config.rb +37 -0
  16. data/lib/ruby_slippers/context.rb +38 -0
  17. data/lib/ruby_slippers/engine.rb +21 -0
  18. data/lib/ruby_slippers/repo.rb +21 -0
  19. data/lib/ruby_slippers/site.rb +115 -0
  20. data/lib/ruby_slippers/template.rb +28 -0
  21. data/lib/tasks/gemspec.rake +24 -0
  22. data/lib/tasks/test.rake +17 -0
  23. data/test/fixtures/articles/2010-05-17-the-wonderful-wizard-of-oz.txt +6 -0
  24. data/test/fixtures/articles/2010-05-18-the-marvelous-land-of-oz.txt +7 -0
  25. data/test/fixtures/articles/2010-05-20-dorothy-and-the-wizard-of-oz.txt +6 -0
  26. data/test/fixtures/articles/2011-05-18-ozma-of-oz.txt +10 -0
  27. data/test/fixtures/images/ozma.png +0 -0
  28. data/test/fixtures/pages/about.html.erb +1 -0
  29. data/test/fixtures/pages/archives.html.erb +23 -0
  30. data/test/fixtures/pages/article.html.erb +28 -0
  31. data/test/fixtures/pages/index.html.erb +27 -0
  32. data/test/fixtures/pages/sitemap.html.erb +0 -0
  33. data/test/fixtures/pages/tagged.html.erb +9 -0
  34. data/test/fixtures/templates/index.builder +21 -0
  35. data/test/fixtures/templates/layout.html.erb +4 -0
  36. data/test/fixtures/templates/repo.html.erb +1 -0
  37. data/test/fixtures/templates/sitemap.builder +25 -0
  38. data/test/integration/about_test.rb +23 -0
  39. data/test/integration/archives_test.rb +34 -0
  40. data/test/integration/articles_test.rb +56 -0
  41. data/test/integration/atom_test.rb +24 -0
  42. data/test/integration/invalid_route_test.rb +31 -0
  43. data/test/integration/tags_test.rb +24 -0
  44. data/test/support/test_helper.rb +47 -0
  45. data/test/unit/app_test.rb +14 -0
  46. data/test/unit/archives_test.rb +10 -0
  47. data/test/unit/article_test.rb +145 -0
  48. data/test/unit/date_patch_test.rb +10 -0
  49. data/test/unit/engine_test.rb +71 -0
  50. data/test/unit/site_test.rb +52 -0
  51. metadata +270 -0
@@ -0,0 +1,47 @@
1
+ module RubySlippers
2
+ module Engine
3
+ class App
4
+ attr_reader :config, :site
5
+
6
+ @@site = nil
7
+
8
+ def initialize config = {}, &blk
9
+ @config = config.is_a?(Config) ? config : Config.new(config)
10
+ @config.instance_eval(&blk) if block_given?
11
+ @site ||= @@site = Site.new(@config)
12
+ end
13
+
14
+ def self.site
15
+ @@site
16
+ end
17
+
18
+ def call env
19
+ @request = Rack::Request.new env
20
+ @response = Rack::Response.new
21
+
22
+ return [400, {}, []] unless @request.get?
23
+
24
+ path, mime = @request.path_info.split('.')
25
+ route = (path || '/').split('/').reject {|i| i.empty? }
26
+
27
+ response = @site.go(route, env, *(mime ? mime : []))
28
+
29
+ @response.body = [response[:body]]
30
+ @response['Content-Length'] = response[:body].length.to_s unless response[:body].empty?
31
+ @response['Content-Type'] = Rack::Mime.mime_type(".#{response[:type]}")
32
+
33
+ # Set http cache headers
34
+ @response['Cache-Control'] = if RubySlippers::Engine.env == 'production'
35
+ "public, max-age=#{@config[:cache]}"
36
+ else
37
+ "no-cache, must-revalidate"
38
+ end
39
+
40
+ @response['ETag'] = %("#{Digest::SHA1.hexdigest(response[:body])}")
41
+
42
+ @response.status = response[:status]
43
+ @response.finish
44
+ end
45
+ end
46
+ end
47
+ 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,104 @@
1
+ require 'ruby_slippers/template'
2
+
3
+ module RubySlippers::Engine
4
+
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 thumb
36
+ "/img/archives/#{article.slug}-full.png"
37
+ end
38
+
39
+ def slug
40
+ self[:slug] || self[:title].slugize
41
+ end
42
+
43
+ def summary length = nil
44
+ config = @config[:summary]
45
+ sum = if self[:body] =~ config[:delim]
46
+ self[:body].split(config[:delim]).first
47
+ else
48
+ self[:body].match(/(.{1,#{length || config[:length] || config[:max]}}.*?)(\n|\Z)/m).to_s
49
+ end
50
+ markdown(sum.length >= self[:body].length-1 ? sum : sum.strip.sub(/\.\Z/, '&hellip;'))
51
+ end
52
+
53
+ def url
54
+ "http://#{(@config[:url].sub("http://", '') + self.path).squeeze('/')}"
55
+ end
56
+ alias :permalink url
57
+
58
+ def body
59
+ markdown self[:body].sub(@config[:summary][:delim], '') rescue markdown self[:body]
60
+ end
61
+
62
+ def path
63
+ "/#{@config[:prefix]}#{self[:date].strftime("/%Y/%m/%d/#{slug}/")}".squeeze('/')
64
+ end
65
+
66
+ def tags
67
+ return [] unless self[:tags]
68
+ self[:tags].split(',').collect do |tag|
69
+ "#{tag.strip.humanize.downcase}"
70
+ end.join(@config[:tag_separator])
71
+ end
72
+
73
+ def tag_links
74
+ return '' unless self[:tags]
75
+ self[:tags].split(',').collect do |tag|
76
+ "<a href=\"/tagged/#{tag.strip.slugize}\">#{tag.strip.humanize.downcase}</a>"
77
+ end.join(@config[:tag_separator])
78
+ end
79
+
80
+ def related_articles
81
+ Site.send(:articles, :txt).each do |article|
82
+ raise article.to_yaml
83
+ Article.new article, @config
84
+ end
85
+ end
86
+
87
+ def full_image_path
88
+ return nil unless self[:image]
89
+ self[:date].strftime("/img/articles/%Y/%B/#{self[:image]}").downcase
90
+ end
91
+
92
+ def has_more_then_summary?
93
+ self[:body].length > self.summary.length
94
+ end
95
+
96
+ def title() self[:title] || "an article" end
97
+ def date() @config[:date].call(self[:date]) end
98
+ def author() self[:author] || @config[:author] end
99
+ def image_src() full_image_path end
100
+ def to_html() self.load; super(:article, @config) end
101
+ alias :to_s to_html
102
+
103
+ end
104
+ end
@@ -0,0 +1,37 @@
1
+ module RubySlippers::Engine
2
+ class Config < Hash
3
+ Defaults = {
4
+ :author => ENV['USER'], # blog author
5
+ :title => Dir.pwd.split('/').last, # site title
6
+ :root => "index", # site index
7
+ :url => "http://127.0.0.1", # root URL of the site
8
+ :prefix => "", # common path prefix for the blog
9
+ :date => lambda {|now| now.strftime("%d/%m/%Y") }, # date function
10
+ :markdown => :smart, # use markdown
11
+ :disqus => false, # disqus name
12
+ :summary => {:max => 150, :delim => /~\n/}, # length of summary and delimiter
13
+ :ext => 'txt', # extension for articles
14
+ :cache => 28800, # cache duration (seconds)
15
+ :tag_separator => ', ', # tag separator for articles
16
+ :github => {:user => "dreamr", :repos => [], :ext => 'md'}, # Github username and list of repos
17
+ :to_html => lambda {|path, page, ctx| # returns an html, from a path & context
18
+ ERB.new(File.read("#{path}/#{page}.html.erb")).result(ctx)
19
+ },
20
+ :error => lambda {|code| # The HTML for your error page
21
+ "<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>"
22
+ }
23
+ }
24
+ def initialize obj
25
+ self.update Defaults
26
+ self.update obj
27
+ end
28
+
29
+ def set key, val = nil, &blk
30
+ if val.is_a? Hash
31
+ self[key].update val
32
+ else
33
+ self[key] = block_given?? blk : val
34
+ end
35
+ end
36
+ end
37
+ 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
@@ -0,0 +1,115 @@
1
+ module RubySlippers
2
+ module Engine
3
+
4
+ class Site
5
+ def initialize config
6
+ @config = config
7
+ end
8
+
9
+ def config
10
+ @config
11
+ end
12
+
13
+ def [] *args
14
+ @config[*args]
15
+ end
16
+
17
+ def []= key, value
18
+ @config.set key, value
19
+ end
20
+
21
+ def articles
22
+ self.class.articles self[:ext]
23
+ end
24
+
25
+ def sitemap type = :xml
26
+ articles = type == :html ? self.articles.reverse : self.articles
27
+ {:articles => articles.map do |article|
28
+ Article.new article, @config
29
+ end}.merge archives
30
+ end
31
+
32
+ def index type = :html
33
+ articles = type == :html ? self.articles.reverse : self.articles
34
+ {:articles => articles.map do |article|
35
+ Article.new article, @config
36
+ end}.merge archives
37
+ end
38
+
39
+ def archives filter = ""
40
+ entries = ! self.articles.empty??
41
+ self.articles.select do |a|
42
+ filter !~ /^\d{4}/ || File.basename(a) =~ /^#{filter}/
43
+ end.reverse.map do |article|
44
+ Article.new article, @config
45
+ end : []
46
+
47
+ return :archives => Archives.new(entries, @config)
48
+ end
49
+
50
+ def article route
51
+ Article.new("#{Paths[:articles]}/#{route.join('-')}.#{self[:ext]}", @config).load
52
+ end
53
+
54
+ def tagged tag
55
+ articles = self.articles.collect do |article|
56
+ Article.new article, @config
57
+ end.select do |article|
58
+ article[:tags].index(tag.humanize.downcase) if article[:tags]
59
+ end
60
+
61
+ {:articles => articles, :tagged => tag}
62
+ end
63
+
64
+ def /
65
+ self[:root]
66
+ end
67
+
68
+ def go route, env = {}, type = :html
69
+ route << self./ if route.empty?
70
+ type, path = type =~ /html|xml|json/ ? type.to_sym : :html, route.join('/')
71
+ context = lambda do |data, page|
72
+ Context.new(data, @config, path, env).render(page, type)
73
+ end
74
+
75
+ body, status = if Context.new.respond_to?(:"to_#{type}")
76
+ if route.first =~ /\d{4}/
77
+ case route.size
78
+ when 1..3
79
+ context[archives(route * '-'), :archives]
80
+ when 4
81
+ context[article(route), :article]
82
+ else http 400
83
+ end
84
+ elsif route.first == "tagged"
85
+ context[tagged(route.last), :tagged]
86
+ elsif respond_to?(path)
87
+ context[send(path, type), path.to_sym]
88
+ elsif (repo = @config[:github][:repos].grep(/#{path}/).first) &&
89
+ !@config[:github][:user].empty?
90
+ context[Repo.new(repo, @config), :repo]
91
+ else
92
+ context[{}, path.to_sym]
93
+ end
94
+ else
95
+ http 400
96
+ end
97
+
98
+ rescue NoMethodError, Errno::ENOENT => e
99
+ return :body => http(404).first, :type => :html, :status => 404
100
+ else
101
+ return :body => body || "", :type => type, :status => status || 200
102
+ end
103
+
104
+ protected
105
+
106
+ def http code
107
+ [@config[:error].call(code), code]
108
+ end
109
+
110
+ def self.articles ext
111
+ Dir["#{Paths[:articles]}/*.#{ext}"].sort_by {|entry| File.basename(entry) }
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,28 @@
1
+ module RubySlippers
2
+ module Engine
3
+ module Template
4
+ def to_html page, config, &blk
5
+ path = ([:layout, :repo].include?(page) ? Paths[:templates] : Paths[:pages])
6
+ config[:to_html].call(path, page, binding)
7
+ end
8
+
9
+ def markdown text
10
+ if (options = @config[:markdown])
11
+ Markdown.new(text.to_s.strip, *(options.eql?(true) ? [] : options)).to_html
12
+ else
13
+ text.strip
14
+ end
15
+ end
16
+
17
+ def method_missing m, *args, &blk
18
+ self.keys.include?(m) ? self[m] : super
19
+ end
20
+
21
+ def self.included obj
22
+ obj.class_eval do
23
+ define_method(obj.to_s.split('::').last.downcase) { self }
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ namespace :gem do
2
+ begin
3
+ require 'jeweler'
4
+ Jeweler::Tasks.new do |gem|
5
+ gem.name = "ruby_slippers"
6
+ gem.summary = %Q{the smartest blog-engine in all of Oz}
7
+ gem.description = %Q{A ruby and rack based blog engine for heroku}
8
+ gem.email = "james@rubyloves.me"
9
+ gem.homepage = "http://github.com/dreamr/ruby_slippers"
10
+ gem.authors = ["dreamr", "cloudhead"]
11
+ gem.add_development_dependency "riot"
12
+ gem.add_dependency "builder"
13
+ gem.add_dependency "rack"
14
+ if RUBY_PLATFORM =~ /win32/
15
+ gem.add_dependency "maruku"
16
+ else
17
+ gem.add_dependency "rdiscount"
18
+ end
19
+ end
20
+ Jeweler::GemcutterTasks.new
21
+ rescue LoadError
22
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
23
+ end
24
+ end