secondplanet-toto 0.4.9

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 31ce3072c1b24c7bbd088b1902453d6169a738b3
4
+ data.tar.gz: 9e2818c5638044baac3d5a05fbd8bfbf7ac477e1
5
+ SHA512:
6
+ metadata.gz: 54160635e018878918a10e1f23f50fb24f313c6c7476a5b84f2da6517fcd24087a9752b420b1b6ee9fa3dd2419eb844c3aa485fe624cf3fd5bd5c62bd133250c
7
+ data.tar.gz: 82d76f9653a8a711167607211528f669239f2ed4c762d4dd75a054a6fd9f325c28ffeb3109a66d1d16728f57eabe6b2477a96ec5046e1e4c431fad4429e5f960
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 cloudhead
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,162 @@
1
+ toto
2
+ ====
3
+
4
+ the tiniest blogging engine in Oz!
5
+
6
+ introduction
7
+ ------------
8
+
9
+ toto is a git-powered, minimalist blog engine for the hackers of Oz. The engine weighs around ~300 sloc at its worse.
10
+ There is no toto client, at least for now; everything goes through git.
11
+
12
+ blog in 10 seconds
13
+ ------------------
14
+
15
+ $ git clone git://github.com/cloudhead/dorothy.git myblog
16
+ $ cd myblog
17
+ $ heroku create myblog
18
+ $ git push heroku master
19
+
20
+ philosophy
21
+ ----------
22
+
23
+ Everything that can be done better with another tool should be, but one should not have too much pie to stay fit.
24
+ In other words, toto does away with web frameworks or DSLs such as sinatra, and is built right on top of **rack**.
25
+ There is no database or ORM either, we use plain text files.
26
+
27
+ Toto was designed to be used with a reverse-proxy cache, such as [Varnish](http://varnish-cache.org).
28
+ This makes it an ideal candidate for [heroku](http://heroku.com).
29
+
30
+ Oh, and everything that can be done with git, _is_.
31
+
32
+ how it works
33
+ ------------
34
+
35
+ - content is entirely managed through **git**; you get full fledged version control for free.
36
+ - articles are stored as _.txt_ files, with embedded metadata (in yaml format).
37
+ - articles are processed through a markdown converter (rdiscount) by default.
38
+ - templating is done through **ERB**.
39
+ - toto is built right on top of **Rack**.
40
+ - toto was built to take advantage of _HTTP caching_.
41
+ - toto was built with heroku in mind.
42
+ - comments are handled by [disqus](http://disqus.com)
43
+ - individual articles can be accessed through urls such as _/2009/11/21/blogging-with-toto_
44
+ - the archives can be accessed by year, month or day, with the same format as above.
45
+ - arbitrary metadata can be included in articles files, and accessed from the templates.
46
+ - summaries are generated intelligently by toto, following the `:max` setting you give it.
47
+ - you can also define how long your summary is, by adding `~` at the end of it (`:delim`).
48
+
49
+ dorothy
50
+ -------
51
+
52
+ Dorothy is toto's default template, you can get it at <http://github.com/cloudhead/dorothy>. It
53
+ comes with a very minimalistic but functional template, and a _config.ru_ file to get you started.
54
+ It also includes a _.gems_ file, for heroku.
55
+
56
+ synopsis
57
+ --------
58
+
59
+ One would start by installing _toto_, with `sudo gem install toto`, and then forking or
60
+ cloning the `dorothy` repo, to get a basic skeleton:
61
+
62
+ $ git clone git://github.com/cloudhead/dorothy.git weblog
63
+ $ cd weblog/
64
+
65
+ One would then edit the template at will, it has the following structure:
66
+
67
+ templates/
68
+ |
69
+ +- layout.rhtml # the main site layout, shared by all pages
70
+ |
71
+ +- index.builder # the builder template for the atom feed
72
+ |
73
+ +- pages/ # pages, such as home, about, etc go here
74
+ |
75
+ +- index.rhtml # the default page loaded from `/`, it displays the list of articles
76
+ |
77
+ +- article.rhtml # the article (post) partial and page
78
+ |
79
+ +- about.rhtml
80
+
81
+ One could then create a .txt article file in the `articles/` folder, and make sure it has the following format:
82
+
83
+ title: The Wonderful Wizard of Oz
84
+ author: Lyman Frank Baum
85
+ date: 1900/05/17
86
+
87
+ Dorothy lived in the midst of the great Kansas prairies, with Uncle Henry,
88
+ who was a farmer, and Aunt Em, who was the farmer's wife.
89
+
90
+ If one is familiar with webby or aerial, this shouldn't look funny. Basically the top of the file is in YAML format,
91
+ and the rest of it is the blog post. They are delimited by an empty line `/\n\n/`, as you can see above.
92
+ None of the information is compulsory, but it's strongly encouraged you specify it.
93
+ Note that one can also use `rake` to create an article stub, with `rake new`.
94
+
95
+ Once he finishes writing his beautiful tale, one can push to the git repo, as usual:
96
+
97
+ $ git add articles/wizard-of-oz.txt
98
+ $ git commit -m 'wrote the wizard of oz.'
99
+ $ git push remote master
100
+
101
+ Where `remote` is the name of your remote git repository. The article is now published.
102
+
103
+ ### deployment
104
+
105
+ Toto is built on top of **Rack**, and hence has a **rackup** file: _config.ru_.
106
+
107
+ #### on your own server
108
+
109
+ Once you have created the remote git repo, and pushed your changes to it, you can run toto with any Rack compliant web server,
110
+ such as **thin**, **mongrel** or **unicorn**.
111
+
112
+ With thin, you would do something like:
113
+
114
+ $ thin start -R config.ru
115
+
116
+ With unicorn, you can just do:
117
+
118
+ $ unicorn
119
+
120
+ #### on heroku
121
+
122
+ Toto was designed to work well with [heroku](http://heroku.com), it makes the most out of it's state-of-the-art caching,
123
+ by setting the _Cache-Control_ and _Etag_ HTTP headers. Deploying on Heroku is really easy, just get the heroku gem,
124
+ create a heroku app with `heroku create`, and push with `git push heroku master`.
125
+
126
+ $ heroku create weblog
127
+ $ git push heroku master
128
+ $ heroku open
129
+
130
+ ### configuration
131
+
132
+ You can configure toto, by modifying the _config.ru_ file. For example, if you want to set the blog author to 'John Galt',
133
+ you could add `set :author, 'John Galt'` inside the `Toto::Server.new` block. Here are the defaults, to get you started:
134
+
135
+ set :author, ENV['USER'] # blog author
136
+ set :title, Dir.pwd.split('/').last # site title
137
+ set :url, 'http://example.com' # site root URL
138
+ set :prefix, '' # common path prefix for all pages
139
+ set :root, "index" # page to load on /
140
+ set :date, lambda {|now| now.strftime("%d/%m/%Y") } # date format for articles
141
+ set :markdown, :smart # use markdown + smart-mode
142
+ set :disqus, false # disqus id, or false
143
+ set :summary, :max => 150, :delim => /~\n/ # length of article summary and delimiter
144
+ set :ext, 'txt' # file extension for articles
145
+ set :cache, 28800 # cache site for 8 hours
146
+
147
+ set :to_html do |path, page, ctx| # returns an html, from a path & context
148
+ ERB.new(File.read("#{path}/#{page}.rhtml")).result(ctx)
149
+ end
150
+
151
+ set :error do |code| # The HTML for your error page
152
+ "<font style='font-size:300%'>toto, we're not in Kansas anymore (#{code})</font>"
153
+ end
154
+
155
+ thanks
156
+ ------
157
+
158
+ To heroku for making this easy as pie.
159
+ To adam wiggins, as I stole a couple of ideas from Scanty.
160
+ To the developers of Rack, for making such an awesome platform.
161
+
162
+ Copyright (c) 2009-2010 cloudhead. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,35 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "toto"
8
+ gem.summary = %Q{the tiniest blog-engine in Oz}
9
+ gem.description = %Q{the tiniest blog-engine in Oz.}
10
+ gem.email = "self@cloudhead.net"
11
+ gem.homepage = "http://github.com/cloudhead/toto"
12
+ gem.authors = ["cloudhead"]
13
+ gem.add_development_dependency "riot"
14
+ gem.add_dependency "builder"
15
+ gem.add_dependency "rack"
16
+ if RUBY_PLATFORM =~ /win32/
17
+ gem.add_dependency "maruku"
18
+ else
19
+ gem.add_dependency "rdiscount"
20
+ end
21
+ end
22
+ Jeweler::GemcutterTasks.new
23
+ rescue LoadError
24
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
25
+ end
26
+
27
+ require 'rake/testtask'
28
+ Rake::TestTask.new(:test) do |test|
29
+ test.libs << 'lib' << 'test'
30
+ test.pattern = 'test/**/*_test.rb'
31
+ test.verbose = true
32
+ end
33
+
34
+ task :test => :check_dependencies
35
+ task :default => :test
data/TODO ADDED
@@ -0,0 +1,2 @@
1
+ TODO
2
+ ====
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.4.9
data/lib/ext/ext.rb ADDED
@@ -0,0 +1,52 @@
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
+
18
+ if RUBY_VERSION < "1.9"
19
+ def bytesize
20
+ size
21
+ end
22
+ end
23
+ end
24
+
25
+ class Fixnum
26
+ def ordinal
27
+ # 1 => 1st
28
+ # 2 => 2nd
29
+ # 3 => 3rd
30
+ # ...
31
+ case self % 100
32
+ when 11..13; "#{self}th"
33
+ else
34
+ case self % 10
35
+ when 1; "#{self}st"
36
+ when 2; "#{self}nd"
37
+ when 3; "#{self}rd"
38
+ else "#{self}th"
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ class Date
45
+ # This check is for people running Toto with ActiveSupport, avoid a collision
46
+ unless respond_to? :iso8601
47
+ # Return the date as a String formatted according to ISO 8601.
48
+ def iso8601
49
+ ::Time.utc(year, month, day, 0, 0, 0, 0).iso8601
50
+ end
51
+ end
52
+ end
data/lib/toto.rb ADDED
@@ -0,0 +1,355 @@
1
+ require 'yaml'
2
+ require 'date'
3
+ require 'erb'
4
+ require 'rack'
5
+ require 'digest'
6
+ require 'open-uri'
7
+ require 'rdiscount'
8
+ require 'org-ruby'
9
+
10
+ Markdown = Orgmode::Parser
11
+
12
+ require 'builder'
13
+
14
+ $:.unshift File.dirname(__FILE__)
15
+
16
+ require 'ext/ext'
17
+
18
+ module Toto
19
+ Paths = {
20
+ :templates => "templates",
21
+ :pages => "templates/pages",
22
+ :articles => "articles"
23
+ }
24
+
25
+ def self.env
26
+ ENV['RACK_ENV'] || 'production'
27
+ end
28
+
29
+ def self.env= env
30
+ ENV['RACK_ENV'] = env
31
+ end
32
+
33
+ module Template
34
+ def to_html page, config, &blk
35
+ path = ([:layout, :repo].include?(page) ? Paths[:templates] : Paths[:pages])
36
+ config[:to_html].call(path, page, binding)
37
+ end
38
+
39
+ def markdown text
40
+ if (options = @config[:markdown])
41
+ Markdown.new(text.to_s.strip).to_html
42
+ else
43
+ text.strip
44
+ end
45
+ end
46
+
47
+ def method_missing m, *args, &blk
48
+ self.keys.include?(m) ? self[m] : super
49
+ end
50
+
51
+ def self.included obj
52
+ obj.class_eval do
53
+ define_method(obj.to_s.split('::').last.downcase) { self }
54
+ end
55
+ end
56
+ end
57
+
58
+ class Site
59
+ def initialize config
60
+ @config = config
61
+ end
62
+
63
+ def [] *args
64
+ @config[*args]
65
+ end
66
+
67
+ def []= key, value
68
+ @config.set key, value
69
+ end
70
+
71
+ def index type = :html
72
+ articles = type == :html ? self.articles.reverse : self.articles
73
+ {:articles => articles.map do |article|
74
+ Article.new article, @config
75
+ end}.merge archives
76
+ end
77
+
78
+ def archives filter = ""
79
+ entries = ! self.articles.empty??
80
+ self.articles.select do |a|
81
+ filter !~ /^\d{4}/ || File.basename(a) =~ /^#{filter}/
82
+ end.reverse.map do |article|
83
+ Article.new article, @config
84
+ end : []
85
+
86
+ return :archives => Archives.new(entries, @config)
87
+ end
88
+
89
+ def article route
90
+ Article.new("#{Paths[:articles]}/#{route.join('-')}.#{self[:ext]}", @config).load
91
+ end
92
+
93
+ def /
94
+ self[:root]
95
+ end
96
+
97
+ def go route, env = {}, type = :html
98
+ route << self./ if route.empty?
99
+ type, path = type =~ /html|xml|json/ ? type.to_sym : :html, route.join('/')
100
+ context = lambda do |data, page|
101
+ Context.new(data, @config, path, env).render(page, type)
102
+ end
103
+
104
+ body, status = if Context.new.respond_to?(:"to_#{type}")
105
+ if route.first =~ /\d{4}/
106
+ case route.size
107
+ when 1..3
108
+ context[archives(route * '-'), :archives]
109
+ when 4
110
+ context[article(route), :article]
111
+ else http 400
112
+ end
113
+ elsif respond_to?(path)
114
+ context[send(path, type), path.to_sym]
115
+ elsif (repo = @config[:github][:repos].grep(/#{path}/).first) &&
116
+ !@config[:github][:user].empty?
117
+ context[Repo.new(repo, @config), :repo]
118
+ else
119
+ context[{}, path.to_sym]
120
+ end
121
+ else
122
+ http 400
123
+ end
124
+
125
+ rescue Errno::ENOENT => e
126
+ return :body => http(404).first, :type => :html, :status => 404
127
+ else
128
+ return :body => body || "", :type => type, :status => status || 200
129
+ end
130
+
131
+ protected
132
+
133
+ def http code
134
+ [@config[:error].call(code), code]
135
+ end
136
+
137
+ def articles
138
+ self.class.articles self[:ext]
139
+ end
140
+
141
+ def self.articles ext
142
+ Dir["#{Paths[:articles]}/*.#{ext}"].sort_by {|entry| File.basename(entry) }
143
+ end
144
+
145
+ class Context
146
+ include Template
147
+ attr_reader :env
148
+
149
+ def initialize ctx = {}, config = {}, path = "/", env = {}
150
+ @config, @context, @path, @env = config, ctx, path, env
151
+ @articles = Site.articles(@config[:ext]).reverse.map do |a|
152
+ Article.new(a, @config)
153
+ end
154
+
155
+ ctx.each do |k, v|
156
+ meta_def(k) { ctx.instance_of?(Hash) ? v : ctx.send(k) }
157
+ end
158
+ end
159
+
160
+ def title
161
+ @config[:title]
162
+ end
163
+
164
+ def render page, type
165
+ content = to_html page, @config
166
+ type == :html ? to_html(:layout, @config, &Proc.new { content }) : send(:"to_#{type}", page)
167
+ end
168
+
169
+ def to_xml page
170
+ xml = Builder::XmlMarkup.new(:indent => 2)
171
+ instance_eval File.read("#{Paths[:templates]}/#{page}.builder")
172
+ end
173
+ alias :to_atom to_xml
174
+
175
+ def method_missing m, *args, &blk
176
+ @context.respond_to?(m) ? @context.send(m, *args, &blk) : super
177
+ end
178
+ end
179
+ end
180
+
181
+ class Repo < Hash
182
+ include Template
183
+
184
+ README = "https://github.com/%s/%s/raw/master/README.%s"
185
+
186
+ def initialize name, config
187
+ self[:name], @config = name, config
188
+ end
189
+
190
+ def readme
191
+ markdown open(README %
192
+ [@config[:github][:user], self[:name], @config[:github][:ext]]).read
193
+ rescue Timeout::Error, OpenURI::HTTPError => e
194
+ "This page isn't available."
195
+ end
196
+ alias :content readme
197
+ end
198
+
199
+ class Archives < Array
200
+ include Template
201
+
202
+ def initialize articles, config
203
+ self.replace articles
204
+ @config = config
205
+ end
206
+
207
+ def [] a
208
+ a.is_a?(Range) ? self.class.new(self.slice(a) || [], @config) : super
209
+ end
210
+
211
+ def to_html
212
+ super(:archives, @config)
213
+ end
214
+ alias :to_s to_html
215
+ alias :archive archives
216
+ end
217
+
218
+ class Article < Hash
219
+ include Template
220
+
221
+ def initialize obj, config = {}
222
+ @obj, @config = obj, config
223
+ self.load if obj.is_a? Hash
224
+ end
225
+
226
+ def load
227
+ data = if @obj.is_a? String
228
+ meta, self[:body] = File.read(@obj).split(/\n\n/, 2)
229
+
230
+ # use the date from the filename, or else toto won't find the article
231
+ @obj =~ /\/(\d{4}-\d{2}-\d{2})[^\/]*$/
232
+ ($1 ? {:date => $1} : {}).merge(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] = Date.parse(self[:date].gsub('/', '-')) rescue Date.today
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 path
272
+ "/#{@config[:prefix]}#{self[:date].strftime("/%Y/%m/%d/#{slug}/")}".squeeze('/')
273
+ end
274
+
275
+ def title() self[:title] || "an article" end
276
+ def date() @config[:date].call(self[:date]) end
277
+ def author() self[:author] || @config[:author] end
278
+ def to_html() self.load; super(:article, @config) end
279
+ alias :to_s to_html
280
+ end
281
+
282
+ class Config < Hash
283
+ Defaults = {
284
+ :author => ENV['USER'], # blog author
285
+ :title => Dir.pwd.split('/').last, # site title
286
+ :root => "index", # site index
287
+ :url => "http://127.0.0.1", # root URL of the site
288
+ :prefix => "", # common path prefix for the blog
289
+ :date => lambda {|now| now.strftime("%d/%m/%Y") }, # date function
290
+ :markdown => :smart, # use markdown
291
+ :disqus => false, # disqus name
292
+ :summary => {:max => 150, :delim => /~\n/}, # length of summary and delimiter
293
+ :ext => 'txt', # extension for articles
294
+ :cache => 28800, # cache duration (seconds)
295
+ :github => {:user => "", :repos => [], :ext => 'md'}, # Github username and list of repos
296
+ :to_html => lambda {|path, page, ctx| # returns an html, from a path & context
297
+ ERB.new(File.read("#{path}/#{page}.rhtml")).result(ctx)
298
+ },
299
+ :error => lambda {|code| # The HTML for your error page
300
+ "<font style='font-size:300%'>toto, we're not in Kansas anymore (#{code})</font>"
301
+ }
302
+ }
303
+ def initialize obj
304
+ self.update Defaults
305
+ self.update obj
306
+ end
307
+
308
+ def set key, val = nil, &blk
309
+ if val.is_a? Hash
310
+ self[key].update val
311
+ else
312
+ self[key] = block_given?? blk : val
313
+ end
314
+ end
315
+ end
316
+
317
+ class Server
318
+ attr_reader :config, :site
319
+
320
+ def initialize config = {}, &blk
321
+ @config = config.is_a?(Config) ? config : Config.new(config)
322
+ @config.instance_eval(&blk) if block_given?
323
+ @site = Toto::Site.new(@config)
324
+ end
325
+
326
+ def call env
327
+ @request = Rack::Request.new env
328
+ @response = Rack::Response.new
329
+
330
+ return [400, {}, []] unless @request.get?
331
+
332
+ path, mime = @request.path_info.split('.')
333
+ route = (path || '/').split('/').reject {|i| i.empty? }
334
+
335
+ response = @site.go(route, env, *(mime ? mime : []))
336
+
337
+ @response.body = [response[:body]]
338
+ @response['Content-Length'] = response[:body].bytesize.to_s unless response[:body].empty?
339
+ @response['Content-Type'] = Rack::Mime.mime_type(".#{response[:type]}")
340
+
341
+ # Set http cache headers
342
+ @response['Cache-Control'] = if Toto.env == 'production'
343
+ "public, max-age=#{@config[:cache]}"
344
+ else
345
+ "no-cache, must-revalidate"
346
+ end
347
+
348
+ @response['ETag'] = %("#{Digest::SHA1.hexdigest(response[:body])}")
349
+
350
+ @response.status = response[:status]
351
+ @response.finish
352
+ end
353
+ end
354
+ end
355
+