glinda 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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/, '&hellip;'))
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
+
@@ -0,0 +1,5 @@
1
+ title: The Wonderful Wizard of Oz
2
+ date: 17/05/1900
3
+
4
+ _Once upon a time_...
5
+
@@ -0,0 +1,5 @@
1
+ title: the wizard of oz
2
+ date: 12/10/1932
3
+
4
+ Once upon a time...
5
+
@@ -0,0 +1,5 @@
1
+ title: the wizard of oz
2
+ date: 12/10/1932
3
+
4
+ Once upon a time...
5
+
@@ -0,0 +1,5 @@
1
+ title: the wizard of oz
2
+ date: 12/10/1932
3
+
4
+ Once upon a time...
5
+
@@ -0,0 +1,5 @@
1
+ title: the wizard of oz
2
+ date: 12/10/1932
3
+
4
+ Once upon a time...
5
+