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.
- data/.rvmrc +2 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +45 -0
- data/Guardfile +16 -0
- data/LICENSE +20 -0
- data/README.md +122 -0
- data/Rakefile +5 -0
- data/TODO +32 -0
- data/VERSION +1 -0
- data/lib/ext/ext.rb +164 -0
- data/lib/ruby_slippers.rb +28 -0
- data/lib/ruby_slippers/app.rb +47 -0
- data/lib/ruby_slippers/archives.rb +22 -0
- data/lib/ruby_slippers/article.rb +104 -0
- data/lib/ruby_slippers/config.rb +37 -0
- data/lib/ruby_slippers/context.rb +38 -0
- data/lib/ruby_slippers/engine.rb +21 -0
- data/lib/ruby_slippers/repo.rb +21 -0
- data/lib/ruby_slippers/site.rb +115 -0
- data/lib/ruby_slippers/template.rb +28 -0
- data/lib/tasks/gemspec.rake +24 -0
- data/lib/tasks/test.rake +17 -0
- data/test/fixtures/articles/2010-05-17-the-wonderful-wizard-of-oz.txt +6 -0
- data/test/fixtures/articles/2010-05-18-the-marvelous-land-of-oz.txt +7 -0
- data/test/fixtures/articles/2010-05-20-dorothy-and-the-wizard-of-oz.txt +6 -0
- data/test/fixtures/articles/2011-05-18-ozma-of-oz.txt +10 -0
- data/test/fixtures/images/ozma.png +0 -0
- data/test/fixtures/pages/about.html.erb +1 -0
- data/test/fixtures/pages/archives.html.erb +23 -0
- data/test/fixtures/pages/article.html.erb +28 -0
- data/test/fixtures/pages/index.html.erb +27 -0
- data/test/fixtures/pages/sitemap.html.erb +0 -0
- data/test/fixtures/pages/tagged.html.erb +9 -0
- data/test/fixtures/templates/index.builder +21 -0
- data/test/fixtures/templates/layout.html.erb +4 -0
- data/test/fixtures/templates/repo.html.erb +1 -0
- data/test/fixtures/templates/sitemap.builder +25 -0
- data/test/integration/about_test.rb +23 -0
- data/test/integration/archives_test.rb +34 -0
- data/test/integration/articles_test.rb +56 -0
- data/test/integration/atom_test.rb +24 -0
- data/test/integration/invalid_route_test.rb +31 -0
- data/test/integration/tags_test.rb +24 -0
- data/test/support/test_helper.rb +47 -0
- data/test/unit/app_test.rb +14 -0
- data/test/unit/archives_test.rb +10 -0
- data/test/unit/article_test.rb +145 -0
- data/test/unit/date_patch_test.rb +10 -0
- data/test/unit/engine_test.rb +71 -0
- data/test/unit/site_test.rb +52 -0
- 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/, '…'))
|
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
|