toto-bongo 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/toto-bongo.rb ADDED
@@ -0,0 +1,543 @@
1
+ require 'yaml'
2
+ require 'date'
3
+ require 'haml'
4
+ require 'rack'
5
+ require 'digest'
6
+ require 'open-uri'
7
+ require 'RedCloth'
8
+ require 'builder'
9
+ require 'logger'
10
+ $:.unshift File.dirname(__FILE__)
11
+
12
+ require 'ext/ext'
13
+
14
+ #
15
+ # TotoBongo
16
+ #
17
+ # TotoBongo Consits of the following
18
+ #
19
+ # Module TotoBongo
20
+ # encapsulates the app
21
+ #
22
+ # Module Template
23
+ # Handles the conversions to html
24
+ #
25
+ #
26
+ module TotoBongo
27
+
28
+ #default paths
29
+ Paths = {
30
+ :templates => "templates",
31
+ :pages => "templates/pages",
32
+ :articles => "articles"
33
+ }
34
+
35
+ def self.env
36
+ ENV['RACK_ENV'] || 'production'
37
+ end
38
+
39
+ def self.env= env
40
+ ENV['RACK_ENV'] = env
41
+ end
42
+
43
+ class << self
44
+ attr_accessor :logger
45
+ end
46
+
47
+ @logger = Logger.new(STDOUT)
48
+ @logger.level = Logger::WARN
49
+
50
+ #set logger for debug
51
+ if(ENV['TOTODEBUG'])
52
+ @logger.level = Logger::DEBUG
53
+ end
54
+
55
+ #
56
+ # Handles all templating options
57
+ # Is responsible for:
58
+ # 1. Calling the Haml engine on pages to render them to html
59
+ # 2. Calling the Textile engine on textile text to render them to html
60
+ # 3. Registering All the classes at initialization
61
+ #
62
+ module Template
63
+ #
64
+ # This will call Haml render
65
+ # Call the config block to make convert the
66
+ # page to html
67
+ #
68
+ #
69
+ def to_html page, config, &blk
70
+ TotoBongo::logger.debug("Called Template::to_html")
71
+ path = ([:layout, :repo].include?(page) ? Paths[:templates] : Paths[:pages])
72
+ result = config[:to_html].call(path, page, binding)
73
+ end
74
+
75
+ #
76
+ #Converst a textile text into html
77
+ #
78
+ def textile text
79
+ TotoBongo::logger.debug("Called Template::Textile")
80
+ RedCloth.new(text.to_s.strip).to_html
81
+ end
82
+
83
+ #
84
+ # Intercept any method missing
85
+ #
86
+ def method_missing m, *args, &blk
87
+ TotoBongo::logger.debug("Called method_missing: method = #{method_missin}")
88
+ self.keys.include?(m) ? self[m] : super
89
+ end
90
+
91
+
92
+ # define the following methods during initialization
93
+ # TotoBongo::Site::Context
94
+ # TotoBongo::Repo
95
+ # TotoBongo::Archives
96
+ # TotoBongo::Article
97
+ #
98
+ def self.included obj
99
+ TotoBongo::logger.debug("Called Template::include: obj = #{obj}")
100
+ obj.class_eval do
101
+ define_method(obj.to_s.split('::').last.downcase) { self }
102
+ end
103
+ end
104
+
105
+ end #Template
106
+
107
+
108
+
109
+ # Site
110
+ # Is responsible for handling the site
111
+ # It has handles the
112
+ #
113
+ #
114
+ class Site
115
+
116
+ def initialize config
117
+ TotoBongo::logger.debug("Called Site::initialize")
118
+ @config = config
119
+ end
120
+
121
+ def [] *args
122
+ TotoBongo::logger.debug("Called Site::[]: args = #{args}")
123
+ @config[*args]
124
+ end
125
+
126
+ def []= key, value
127
+ TotoBongo::logger.debug("Called Site::[]=: key = #{key} value=#{value}")
128
+ @config.set key, value
129
+ end
130
+
131
+ #Called when index is requested
132
+ #
133
+ def index type = :html
134
+ TotoBongo::logger.debug("Called Site::index")
135
+ articles = type == :html ? self.articles.reverse : self.articles
136
+ #know initialize the articles
137
+ {:articles => articles.map do |article|
138
+ Article.new article, @config
139
+ end}.merge archives
140
+ end
141
+
142
+ #
143
+ # Makes the list's of all articles
144
+ # pages/archives.html.haml with access to :archives to generate the full list of posts
145
+ #
146
+ def archives filter = ""
147
+ TotoBongo::logger.debug("Called Site::archive ")
148
+ entries = ! self.articles.empty??
149
+ self.articles.select do |a|
150
+ filter !~ /^\d{4}/ || File.basename(a) =~ /^#{filter}/
151
+ end.reverse.map do |article|
152
+ Article.new article, @config
153
+ end : []
154
+ return :archives => Archives.new(entries, @config)
155
+ end
156
+
157
+ def article route
158
+ TotoBongo::logger.debug("Called Site::article ")
159
+ Article.new("#{Paths[:articles]}/#{route.join('-')}.#{self[:ext]}", @config).load
160
+ end
161
+
162
+ # called when the user requests a /
163
+ # Returns whatever the site index config is
164
+ # default is "index"
165
+ def /
166
+ TotoBongo::logger.debug("Called Site::/ ")
167
+ self[:root]
168
+ end
169
+
170
+ #
171
+ # Called by the server after the route and the mime type are
172
+ # taken from the request,
173
+ #
174
+ # This is the first function that is called from
175
+ # the server.
176
+ # It should return the html to render
177
+ #
178
+ def go route, env = {}, type = :html
179
+ TotoBongo::logger.debug("Called Site::go ")
180
+ #check if the request includes an specific route
181
+ #else call / to get the index
182
+ route << self./ if route.empty?
183
+
184
+ type, path = type =~ /html|xml|json/ ? type.to_sym : :html, route.join('/')
185
+ context = lambda do |data, page|
186
+ Context.new(data, @config, path, env).render(page, type)
187
+ end
188
+
189
+ body, status = if Context.new.respond_to?(:"to_#{type}")
190
+
191
+ if route.first =~ /\d{4}/
192
+ case route.size
193
+ when 1..3
194
+ context[archives(route * '-'), :archives]
195
+ when 4
196
+ context[article(route), :article]
197
+ else
198
+ puts "400"
199
+ http 400
200
+ end #end case
201
+
202
+
203
+ # Responde to a path, when the request is for example index
204
+ elsif respond_to?(path)
205
+ #call the path, it will return the HTML of the path
206
+ context[send(path, type), path.to_sym]
207
+ else
208
+ context[{}, path.to_sym]
209
+ end
210
+ else
211
+ http 400
212
+ end #end context new respond
213
+
214
+ return body, status
215
+
216
+ rescue Errno::ENOENT => e
217
+ TotoBongo::logger.info("Errno:ENOENT: #{e.message} ")
218
+ return :body => http(404).first, :type => :html, :status => 404
219
+ else
220
+ TotoBongo::logger.debug("Status set 200 OK")
221
+ return :body => body || "", :type => type, :status => status || 200
222
+ end
223
+
224
+
225
+
226
+ protected
227
+
228
+ #sets the error code
229
+ def http code
230
+ TotoBongo::logger.debug("http with code #{code}")
231
+ [@config[:error].call(code), code]
232
+ end
233
+
234
+ # return a path to an article
235
+ def articles
236
+ TotoBongo::logger.debug("articles")
237
+ self.class.articles self[:ext]
238
+ end
239
+
240
+ #
241
+ #Returns the path to an article based on an extension
242
+ #Default ext is .txt
243
+ def self.articles ext
244
+ TotoBongo::logger.debug("self.articles")
245
+ Dir["#{Paths[:articles]}/*.#{ext}"].sort_by {|entry| File.basename(entry) }
246
+ end
247
+
248
+
249
+ #
250
+ # This class holds all the context to set the scope during rendering
251
+ # The context has access to the config and the article
252
+ # and defines all the article and archive method
253
+ #
254
+ class Context
255
+ include Template
256
+ attr_reader :env
257
+
258
+ def initialize ctx = {}, config = {}, path = "/", env = {}
259
+ TotoBongo::logger.debug("Initialize context")
260
+ @config, @context, @path, @env = config, ctx, path, env
261
+ #for each article, initialize an article object
262
+ @articles = Site.articles(@config[:ext]).reverse.map do |a|
263
+ Article.new(a, @config)
264
+ end
265
+
266
+ ctx.each do |k, v|
267
+ meta_def(k) { ctx.instance_of?(Hash) ? v : ctx.send(k) }
268
+ end
269
+ TotoBongo::logger.debug("End of initialize context")
270
+ end
271
+
272
+ def title
273
+ TotoBongo::logger.debug("Context::title")
274
+ @config[:title]
275
+ end
276
+
277
+ def description
278
+ TotoBongo::logger.debug("Context::desciption")
279
+ @config[:description]
280
+ end
281
+
282
+ def keywords
283
+ TotoBongo::logger.debug("Context::keywords")
284
+ @config[:keywords]
285
+ end
286
+
287
+
288
+ def render page, type
289
+ TotoBongo::logger.debug("Context::render")
290
+ content = to_html page, @config
291
+ type == :html ? to_html(:layout, @config, &Proc.new { content }) : send(:"to_#{type}", page)
292
+ end
293
+
294
+ def to_xml page
295
+ TotoBongo::logger.debug("Context::to_xml")
296
+ xml = Builder::XmlMarkup.new(:indent => 2)
297
+ instance_eval File.read("#{Paths[:templates]}/#{page}.builder")
298
+ end
299
+ alias :to_atom to_xml
300
+
301
+ def method_missing m, *args, &blk
302
+ TotoBongo::logger.debug("Context::missing_method #{m}")
303
+ @context.respond_to?(m) ? @context.send(m, *args, &blk) : super
304
+ end
305
+
306
+ end #end class contex
307
+
308
+ end #End site
309
+
310
+
311
+ class Archives < Array
312
+ include Template
313
+
314
+ def initialize articles, config
315
+ TotoBongo::logger.debug("Archives::initialize")
316
+ self.replace articles
317
+ @config = config
318
+ end
319
+
320
+ def [] a
321
+ TotoBongo::logger.debug("Archives::[]: a = #{a}")
322
+ a.is_a?(Range) ? self.class.new(self.slice(a) || [], @config) : super
323
+ end
324
+
325
+ def to_html
326
+ TotoBongo::logger.debug("Archives::to_html")
327
+ super(:archives, @config)
328
+ end
329
+ alias :to_s to_html
330
+ alias :archive archives
331
+ end
332
+
333
+
334
+
335
+ class Article < Hash
336
+ include Template
337
+
338
+ def initialize obj, config = {}
339
+ TotoBongo::logger.debug("Article::initialize")
340
+ @obj, @config = obj, config
341
+ self.load if obj.is_a? Hash
342
+ end
343
+
344
+
345
+ def load
346
+ TotoBongo::logger.debug("Article::load")
347
+ data = if @obj.is_a? String
348
+ meta, self[:body] = File.read(@obj).split(/\n\n/, 2)
349
+
350
+ # use the date from the filename, or else toto won't find the article
351
+ @obj =~ /\/(\d{4}-\d{2}-\d{2})[^\/]*$/
352
+ ($1 ? {:date => $1} : {}).merge(YAML.load(meta))
353
+ elsif @obj.is_a? Hash
354
+ @obj
355
+ end.inject({}) {|h, (k,v)| h.merge(k.to_sym => v) }
356
+
357
+ self.taint
358
+ self.update data
359
+ self[:date] = Date.parse(self[:date].gsub('/', '-')) rescue Date.today
360
+ self
361
+ end
362
+
363
+ #
364
+ # Called by path when constructing the SEO url
365
+ #
366
+ def [] key
367
+ TotoBongo::logger.debug("Article::key: key = #{key}")
368
+
369
+ self.load unless self.tainted?
370
+ super
371
+ end
372
+
373
+ def slug
374
+ TotoBongo::logger.debug("Article::slug")
375
+ self[:slug] || self[:title].slugize
376
+ end
377
+
378
+ #create a small summary of the body.
379
+ # Defaulsts 150 characters
380
+ def summary length = nil
381
+ TotoBongo::logger.debug("Article::summary")
382
+ config = @config[:summary]
383
+ sum = if self[:body] =~ config[:delim]
384
+ self[:body].split(config[:delim]).first
385
+ else
386
+ self[:body].match(/(.{1,#{length || config[:length] || config[:max]}}.*?)(\n|\Z)/m).to_s
387
+ end
388
+ textile(sum.length == self[:body].length ? sum : sum.strip.sub(/\.\Z/, '&hellip;'))
389
+ end
390
+
391
+ def url
392
+ TotoBongo::logger.debug("Article::url")
393
+ "http://#{(@config[:url].sub("http://", '') + self.path).squeeze('/')}"
394
+ end
395
+ alias :permalink url
396
+
397
+ def body
398
+ TotoBongo::logger.debug("Article::body")
399
+ textile self[:body].sub(@config[:summary][:delim], '') rescue textile self[:body]
400
+ end
401
+
402
+ #Path returns a SEO friendly URL path
403
+ # Eg for blog/articles/1900-05-17-the-wonderful-wizard-of-oz.txt
404
+ # it returns /blog/1900/05/17/the-wonderful-wizard-of-oz/
405
+ def path
406
+ TotoBongo::logger.debug("Article::path")
407
+ "/#{@config[:prefix]}#{self[:date].strftime("/%Y/%m/%d/#{slug}/")}".squeeze('/')
408
+ end
409
+
410
+ def title()
411
+ TotoBongo::logger.debug("Article::title")
412
+ self[:title] || "an article"
413
+ end
414
+ def date()
415
+ TotoBongo::logger.debug("Article::path")
416
+ @config[:date].call(self[:date])
417
+
418
+ end
419
+ def author()
420
+ TotoBongo::logger.debug("Article::path")
421
+ self[:author] || @config[:author]
422
+ end
423
+
424
+ def description()
425
+ TotoBongo::logger.debug("Article::path")
426
+ self[:description] || title()
427
+ end
428
+
429
+ def keywords()
430
+ TotoBongo::logger.debug("Article::keywords")
431
+ self[:keywords] || title()
432
+ end
433
+
434
+
435
+ def to_html()
436
+ TotoBongo::logger.debug("Article::path")
437
+ self.load; super(:article, @config)
438
+ end
439
+ alias :to_s to_html
440
+
441
+ end
442
+
443
+
444
+
445
+
446
+ class Config < Hash
447
+
448
+ #
449
+ #This is the hash that stores all teh configuation options
450
+ #
451
+
452
+ Defaults = {
453
+ :author => ENV['USER'], # blog author
454
+ :title => Dir.pwd.split('/').last, # blog index title
455
+ :description => "Blog for your existing rails app", # blog meta description
456
+ :keywords => "blog rails existing", # blog meta keywords
457
+ :root => "index", # site index
458
+ :url => "http://127.0.0.1", # root URL of the site
459
+ :prefix => "blog", # common path prefix for the blog
460
+ :date => lambda {|now| now.strftime("%d/%m/%Y") }, # date function
461
+ :disqus => false, # disqus name
462
+ :summary => {:max => 150, :delim => /~\n/}, # length of summary and delimiter
463
+ :ext => "txt", # extension for articles
464
+ :cache => 28800, # cache duration (seconds)
465
+ :to_html => lambda {|path, page, ctx| # returns an html, from a path & context
466
+ Haml::Engine.new(File.read("#{path}/#{page}.html.haml")).render(ctx)
467
+ },
468
+ :error => lambda {|code| # The HTML for your error page
469
+ "<font style='font-size:300%'>toto-bongo error (#{code})</font>"
470
+ }
471
+ }
472
+
473
+
474
+ def initialize obj
475
+
476
+ self.update Defaults
477
+ self.update obj
478
+ end
479
+
480
+ def set key, val = nil, &blk
481
+ if val.is_a? Hash
482
+ self[key].update val
483
+ else
484
+ self[key] = block_given?? blk : val
485
+ end
486
+ end
487
+ end
488
+
489
+
490
+
491
+
492
+
493
+
494
+ # The HTTP server
495
+ class Server
496
+ attr_reader :config, :site
497
+
498
+ def initialize config = {}, &blk
499
+ @config = config.is_a?(Config) ? config : Config.new(config)
500
+ @config.instance_eval(&blk) if block_given?
501
+ @site = TotoBongo::Site.new(@config)
502
+ end
503
+
504
+
505
+ #
506
+ # This is the entry point of the request
507
+ # On each request, this is the first method that gets
508
+ # called
509
+ #
510
+ def call env
511
+ TotoBongo::logger.debug("***************REQUEST BEGIN*************")
512
+ @request = Rack::Request.new env
513
+ @response = Rack::Response.new
514
+ return [400, {}, []] unless @request.get?
515
+
516
+
517
+ #puts "Request path info is: #{@request.path_info}"
518
+
519
+ path, mime = @request.path_info.split('.')
520
+ route = (path || '/').split('/').reject {|i| i.empty? }
521
+
522
+ response = @site.go(route, env, *(mime ? mime : []))
523
+
524
+ @response.body = [response[:body]]
525
+ @response['Content-Length'] = response[:body].length.to_s unless response[:body].empty?
526
+ @response['Content-Type'] = Rack::Mime.mime_type(".#{response[:type]}")
527
+
528
+ # Set http cache headers
529
+ @response['Cache-Control'] = if TotoBongo.env == 'production'
530
+ "public, max-age=#{@config[:cache]}"
531
+ else
532
+ "no-cache, must-revalidate"
533
+ end
534
+
535
+ @response['ETag'] = %("#{Digest::SHA1.hexdigest(response[:body])}")
536
+ TotoBongo::logger.debug("****************REQUEST END******************")
537
+
538
+ @response.status = response[:status]
539
+ @response.finish
540
+ end
541
+ end
542
+ end
543
+
@@ -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
+
data/test/autotest.rb ADDED
@@ -0,0 +1,34 @@
1
+ #
2
+ # Convenience Methods
3
+ #
4
+ def run(cmd)
5
+ print "\n\n"
6
+ puts(cmd)
7
+ system(cmd)
8
+ print "\n\n"
9
+ end
10
+
11
+ def run_all_tests
12
+ # see Rakefile for the definition of the test:all task
13
+ system("rake -s test:all VERBOSE=true")
14
+ end
15
+
16
+ #
17
+ # Watchr Rules
18
+ #
19
+ watch('^test/.*?_test\.rb' ) {|m| run("ruby -rubygems %s" % m[0]) }
20
+ watch('^lib/(.*)\.rb' ) {|m| run("ruby -rubygems test/%s_test.rb" % m[1]) }
21
+ watch('^lib/toto/(.*)\.rb' ) {|m| run("ruby -rubygems test/%s_test.rb" % m[1]) }
22
+ watch('^test/test_helper\.rb') { run_all_tests }
23
+
24
+ #
25
+ # Signal Handling
26
+ #
27
+ # Ctrl-\
28
+ Signal.trap('QUIT') do
29
+ puts " --- Running all tests ---\n\n"
30
+ run_all_tests
31
+ end
32
+
33
+ # Ctrl-C
34
+ Signal.trap('INT') { abort("\n") }
File without changes
@@ -0,0 +1,7 @@
1
+ %h1= @path
2
+ %ul
3
+ - if archives.length > 0
4
+ - for entry in archives
5
+ %li
6
+ %a{:href => entry.path}= entry.title
7
+
@@ -0,0 +1,16 @@
1
+ %article.post
2
+ %header
3
+ %h1= title
4
+ %span.date= date
5
+ %p= author
6
+ %section.content
7
+ = body
8
+ %section.comments
9
+ - if @config[:disqus]
10
+ #disqus_thread
11
+ %script{:src => "http://disqus.com/forums/#{@config[:disqus]}/embed.js", :type => "text/javascript"} %noscript
12
+ %a{:href => "http://#{@config[:disqus]}.disqus.com/?url=ref"} View the discussion thread.
13
+ %a.dsq-brlink{:href => "http://disqus.com"}
14
+ blog comments powered by
15
+ %span.logo-disqus Disqus
16
+
@@ -0,0 +1,21 @@
1
+ xml.instruct!
2
+ xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
3
+ xml.title @config[:title]
4
+ xml.id @config[:url]
5
+ xml.updated articles.first[:date].iso8601 unless articles.empty?
6
+ xml.author { xml.name @config[:author] }
7
+
8
+ articles.each do |article|
9
+ xml.entry do
10
+ xml.title article.title
11
+ xml.link "rel" => "alternate", "href" => article.url
12
+ xml.id article.url
13
+ xml.published article[:date].iso8601
14
+ xml.updated article[:date].iso8601
15
+ xml.author { xml.name @config[:author] }
16
+ xml.summary article.summary, "type" => "html"
17
+ xml.content article.body, "type" => "html"
18
+ end
19
+ end
20
+ end
21
+
@@ -0,0 +1,21 @@
1
+ xml.instruct!
2
+ xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
3
+ xml.title @config[:title]
4
+ xml.id @config[:url]
5
+ xml.updated articles.first[:date].iso8601 unless articles.empty?
6
+ xml.author { xml.name @config[:author] }
7
+
8
+ articles.reverse[0...10].each do |article|
9
+ xml.entry do
10
+ xml.title article.title
11
+ xml.link "rel" => "alternate", "href" => article.url
12
+ xml.id article.url
13
+ xml.published article[:date].iso8601
14
+ xml.updated article[:date].iso8601
15
+ xml.author { xml.name @config[:author] }
16
+ xml.summary article.summary, "type" => "html"
17
+ xml.content article.body, "type" => "html"
18
+ end
19
+ end
20
+ end
21
+
@@ -0,0 +1,14 @@
1
+ %section#articles
2
+ - for article in articles[0...3]
3
+ %article.post
4
+ %header
5
+ %h1
6
+ %a{:href => article.path}= article.title
7
+ %span.date= article.date
8
+ %section.content
9
+ = article.summary
10
+ .more
11
+ %a{:href => article.path} read on »
12
+ %section#archives
13
+ = archives[3..-1]
14
+