ceilingfish-toto 0.3.6 → 0.3.7

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