tinman 0.1.0

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/.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,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 codesponge
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,166 @@
1
+ tinman
2
+ ====
3
+
4
+ the tiniest blogging engine in Oz!
5
+
6
+ introduction
7
+ ------------
8
+
9
+ tinman 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 tinman client, at least for now; everything goes through git.
11
+
12
+ tinman is almost identical to toto. It is the intent that everything should work the same, only difference is that
13
+ Markdown has been replaced with textile. For more information or if you prefer markdown to textile then you should checkout
14
+ "toto":http://cloudhead.io/toto
15
+
16
+ blog in 10 seconds
17
+ ------------------
18
+
19
+ $ git clone git://github.com/cloudhead/dorothy.git myblog
20
+ $ cd myblog
21
+ $ heroku create myblog
22
+ $ git push heroku master
23
+
24
+ philosophy
25
+ ----------
26
+
27
+ Everything that can be done better with another tool should be, but one should not have too much pie to stay fit.
28
+ In other words, tinman does away with web frameworks or DSLs such as sinatra, and is built right on top of **rack**.
29
+ There is no database or ORM either, we use plain text files.
30
+
31
+ TinMan was designed to be used with a reverse-proxy cache, such as [Varnish](http://varnish-cache.org).
32
+ This makes it an ideal candidate for [heroku](http://heroku.com).
33
+
34
+ Oh, and everything that can be done with git, _is_.
35
+
36
+ how it works
37
+ ------------
38
+
39
+ - content is entirely managed through **git**; you get full fledged version control for free.
40
+ - articles are stored as _.txt_ files, with embeded metadata (in yaml format).
41
+ - articles are processed through a textile converter (RedCloth).
42
+ - templating is done through **ERB**.
43
+ - tinman is built right on top of **Rack**.
44
+ - tinman was built to take advantage of _HTTP caching_.
45
+ - tinman was built with heroku in mind.
46
+ - comments are handled by [disqus](http://disqus.com)
47
+ - individual articles can be accessed through urls such as _/2009/11/21/blogging-with-tinman_
48
+ - the archives can be accessed by year, month or day, wih the same format as above.
49
+ - arbitrary metadata can be included in articles files, and accessed from the templates.
50
+ - summaries are generated intelligently by tinman, following the `:max` setting you give it.
51
+ - you can also define how long your summary is, by adding `~` at the end of it (`:delim`).
52
+
53
+ dorothy
54
+ -------
55
+
56
+ Dorothy is tinman's default template, you can get it at <http://github.com/cloudhead/dorothy>. It
57
+ comes with a very minimalistic but functional template, and a _config.ru_ file to get you started.
58
+ It also includes a _.gems_ file, for heroku.
59
+
60
+ synopsis
61
+ --------
62
+
63
+ One would start by installing _tinman_, with `sudo gem install tinman`, and then forking or
64
+ cloning the `dorothy` repo, to get a basic skeleton:
65
+
66
+ $ git clone git://github.com/cloudhead/dorothy.git weblog
67
+ $ cd weblog/
68
+
69
+ One would then edit the template at will, it has the following structure:
70
+
71
+ templates/
72
+ |
73
+ +- layout.rhtml # the main site layout, shared by all pages
74
+ |
75
+ +- index.builder # the builder template for the atom feed
76
+ |
77
+ +- pages/ # pages, such as home, about, etc go here
78
+ |
79
+ +- index.rhtml # the default page loaded from `/`, it displays the list of articles
80
+ |
81
+ +- article.rhtml # the article (post) partial and page
82
+ |
83
+ +- about.rhtml
84
+
85
+ One could then create a .txt article file in the `articles/` folder, and make sure it has the following format:
86
+
87
+ title: The Wonderful Wizard of Oz
88
+ author: Lyman Frank Baum
89
+ date: 1900/05/17
90
+
91
+ Dorothy lived in the midst of the great Kansas prairies, with Uncle Henry,
92
+ who was a farmer, and Aunt Em, who was the farmer's wife.
93
+
94
+ If one is familiar with webby or aerial, this shouldn't look funny. Basically the top of the file is in YAML format,
95
+ and the rest of it is the blog post. They are delimited by an empty line `/\n\n/`, as you can see above.
96
+ None of the information is compulsory, but it's strongly encouraged you specify it.
97
+ Note that one can also use `rake` to create an article stub, with `rake new`.
98
+
99
+ Once he finishes writing his beautiful tale, one can push to the git repo, as usual:
100
+
101
+ $ git add articles/wizard-of-oz.txt
102
+ $ git commit -m 'wrote the wizard of oz.'
103
+ $ git push remote master
104
+
105
+ Where `remote` is the name of your remote git repository. The article is now published.
106
+
107
+ ### deployment
108
+
109
+ TinMan is built on top of **Rack**, and hence has a **rackup** file: _config.ru_.
110
+
111
+ #### on your own server
112
+
113
+ Once you have created the remote git repo, and pushed your changes to it, you can run tinman with any Rack compliant web server,
114
+ such as **thin**, **mongrel** or **unicorn**.
115
+
116
+ With thin, you would do something like:
117
+
118
+ $ thin start -R config.ru
119
+
120
+ With unicorn, you can just do:
121
+
122
+ $ unicorn
123
+
124
+ #### on heroku
125
+
126
+ TinMan was designed to work well with [heroku](http://heroku.com), it makes the most out of it's state-of-the-art caching,
127
+ by setting the _Cache-Control_ and _Etag_ HTTP headers. Deploying on Heroku is really easy, just get the heroku gem,
128
+ create a heroku app with `heroku create`, and push with `git push heroku master`.
129
+
130
+ $ heroku create weblog
131
+ $ git push heroku master
132
+ $ heroku open
133
+
134
+ ### configuration
135
+
136
+ You can configure tinman, by modifying the _config.ru_ file. For example, if you want to set the blog author to 'John Galt',
137
+ you could add `set :author, 'John Galt'` inside the `TinMan::Server.new` block. Here are the defaults, to get you started:
138
+
139
+ set :author, ENV['USER'] # blog author
140
+ set :title, Dir.pwd.split('/').last # site title
141
+ set :url, 'http://example.com' # site root URL
142
+ set :prefix, '' # common path prefix for all pages
143
+ set :root, "index" # page to load on /
144
+ set :date, lambda {|now| now.strftime("%d/%m/%Y") } # date format for articles
145
+ set :redcloth, :smart # use redcloth + smart-mode
146
+ set :disqus, false # disqus id, or false
147
+ set :summary, :max => 150, :delim => /~\n/ # length of article summary and delimiter
148
+ set :ext, 'txt' # file extension for articles
149
+ set :cache, 28800 # cache site for 8 hours
150
+
151
+ set :to_html do |path, page, ctx| # returns an html, from a path & context
152
+ ERB.new(File.read("#{path}/#{page}.rhtml")).result(ctx)
153
+ end
154
+
155
+ set :error do |code| # The HTML for your error page
156
+ "<font style='font-size:300%'>tinman, we're not in Kansas anymore (#{code})</font>"
157
+ end
158
+
159
+ thanks
160
+ ------
161
+
162
+ To heroku for making this easy as pie.
163
+ To adam wiggins, as I stole a couple of ideas from Scanty.
164
+ To the developpers of Rack, for making such an awesome platform.
165
+
166
+ Copyright (c) 2009-2010 cloudhead. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,56 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "tinman"
8
+ gem.summary = %Q{toto with textile (via RedCloth)}
9
+ gem.description = %Q{toto with textile (via RedCloth)}
10
+ gem.email = "billy@codesponge.com"
11
+ gem.homepage = "http://github.com/codesponge/tinman"
12
+ gem.authors = ["clooudhead codesponge"]
13
+ gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
14
+ gem.add_dependency "builder"
15
+ gem.add_dependency "rack"
16
+ gem.add_dependency "RedCloth"
17
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
18
+ end
19
+ Jeweler::GemcutterTasks.new
20
+ rescue LoadError
21
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
22
+ end
23
+
24
+ require 'rake/testtask'
25
+ Rake::TestTask.new(:test) do |test|
26
+ test.libs << 'lib' << 'test'
27
+ test.pattern = 'test/**/test_*.rb'
28
+ test.verbose = true
29
+ end
30
+
31
+ begin
32
+ require 'rcov/rcovtask'
33
+ Rcov::RcovTask.new do |test|
34
+ test.libs << 'test'
35
+ test.pattern = 'test/**/test_*.rb'
36
+ test.verbose = true
37
+ end
38
+ rescue LoadError
39
+ task :rcov do
40
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
41
+ end
42
+ end
43
+
44
+ task :test => :check_dependencies
45
+
46
+ task :default => :test
47
+
48
+ require 'rake/rdoctask'
49
+ Rake::RDocTask.new do |rdoc|
50
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
51
+
52
+ rdoc.rdoc_dir = 'rdoc'
53
+ rdoc.title = "tinman #{version}"
54
+ rdoc.rdoc_files.include('README*')
55
+ rdoc.rdoc_files.include('lib/**/*.rb')
56
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/lib/ext/ext.rb ADDED
@@ -0,0 +1,46 @@
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
37
+
38
+ class Date
39
+ # This check is for people running TinMan with ActiveSupport, avoid a collision
40
+ unless respond_to? :iso8601
41
+ # Return the date as a String formatted according to ISO 8601.
42
+ def iso8601
43
+ ::Time.utc(year, month, day, 0, 0, 0, 0).iso8601
44
+ end
45
+ end
46
+ end
data/lib/tinman.rb ADDED
@@ -0,0 +1,350 @@
1
+ require 'yaml'
2
+ require 'date'
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 TinMan
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, config, &blk
32
+ path = ([:layout, :repo].include?(page) ? Paths[:templates] : Paths[:pages])
33
+ config[:to_html].call(path, page, binding)
34
+ end
35
+
36
+ def redcloth text
37
+ if (options = @config[:redcloth])
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
+ class Site
56
+ def initialize config
57
+ @config = config
58
+ end
59
+
60
+ def [] *args
61
+ @config[*args]
62
+ end
63
+
64
+ def []= key, value
65
+ @config.set key, value
66
+ end
67
+
68
+ def index type = :html
69
+ articles = type == :html ? self.articles.reverse : self.articles
70
+ {:articles => articles.map do |article|
71
+ Article.new article, @config
72
+ end}.merge archives
73
+ end
74
+
75
+ def archives filter = ""
76
+ entries = ! self.articles.empty??
77
+ self.articles.select do |a|
78
+ filter !~ /^\d{4}/ || File.basename(a) =~ /^#{filter}/
79
+ end.reverse.map do |article|
80
+ Article.new article, @config
81
+ end : []
82
+
83
+ return :archives => Archives.new(entries, @config)
84
+ end
85
+
86
+ def article route
87
+ Article.new("#{Paths[:articles]}/#{route.join('-')}.#{self[:ext]}", @config).load
88
+ end
89
+
90
+ def /
91
+ self[:root]
92
+ end
93
+
94
+ def go route, type = :html
95
+ route << self./ if route.empty?
96
+ type, path = type =~ /html|xml|json/ ? type.to_sym : :html, route.join('/')
97
+ context = lambda do |data, page|
98
+ Context.new(data, @config, path).render(page, type)
99
+ end
100
+
101
+ body, status = if Context.new.respond_to?(:"to_#{type}")
102
+ if route.first =~ /\d{4}/
103
+ case route.size
104
+ when 1..3
105
+ context[archives(route * '-'), :archives]
106
+ when 4
107
+ context[article(route), :article]
108
+ else http 400
109
+ end
110
+ elsif respond_to?(path)
111
+ context[send(path, type), path.to_sym]
112
+ elsif (repo = @config[:github][:repos].grep(/#{path}/).first) &&
113
+ !@config[:github][:user].empty?
114
+ context[Repo.new(repo, @config), :repo]
115
+ else
116
+ context[{}, path.to_sym]
117
+ end
118
+ else
119
+ http 400
120
+ end
121
+
122
+ rescue Errno::ENOENT => e
123
+ return :body => http(404).first, :type => :html, :status => 404
124
+ else
125
+ return :body => body || "", :type => type, :status => status || 200
126
+ end
127
+
128
+ protected
129
+
130
+ def http code
131
+ [@config[:error].call(code), code]
132
+ end
133
+
134
+ def articles
135
+ self.class.articles self[:ext]
136
+ end
137
+
138
+ def self.articles ext
139
+ Dir["#{Paths[:articles]}/*.#{ext}"].sort_by {|entry| File.basename(entry) }
140
+ end
141
+
142
+ class Context
143
+ include Template
144
+
145
+ def initialize ctx = {}, config = {}, path = "/"
146
+ @config, @context, @path = config, ctx, path
147
+ @articles = Site.articles(@config[:ext]).reverse.map do |a|
148
+ Article.new(a, @config)
149
+ end
150
+
151
+ ctx.each do |k, v|
152
+ meta_def(k) { ctx.instance_of?(Hash) ? v : ctx.send(k) }
153
+ end
154
+ end
155
+
156
+ def title
157
+ @config[:title]
158
+ end
159
+
160
+ def render page, type
161
+ content = to_html page, @config
162
+ type == :html ? to_html(:layout, @config, &Proc.new { content }) : send(:"to_#{type}", page)
163
+ end
164
+
165
+ def to_xml page
166
+ xml = Builder::XmlMarkup.new(:indent => 2)
167
+ instance_eval File.read("#{Paths[:templates]}/#{page}.builder")
168
+ end
169
+ alias :to_atom to_xml
170
+
171
+ def method_missing m, *args, &blk
172
+ @context.respond_to?(m) ? @context.send(m, *args, &blk) : super
173
+ end
174
+ end
175
+ end
176
+
177
+ class Repo < Hash
178
+ include Template
179
+
180
+ README = "http://github.com/%s/%s/raw/master/README.%s"
181
+
182
+ def initialize name, config
183
+ self[:name], @config = name, config
184
+ end
185
+
186
+ def readme
187
+ redcloth open(README %
188
+ [@config[:github][:user], self[:name], @config[:github][:ext]]).read
189
+ rescue Timeout::Error, OpenURI::HTTPError => e
190
+ "This page isn't available."
191
+ end
192
+ alias :content readme
193
+ end
194
+
195
+ class Archives < Array
196
+ include Template
197
+
198
+ def initialize articles, config
199
+ self.replace articles
200
+ @config = config
201
+ end
202
+
203
+ def [] a
204
+ a.is_a?(Range) ? self.class.new(self.slice(a) || [], @config) : super
205
+ end
206
+
207
+ def to_html
208
+ super(:archives, @config)
209
+ end
210
+ alias :to_s to_html
211
+ alias :archive archives
212
+ end
213
+
214
+ class Article < Hash
215
+ include Template
216
+
217
+ def initialize obj, config = {}
218
+ @obj, @config = obj, config
219
+ self.load if obj.is_a? Hash
220
+ end
221
+
222
+ def load
223
+ data = if @obj.is_a? String
224
+ meta, self[:body] = File.read(@obj).split(/\n\n/, 2)
225
+
226
+ # use the date from the filename, or else tinman won't find the article
227
+ @obj =~ /\/(\d{4}-\d{2}-\d{2})[^\/]*$/
228
+ ($1 ? {:date => $1} : {}).merge(YAML.load(meta))
229
+ elsif @obj.is_a? Hash
230
+ @obj
231
+ end.inject({}) {|h, (k,v)| h.merge(k.to_sym => v) }
232
+
233
+ self.taint
234
+ self.update data
235
+ self[:date] = Date.parse(self[:date].gsub('/', '-')) rescue Date.today
236
+ self
237
+ end
238
+
239
+ def [] key
240
+ self.load unless self.tainted?
241
+ super
242
+ end
243
+
244
+ def slug
245
+ self[:slug] || self[:title].slugize
246
+ end
247
+
248
+ def summary length = nil
249
+ config = @config[:summary]
250
+ sum = if self[:body] =~ config[:delim]
251
+ self[:body].split(config[:delim]).first
252
+ else
253
+ self[:body].match(/(.{1,#{length || config[:length] || config[:max]}}.*?)(\n|\Z)/m).to_s
254
+ end
255
+ redcloth(sum.length == self[:body].length ? sum : sum.strip.sub(/\.\Z/, '&hellip;'))
256
+ end
257
+
258
+ def url
259
+ "http://#{(@config[:url].sub("http://", '') + self.path).squeeze('/')}"
260
+ end
261
+ alias :permalink url
262
+
263
+ def body
264
+ redcloth self[:body].sub(@config[:summary][:delim], '') rescue redcloth self[:body]
265
+ end
266
+
267
+ def path
268
+ @config[:prefix] + self[:date].strftime("/%Y/%m/%d/#{slug}/")
269
+ end
270
+
271
+ def title() self[:title] || "an article" end
272
+ def date() @config[:date].call(self[:date]) end
273
+ def author() self[:author] || @config[:author] end
274
+ def to_html() self.load; super(:article, @config) end
275
+ alias :to_s to_html
276
+ end
277
+
278
+ class Config < Hash
279
+ Defaults = {
280
+ :author => ENV['USER'], # blog author
281
+ :title => Dir.pwd.split('/').last, # site title
282
+ :root => "index", # site index
283
+ :url => "http://127.0.0.1", # root URL of the site
284
+ :prefix => "", # common path prefix for the blog
285
+ :date => lambda {|now| now.strftime("%d/%m/%Y") }, # date function
286
+ :redcloth => :smart, # use redcloth
287
+ :disqus => false, # disqus name
288
+ :summary => {:max => 150, :delim => /~\n/}, # length of summary and delimiter
289
+ :ext => 'txt', # extension for articles
290
+ :cache => 28800, # cache duration (seconds)
291
+ :github => {:user => "", :repos => [], :ext => 'md'}, # Github username and list of repos
292
+ :to_html => lambda {|path, page, ctx| # returns an html, from a path & context
293
+ ERB.new(File.read("#{path}/#{page}.rhtml")).result(ctx)
294
+ },
295
+ :error => lambda {|code| # The HTML for your error page
296
+ "<font style='font-size:300%'>tinman, we're not in Kansas anymore (#{code})</font>"
297
+ }
298
+ }
299
+ def initialize obj
300
+ self.update Defaults
301
+ self.update obj
302
+ end
303
+
304
+ def set key, val = nil, &blk
305
+ if val.is_a? Hash
306
+ self[key].update val
307
+ else
308
+ self[key] = block_given?? blk : val
309
+ end
310
+ end
311
+ end
312
+
313
+ class Server
314
+ attr_reader :config
315
+
316
+ def initialize config = {}, &blk
317
+ @config = config.is_a?(Config) ? config : Config.new(config)
318
+ @config.instance_eval(&blk) if block_given?
319
+ end
320
+
321
+ def call env
322
+ @request = Rack::Request.new env
323
+ @response = Rack::Response.new
324
+
325
+ return [400, {}, []] unless @request.get?
326
+
327
+ path, mime = @request.path_info.split('.')
328
+ route = (path || '/').split('/').reject {|i| i.empty? }
329
+
330
+ response = TinMan::Site.new(@config).go(route, *(mime ? mime : []))
331
+
332
+ @response.body = [response[:body]]
333
+ @response['Content-Length'] = response[:body].length.to_s unless response[:body].empty?
334
+ @response['Content-Type'] = Rack::Mime.mime_type(".#{response[:type]}")
335
+
336
+ # Set http cache headers
337
+ @response['Cache-Control'] = if TinMan.env == 'production'
338
+ "public, max-age=#{@config[:cache]}"
339
+ else
340
+ "no-cache, must-revalidate"
341
+ end
342
+
343
+ @response['ETag'] = Digest::SHA1.hexdigest(response[:body])
344
+
345
+ @response.status = response[:status]
346
+ @response.finish
347
+ end
348
+ end
349
+ end
350
+
@@ -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/tinman/(.*)\.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") }
@@ -0,0 +1 @@
1
+ <span id="count"><%= @articles.length %></span>
@@ -0,0 +1,5 @@
1
+ <h1><%= @path %></h1>
2
+ <% for entry in archives %>
3
+ <li class="entry"><%= entry.title %></li>
4
+ <% end %>
5
+
@@ -0,0 +1,4 @@
1
+ <h2><%= title %></h2>
2
+ <span><%= date %></h2>
3
+ <p><%= body %></p>
4
+
@@ -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.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,9 @@
1
+ <ul id="articles">
2
+ <% for article in articles[0...3] %>
3
+ <li><%= article %></li>
4
+ <% end %>
5
+ </ul>
6
+ <div id="archives">
7
+ <%= archives[3...5] %>
8
+ </div>
9
+
@@ -0,0 +1,4 @@
1
+ <html>
2
+ <%= yield %>
3
+ </html>
4
+
@@ -0,0 +1 @@
1
+ <%= readme %>
@@ -0,0 +1,44 @@
1
+ require 'rubygems'
2
+ require 'hpricot'
3
+ require 'riot'
4
+
5
+ $:.unshift File.dirname(__FILE__)
6
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
7
+
8
+ require 'tinman'
9
+
10
+ module TinMan
11
+ class IncludesHTMLMacro < Riot::AssertionMacro
12
+ register :includes_html
13
+
14
+ def evaluate(actual, expected)
15
+ doc = Hpricot.parse(actual)
16
+ expected = expected.to_a.flatten
17
+
18
+ if (doc/expected.first).empty?
19
+ fail("expected #{actual} to contain a <#{expected.first}>")
20
+ elsif !(doc/expected.first).inner_html.match(expected.last)
21
+ fail("expected <#{expected.first}> to contain #{expected.last}")
22
+ else
23
+ pass
24
+ end
25
+ end
26
+ end
27
+
28
+ class IncludesElementsMacro < Riot::AssertionMacro
29
+ register :includes_elements
30
+
31
+ def evaluate(actual, selector, count)
32
+ doc = Hpricot.parse(actual)
33
+ (doc/selector).size == count ? pass : fail("expected #{actual} to contain #{count} #{selector}(s)")
34
+ end
35
+ end
36
+
37
+ class WithinMacro < Riot::AssertionMacro
38
+ register :within
39
+
40
+ def evaluate(actual, expected)
41
+ expected.include?(actual) ? pass : fail("expected #{actual} to be within #{expected}")
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,217 @@
1
+ require 'test/test_helper'
2
+ require 'date'
3
+
4
+ URL = "http://tinman.oz"
5
+ AUTHOR = "tinman"
6
+
7
+ context TinMan do
8
+ setup do
9
+ @config = TinMan::Config.new(:redcloth => true, :author => AUTHOR, :url => URL)
10
+ @tinman = Rack::MockRequest.new(TinMan::Server.new(@config))
11
+ TinMan::Paths[:articles] = "test/articles"
12
+ TinMan::Paths[:pages] = "test/templates"
13
+ TinMan::Paths[:templates] = "test/templates"
14
+ end
15
+
16
+ context "GET /" do
17
+ setup { @tinman.get('/') }
18
+
19
+ asserts("returns a 200") { topic.status }.equals 200
20
+ asserts("body is not empty") { not topic.body.empty? }
21
+ asserts("content type is set properly") { topic.content_type }.equals "text/html"
22
+ should("include a couple of article") { topic.body }.includes_elements("#articles li", 3)
23
+ should("include an archive") { topic.body }.includes_elements("#archives li", 2)
24
+
25
+ context "with no articles" do
26
+ setup { Rack::MockRequest.new(TinMan::Server.new(@config.merge(:ext => 'oxo'))).get('/') }
27
+
28
+ asserts("body is not empty") { not topic.body.empty? }
29
+ asserts("returns a 200") { topic.status }.equals 200
30
+ end
31
+
32
+ context "with a user-defined to_html" do
33
+ setup do
34
+ @config[:to_html] = lambda do |path, page, binding|
35
+ ERB.new(File.read("#{path}/#{page}.rhtml")).result(binding)
36
+ end
37
+ @tinman.get('/')
38
+ end
39
+
40
+ asserts("returns a 200") { topic.status }.equals 200
41
+ asserts("body is not empty") { not topic.body.empty? }
42
+ asserts("content type is set properly") { topic.content_type }.equals "text/html"
43
+ should("include a couple of article") { topic.body }.includes_elements("#articles li", 3)
44
+ should("include an archive") { topic.body }.includes_elements("#archives li", 2)
45
+ asserts("Etag header present") { topic.headers.include? "ETag" }
46
+ asserts("Etag header has a value") { not topic.headers["ETag"].empty? }
47
+ end
48
+ end
49
+
50
+ context "GET /about" do
51
+ setup { @tinman.get('/about') }
52
+ asserts("returns a 200") { topic.status }.equals 200
53
+ asserts("body is not empty") { not topic.body.empty? }
54
+ should("have access to @articles") { topic.body }.includes_html("#count" => /5/)
55
+ end
56
+
57
+ context "GET a single article" do
58
+ setup { @tinman.get("/1900/05/17/the-wonderful-wizard-of-oz") }
59
+ asserts("returns a 200") { topic.status }.equals 200
60
+ asserts("content type is set properly") { topic.content_type }.equals "text/html"
61
+ should("contain the article") { topic.body }.includes_html("p" => /<em>Once upon a time<\/em>/)
62
+ end
63
+
64
+ context "GET to the archive" do
65
+ context "through a year" do
66
+ setup { @tinman.get('/2009') }
67
+ asserts("returns a 200") { topic.status }.equals 200
68
+ should("includes the entries for that year") { topic.body }.includes_elements("li.entry", 3)
69
+ end
70
+
71
+ context "through a year & month" do
72
+ setup { @tinman.get('/2009/12') }
73
+ asserts("returns a 200") { topic.status }.equals 200
74
+ should("includes the entries for that month") { topic.body }.includes_elements("li.entry", 2)
75
+ should("includes the year & month") { topic.body }.includes_html("h1" => /2009\/12/)
76
+ end
77
+
78
+ context "through /archive" do
79
+ setup { @tinman.get('/archive') }
80
+ end
81
+ end
82
+
83
+ context "GET to an unknown route" do
84
+ setup { @tinman.get('/unknown') }
85
+ should("returns a 404") { topic.status }.equals 404
86
+ end
87
+
88
+ context "Request is invalid" do
89
+ setup { @tinman.delete('/invalid') }
90
+ should("returns a 400") { topic.status }.equals 400
91
+ end
92
+
93
+ context "GET /index.xml (atom feed)" do
94
+ setup { @tinman.get('/index.xml') }
95
+ asserts("content type is set properly") { topic.content_type }.equals "application/xml"
96
+ asserts("body should be valid xml") { topic.body }.includes_html("feed > entry" => /.+/)
97
+ asserts("summary shouldn't be empty") { topic.body }.includes_html("summary" => /.{10,}/)
98
+ end
99
+
100
+ context "GET to a repo name" do
101
+ setup do
102
+ class TinMan::Repo
103
+ def readme() "#{self[:name]}'s README" end
104
+ end
105
+ end
106
+
107
+ context "when the repo is in the :repos array" do
108
+ setup do
109
+ @config[:github] = {:user => "cloudhead", :repos => ['the-repo']}
110
+ @tinman.get('/the-repo')
111
+ end
112
+ should("return the-repo's README") { topic.body }.includes("the-repo's README")
113
+ end
114
+
115
+ context "when the repo is not in the :repos array" do
116
+ setup do
117
+ @config[:github] = {:user => "cloudhead", :repos => []}
118
+ @tinman.get('/the-repo')
119
+ end
120
+ should("return a 404") { topic.status }.equals 404
121
+ end
122
+ end
123
+
124
+ context "creating an article" do
125
+ setup do
126
+ @config[:redcloth] = true
127
+ @config[:date] = lambda {|t| "the time is #{t.strftime("%Y/%m/%d %H:%M")}" }
128
+ @config[:summary] = {:length => 50}
129
+ end
130
+
131
+ context "with the bare essentials" do
132
+ setup do
133
+ TinMan::Article.new({
134
+ :title => "TinMan & The Wizard of Oz.",
135
+ :body => "#Chapter I\nhello, *stranger*."
136
+ }, @config)
137
+ end
138
+
139
+ should("have a title") { topic.title }.equals "TinMan & The Wizard of Oz."
140
+ should("parse the body as redcloth") { topic.body }.equals "<h1>Chapter I</h1>\n\n<p>hello, <em>stranger</em>.</p>\n"
141
+ should("create an appropriate slug") { topic.slug }.equals "tinman-and-the-wizard-of-oz"
142
+ should("set the date") { topic.date }.equals "the time is #{Date.today.strftime("%Y/%m/%d %H:%M")}"
143
+ should("create a summary") { topic.summary == topic.body }
144
+ should("have an author") { topic.author }.equals AUTHOR
145
+ should("have a path") { topic.path }.equals Date.today.strftime("/%Y/%m/%d/tinman-and-the-wizard-of-oz/")
146
+ should("have a url") { topic.url }.equals Date.today.strftime("#{URL}/%Y/%m/%d/tinman-and-the-wizard-of-oz/")
147
+ end
148
+
149
+ context "with a user-defined summary" do
150
+ setup do
151
+ TinMan::Article.new({
152
+ :title => "TinMan & The Wizard of Oz.",
153
+ :body => "Well,\nhello ~\n, *stranger*."
154
+ }, @config.merge(:redcloth => false, :summary => {:max => 150, :delim => /~\n/}))
155
+ end
156
+
157
+ should("split the article at the delimiter") { topic.summary }.equals "Well,\nhello"
158
+ should("not have the delimiter in the body") { topic.body !~ /~/ }
159
+ end
160
+
161
+ context "with everything specified" do
162
+ setup do
163
+ TinMan::Article.new({
164
+ :title => "The Wizard of Oz",
165
+ :body => ("a little bit of text." * 5) + "\n" + "filler" * 10,
166
+ :date => "19/10/1976",
167
+ :slug => "wizard-of-oz",
168
+ :author => "toetoe"
169
+ }, @config)
170
+ end
171
+
172
+ should("parse the date") { [topic[:date].month, topic[:date].year] }.equals [10, 1976]
173
+ should("use the slug") { topic.slug }.equals "wizard-of-oz"
174
+ should("use the author") { topic.author }.equals "toetoe"
175
+
176
+ context "and long first paragraph" do
177
+ should("create a valid summary") { topic.summary }.equals "<p>" + ("a little bit of text." * 5).chop + "&hellip;</p>\n"
178
+ end
179
+
180
+ context "and a short first paragraph" do
181
+ setup do
182
+ @config[:redcloth] = false
183
+ TinMan::Article.new({:body => "there ain't such thing as a free lunch\n" * 10}, @config)
184
+ end
185
+
186
+ should("create a valid summary") { topic.summary.size }.within 75..80
187
+ end
188
+ end
189
+ end
190
+
191
+ context "using Config#set with a hash" do
192
+ setup do
193
+ conf = TinMan::Config.new({})
194
+ conf.set(:summary, {:delim => /%/})
195
+ conf
196
+ end
197
+
198
+ should("set summary[:delim] to /%/") { topic[:summary][:delim].source }.equals "%"
199
+ should("leave the :max intact") { topic[:summary][:max] }.equals 150
200
+ end
201
+
202
+ context "using Config#set with a block" do
203
+ setup do
204
+ conf = TinMan::Config.new({})
205
+ conf.set(:to_html) {|path, p, _| path + p }
206
+ conf
207
+ end
208
+
209
+ should("set the value to a proc") { topic[:to_html] }.respond_to :call
210
+ end
211
+
212
+ context "extensions to the core Ruby library" do
213
+ should("respond to iso8601") { Date.today }.respond_to?(:iso8601)
214
+ end
215
+ end
216
+
217
+
data/tinman.gemspec ADDED
@@ -0,0 +1,79 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{tinman}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["clooudhead codesponge"]
12
+ s.date = %q{2010-04-10}
13
+ s.description = %q{toto with textile (via RedCloth)}
14
+ s.email = %q{billy@codesponge.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.md"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "README.md",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "lib/ext/ext.rb",
27
+ "lib/tinman.rb",
28
+ "test/articles/1900-05-17-the-wonderful-wizard-of-oz.txt",
29
+ "test/articles/2001-01-01-two-thousand-and-one.txt",
30
+ "test/articles/2009-04-01-tilt-factor.txt",
31
+ "test/articles/2009-12-04-some-random-article.txt",
32
+ "test/articles/2009-12-11-the-dichotomy-of-design.txt",
33
+ "test/autotest.rb",
34
+ "test/templates/about.rhtml",
35
+ "test/templates/archives.rhtml",
36
+ "test/templates/article.rhtml",
37
+ "test/templates/feed.builder",
38
+ "test/templates/index.builder",
39
+ "test/templates/index.rhtml",
40
+ "test/templates/layout.rhtml",
41
+ "test/templates/repo.rhtml",
42
+ "test/test_helper.rb",
43
+ "test/tinman_test.rb",
44
+ "tinman.gemspec"
45
+ ]
46
+ s.homepage = %q{http://github.com/codesponge/tinman}
47
+ s.rdoc_options = ["--charset=UTF-8"]
48
+ s.require_paths = ["lib"]
49
+ s.rubygems_version = %q{1.3.6}
50
+ s.summary = %q{toto with textile (via RedCloth)}
51
+ s.test_files = [
52
+ "test/autotest.rb",
53
+ "test/test_helper.rb",
54
+ "test/tinman_test.rb"
55
+ ]
56
+
57
+ if s.respond_to? :specification_version then
58
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
59
+ s.specification_version = 3
60
+
61
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
62
+ s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
63
+ s.add_runtime_dependency(%q<builder>, [">= 0"])
64
+ s.add_runtime_dependency(%q<rack>, [">= 0"])
65
+ s.add_runtime_dependency(%q<RedCloth>, [">= 0"])
66
+ else
67
+ s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
68
+ s.add_dependency(%q<builder>, [">= 0"])
69
+ s.add_dependency(%q<rack>, [">= 0"])
70
+ s.add_dependency(%q<RedCloth>, [">= 0"])
71
+ end
72
+ else
73
+ s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
74
+ s.add_dependency(%q<builder>, [">= 0"])
75
+ s.add_dependency(%q<rack>, [">= 0"])
76
+ s.add_dependency(%q<RedCloth>, [">= 0"])
77
+ end
78
+ end
79
+
metadata ADDED
@@ -0,0 +1,136 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tinman
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - clooudhead codesponge
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-04-10 00:00:00 -04:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: thoughtbot-shoulda
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :development
31
+ version_requirements: *id001
32
+ - !ruby/object:Gem::Dependency
33
+ name: builder
34
+ prerelease: false
35
+ requirement: &id002 !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ segments:
40
+ - 0
41
+ version: "0"
42
+ type: :runtime
43
+ version_requirements: *id002
44
+ - !ruby/object:Gem::Dependency
45
+ name: rack
46
+ prerelease: false
47
+ requirement: &id003 !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ segments:
52
+ - 0
53
+ version: "0"
54
+ type: :runtime
55
+ version_requirements: *id003
56
+ - !ruby/object:Gem::Dependency
57
+ name: RedCloth
58
+ prerelease: false
59
+ requirement: &id004 !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ segments:
64
+ - 0
65
+ version: "0"
66
+ type: :runtime
67
+ version_requirements: *id004
68
+ description: toto with textile (via RedCloth)
69
+ email: billy@codesponge.com
70
+ executables: []
71
+
72
+ extensions: []
73
+
74
+ extra_rdoc_files:
75
+ - LICENSE
76
+ - README.md
77
+ files:
78
+ - .document
79
+ - .gitignore
80
+ - LICENSE
81
+ - README.md
82
+ - Rakefile
83
+ - VERSION
84
+ - lib/ext/ext.rb
85
+ - lib/tinman.rb
86
+ - test/articles/1900-05-17-the-wonderful-wizard-of-oz.txt
87
+ - test/articles/2001-01-01-two-thousand-and-one.txt
88
+ - test/articles/2009-04-01-tilt-factor.txt
89
+ - test/articles/2009-12-04-some-random-article.txt
90
+ - test/articles/2009-12-11-the-dichotomy-of-design.txt
91
+ - test/autotest.rb
92
+ - test/templates/about.rhtml
93
+ - test/templates/archives.rhtml
94
+ - test/templates/article.rhtml
95
+ - test/templates/feed.builder
96
+ - test/templates/index.builder
97
+ - test/templates/index.rhtml
98
+ - test/templates/layout.rhtml
99
+ - test/templates/repo.rhtml
100
+ - test/test_helper.rb
101
+ - test/tinman_test.rb
102
+ - tinman.gemspec
103
+ has_rdoc: true
104
+ homepage: http://github.com/codesponge/tinman
105
+ licenses: []
106
+
107
+ post_install_message:
108
+ rdoc_options:
109
+ - --charset=UTF-8
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ segments:
117
+ - 0
118
+ version: "0"
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ segments:
124
+ - 0
125
+ version: "0"
126
+ requirements: []
127
+
128
+ rubyforge_project:
129
+ rubygems_version: 1.3.6
130
+ signing_key:
131
+ specification_version: 3
132
+ summary: toto with textile (via RedCloth)
133
+ test_files:
134
+ - test/autotest.rb
135
+ - test/test_helper.rb
136
+ - test/tinman_test.rb