tinman 0.1.0

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