ruby_slippers 0.2.0

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