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/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
+