glinda 0.1.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/.document +5 -0
- data/.rspec +2 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +71 -0
- data/Glinda.gemspec +91 -0
- data/LICENSE +20 -0
- data/README.md +166 -0
- data/Rakefile +31 -0
- data/VERSION +1 -0
- data/lib/ext/ext.rb +52 -0
- data/lib/glinda.rb +358 -0
- data/spec/articles/1900-05-17-the-wonderful-wizard-of-oz.txt +5 -0
- data/spec/articles/2001-01-01-two-thousand-and-one.txt +5 -0
- data/spec/articles/2009-04-01-tilt-factor.txt +5 -0
- data/spec/articles/2009-12-04-some-random-article.txt +5 -0
- data/spec/articles/2009-12-11-the-dichotomy-of-design.txt +5 -0
- data/spec/glinda_spec.rb +466 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/templates/about.rhtml +1 -0
- data/spec/templates/archives.rhtml +5 -0
- data/spec/templates/article.rhtml +4 -0
- data/spec/templates/feed.builder +21 -0
- data/spec/templates/index.builder +21 -0
- data/spec/templates/index.rhtml +13 -0
- data/spec/templates/layout.rhtml +4 -0
- data/spec/templates/repo.rhtml +1 -0
- metadata +235 -0
data/lib/glinda.rb
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
require 'date'
|
|
3
|
+
require 'erb'
|
|
4
|
+
require 'rack'
|
|
5
|
+
require 'digest'
|
|
6
|
+
require 'open-uri'
|
|
7
|
+
|
|
8
|
+
if RUBY_PLATFORM =~ /win32/
|
|
9
|
+
require 'maruku'
|
|
10
|
+
Markdown = Maruku
|
|
11
|
+
else
|
|
12
|
+
require 'rdiscount'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
require 'builder'
|
|
16
|
+
|
|
17
|
+
$:.unshift File.dirname(__FILE__)
|
|
18
|
+
|
|
19
|
+
require 'ext/ext'
|
|
20
|
+
|
|
21
|
+
module Glinda
|
|
22
|
+
Paths = {
|
|
23
|
+
:templates => "templates",
|
|
24
|
+
:pages => "templates/pages",
|
|
25
|
+
:articles => "articles"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def self.env
|
|
29
|
+
ENV['RACK_ENV'] || 'production'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.env= env
|
|
33
|
+
ENV['RACK_ENV'] = env
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
module Template
|
|
37
|
+
def to_html page, config, &blk
|
|
38
|
+
path = ([:layout, :repo].include?(page) ? Paths[:templates] : Paths[:pages])
|
|
39
|
+
config[:to_html].call(path, page, binding)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def markdown text
|
|
43
|
+
if (options = @config[:markdown])
|
|
44
|
+
Markdown.new(text.to_s.strip, *(options.eql?(true) ? [] : options)).to_html
|
|
45
|
+
else
|
|
46
|
+
text.strip
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def method_missing m, *args, &blk
|
|
51
|
+
self.keys.include?(m) ? self[m] : super
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.included obj
|
|
55
|
+
obj.class_eval do
|
|
56
|
+
define_method(obj.to_s.split('::').last.downcase) { self }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class Site
|
|
62
|
+
def initialize config
|
|
63
|
+
@config = config
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def [] *args
|
|
67
|
+
@config[*args]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def []= key, value
|
|
71
|
+
@config.set key, value
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def index type = :html
|
|
75
|
+
articles = type == :html ? self.articles.reverse : self.articles
|
|
76
|
+
{:articles => articles.map do |article|
|
|
77
|
+
Article.new article, @config
|
|
78
|
+
end}.merge archives
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def archives filter = ""
|
|
82
|
+
entries = ! self.articles.empty??
|
|
83
|
+
self.articles.select do |a|
|
|
84
|
+
filter !~ /^\d{4}/ || File.basename(a) =~ /^#{filter}/
|
|
85
|
+
end.reverse.map do |article|
|
|
86
|
+
Article.new article, @config
|
|
87
|
+
end : []
|
|
88
|
+
|
|
89
|
+
return :archives => Archives.new(entries, @config)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def article route
|
|
93
|
+
Article.new("#{Paths[:articles]}/#{route.join('-')}.#{self[:ext]}", @config).load
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def /
|
|
97
|
+
self[:root]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def go route, env = {}, type = :html
|
|
101
|
+
route << self./ if route.empty?
|
|
102
|
+
type, path = type =~ /html|xml|json/ ? type.to_sym : :html, route.join('/')
|
|
103
|
+
context = lambda do |data, page|
|
|
104
|
+
Context.new(data, @config, path, env).render(page, type)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
body, status = if Context.new.respond_to?(:"to_#{type}")
|
|
108
|
+
if route.first =~ /\d{4}/
|
|
109
|
+
case route.size
|
|
110
|
+
when 1..3
|
|
111
|
+
context[archives(route * '-'), :archives]
|
|
112
|
+
when 4
|
|
113
|
+
context[article(route), :article]
|
|
114
|
+
else http 400
|
|
115
|
+
end
|
|
116
|
+
elsif respond_to?(path)
|
|
117
|
+
context[send(path, type), path.to_sym]
|
|
118
|
+
elsif (repo = @config[:github][:repos].grep(/#{path}/).first) &&
|
|
119
|
+
!@config[:github][:user].empty?
|
|
120
|
+
context[Repo.new(repo, @config), :repo]
|
|
121
|
+
else
|
|
122
|
+
context[{}, path.to_sym]
|
|
123
|
+
end
|
|
124
|
+
else
|
|
125
|
+
http 400
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
rescue Errno::ENOENT => e
|
|
129
|
+
return :body => http(404).first, :type => :html, :status => 404
|
|
130
|
+
else
|
|
131
|
+
return :body => body || "", :type => type, :status => status || 200
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
protected
|
|
135
|
+
|
|
136
|
+
def http code
|
|
137
|
+
[@config[:error].call(code), code]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def articles
|
|
141
|
+
self.class.articles self[:ext]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def self.articles ext
|
|
145
|
+
Dir["#{Paths[:articles]}/*.#{ext}"].sort_by {|entry| File.basename(entry) }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
class Context
|
|
149
|
+
include Template
|
|
150
|
+
attr_reader :env
|
|
151
|
+
|
|
152
|
+
def initialize ctx = {}, config = {}, path = "/", env = {}
|
|
153
|
+
@config, @context, @path, @env = config, ctx, path, env
|
|
154
|
+
@articles = Site.articles(@config[:ext]).reverse.map do |a|
|
|
155
|
+
Article.new(a, @config)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
ctx.each do |k, v|
|
|
159
|
+
meta_def(k) { ctx.instance_of?(Hash) ? v : ctx.send(k) }
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def title
|
|
164
|
+
@config[:title]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def render page, type
|
|
168
|
+
content = to_html page, @config
|
|
169
|
+
type == :html ? to_html(:layout, @config, &Proc.new { content }) : send(:"to_#{type}", page)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def to_xml page
|
|
173
|
+
xml = Builder::XmlMarkup.new(:indent => 2)
|
|
174
|
+
instance_eval File.read("#{Paths[:templates]}/#{page}.builder")
|
|
175
|
+
end
|
|
176
|
+
alias :to_atom to_xml
|
|
177
|
+
|
|
178
|
+
def method_missing m, *args, &blk
|
|
179
|
+
@context.respond_to?(m) ? @context.send(m, *args, &blk) : super
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
class Repo < Hash
|
|
185
|
+
include Template
|
|
186
|
+
|
|
187
|
+
README = "https://github.com/%s/%s/raw/master/README.%s"
|
|
188
|
+
|
|
189
|
+
def initialize name, config
|
|
190
|
+
self[:name], @config = name, config
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def readme
|
|
194
|
+
markdown open(README %
|
|
195
|
+
[@config[:github][:user], self[:name], @config[:github][:ext]]).read
|
|
196
|
+
rescue Timeout::Error, OpenURI::HTTPError => e
|
|
197
|
+
"This page isn't available."
|
|
198
|
+
end
|
|
199
|
+
alias :content readme
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
class Archives < Array
|
|
203
|
+
include Template
|
|
204
|
+
|
|
205
|
+
def initialize articles, config
|
|
206
|
+
self.replace articles
|
|
207
|
+
@config = config
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def [] a
|
|
211
|
+
a.is_a?(Range) ? self.class.new(self.slice(a) || [], @config) : super
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def to_html
|
|
215
|
+
super(:archives, @config)
|
|
216
|
+
end
|
|
217
|
+
alias :to_s to_html
|
|
218
|
+
alias :archive archives
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
class Article < Hash
|
|
222
|
+
include Template
|
|
223
|
+
|
|
224
|
+
def initialize obj, config = {}
|
|
225
|
+
@obj, @config = obj, config
|
|
226
|
+
self.load if obj.is_a? Hash
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def load
|
|
230
|
+
data = if @obj.is_a? String
|
|
231
|
+
meta, self[:body] = File.read(@obj).split(/\n\n/, 2)
|
|
232
|
+
|
|
233
|
+
# use the date from the filename, or else tinman won't find the article
|
|
234
|
+
@obj =~ /\/(\d{4}-\d{2}-\d{2})[^\/]*$/
|
|
235
|
+
($1 ? {:date => $1} : {}).merge(YAML.load(meta))
|
|
236
|
+
elsif @obj.is_a? Hash
|
|
237
|
+
@obj
|
|
238
|
+
end.inject({}) {|h, (k,v)| h.merge(k.to_sym => v) }
|
|
239
|
+
|
|
240
|
+
self.taint
|
|
241
|
+
self.update data
|
|
242
|
+
self[:date] = Date.parse(self[:date].gsub('/', '-')) rescue Date.today
|
|
243
|
+
self
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def [] key
|
|
247
|
+
self.load unless self.tainted?
|
|
248
|
+
super
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def slug
|
|
252
|
+
self[:slug] || self[:title].slugize
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def summary length = nil
|
|
256
|
+
config = @config[:summary]
|
|
257
|
+
sum = if self[:body] =~ config[:delim]
|
|
258
|
+
self[:body].split(config[:delim]).first
|
|
259
|
+
else
|
|
260
|
+
self[:body].match(/(.{1,#{length || config[:length] || config[:max]}}.*?)(\n|\Z)/m).to_s
|
|
261
|
+
end
|
|
262
|
+
markdown(sum.length == self[:body].length ? sum : sum.strip.sub(/\.\Z/, '…'))
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def url
|
|
266
|
+
"http://#{(@config[:url].sub("http://", '') + self.path).squeeze('/')}"
|
|
267
|
+
end
|
|
268
|
+
alias :permalink url
|
|
269
|
+
|
|
270
|
+
def body
|
|
271
|
+
markdown self[:body].sub(@config[:summary][:delim], '') rescue markdown self[:body]
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def path
|
|
275
|
+
"/#{@config[:prefix]}#{self[:date].strftime("/%Y/%m/%d/#{slug}/")}".squeeze('/')
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def title() self[:title] || "an article" end
|
|
279
|
+
def date() @config[:date].call(self[:date]) end
|
|
280
|
+
def author() self[:author] || @config[:author] end
|
|
281
|
+
def to_html() self.load; super(:article, @config) end
|
|
282
|
+
alias :to_s to_html
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
class Config < Hash
|
|
286
|
+
Defaults = {
|
|
287
|
+
:author => ENV['USER'], # blog author
|
|
288
|
+
:title => Dir.pwd.split('/').last, # site title
|
|
289
|
+
:root => "index", # site index
|
|
290
|
+
:url => "http://127.0.0.1", # root URL of the site
|
|
291
|
+
:prefix => "", # common path prefix for the blog
|
|
292
|
+
:date => lambda {|now| now.strftime("%d/%m/%Y") }, # date function
|
|
293
|
+
:markdown => :smart, # use markdown
|
|
294
|
+
:disqus => false, # disqus name
|
|
295
|
+
:summary => {:max => 150, :delim => /~\n/}, # length of summary and delimiter
|
|
296
|
+
:ext => 'txt', # extension for articles
|
|
297
|
+
:cache => 28800, # cache duration (seconds)
|
|
298
|
+
:github => {:user => "", :repos => [], :ext => 'md'}, # Github username and list of repos
|
|
299
|
+
:to_html => lambda {|path, page, ctx| # returns an html, from a path & context
|
|
300
|
+
ERB.new(File.read("#{path}/#{page}.rhtml")).result(ctx)
|
|
301
|
+
},
|
|
302
|
+
:error => lambda {|code| # The HTML for your error page
|
|
303
|
+
"<font style='font-size:300%'>Lions, Tigers and Bears. Oh My! Something went wrong.(#{code})</font>"
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
def initialize obj
|
|
307
|
+
self.update Defaults
|
|
308
|
+
self.update obj
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def set key, val = nil, &blk
|
|
312
|
+
if val.is_a? Hash
|
|
313
|
+
self[key].update val
|
|
314
|
+
else
|
|
315
|
+
self[key] = block_given?? blk : val
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
class Server
|
|
321
|
+
attr_reader :config, :site
|
|
322
|
+
|
|
323
|
+
def initialize config = {}, &blk
|
|
324
|
+
@config = config.is_a?(Config) ? config : Config.new(config)
|
|
325
|
+
@config.instance_eval(&blk) if block_given?
|
|
326
|
+
@site = Glinda::Site.new(@config)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def call env
|
|
330
|
+
@request = Rack::Request.new env
|
|
331
|
+
@response = Rack::Response.new
|
|
332
|
+
|
|
333
|
+
return [400, {}, []] unless @request.get?
|
|
334
|
+
|
|
335
|
+
path, mime = @request.path_info.split('.')
|
|
336
|
+
route = (path || '/').split('/').reject {|i| i.empty? }
|
|
337
|
+
|
|
338
|
+
response = @site.go(route, env, *(mime ? mime : []))
|
|
339
|
+
|
|
340
|
+
@response.body = [response[:body]]
|
|
341
|
+
@response['Content-Length'] = response[:body].bytesize.to_s unless response[:body].empty?
|
|
342
|
+
@response['Content-Type'] = Rack::Mime.mime_type(".#{response[:type]}")
|
|
343
|
+
|
|
344
|
+
# Set http cache headers
|
|
345
|
+
@response['Cache-Control'] = if Glinda.env == 'production'
|
|
346
|
+
"public, max-age=#{@config[:cache]}"
|
|
347
|
+
else
|
|
348
|
+
"no-cache, must-revalidate"
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
@response['ETag'] = %("#{Digest::SHA1.hexdigest(response[:body])}")
|
|
352
|
+
|
|
353
|
+
@response.status = response[:status]
|
|
354
|
+
@response.finish
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|