toto-bongo 1.0.1

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