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 +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.md +166 -0
- data/Rakefile +56 -0
- data/VERSION +1 -0
- data/lib/ext/ext.rb +46 -0
- data/lib/tinman.rb +350 -0
- data/test/articles/1900-05-17-the-wonderful-wizard-of-oz.txt +5 -0
- data/test/articles/2001-01-01-two-thousand-and-one.txt +5 -0
- data/test/articles/2009-04-01-tilt-factor.txt +5 -0
- data/test/articles/2009-12-04-some-random-article.txt +5 -0
- data/test/articles/2009-12-11-the-dichotomy-of-design.txt +5 -0
- data/test/autotest.rb +34 -0
- data/test/templates/about.rhtml +1 -0
- data/test/templates/archives.rhtml +5 -0
- data/test/templates/article.rhtml +4 -0
- data/test/templates/feed.builder +21 -0
- data/test/templates/index.builder +21 -0
- data/test/templates/index.rhtml +9 -0
- data/test/templates/layout.rhtml +4 -0
- data/test/templates/repo.rhtml +1 -0
- data/test/test_helper.rb +44 -0
- data/test/tinman_test.rb +217 -0
- data/tinman.gemspec +79 -0
- metadata +136 -0
data/.document
ADDED
data/.gitignore
ADDED
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/, '…'))
|
|
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
|
+
|
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,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 @@
|
|
|
1
|
+
<%= readme %>
|
data/test/test_helper.rb
ADDED
|
@@ -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
|
data/test/tinman_test.rb
ADDED
|
@@ -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 + "…</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
|