secondplanet-toto 0.4.9

Sign up to get free protection for your applications and to get access to all the features.
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
+