toto 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,5 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 cloudhead
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,108 @@
1
+ toto
2
+ ====
3
+
4
+ the tiniest blogging engine in Oz!
5
+
6
+ introduction
7
+ ------------
8
+
9
+ toto is a git-powered, minimalist blog engine for the hackers of Oz. The engine weights around ~200 sloc at its worse.
10
+ There is no toto client, at least for now; everything goes through git.
11
+
12
+ philosophy
13
+ ----------
14
+
15
+ Everything that can be done better with another tool should be, but one should not have too much pie to stay fit.
16
+
17
+ how it works
18
+ ------------
19
+
20
+ - content is entirely managed trough **git**; you get full fledged version control for free.
21
+ - articles are stored as _.txt_ files, with embeded metadata (in yaml format).
22
+ - templating is done through **ERB**.
23
+ - toto is built right on top of **Rack**.
24
+ - comments are handled by disqus:(http://disqus.com)
25
+ - individual articles can be accessed through urls such as _/2009/11/21/blogging-with-toto_
26
+ - the archives can be accessed by year, month or day, wih the same format as above.
27
+
28
+ synopsis
29
+ --------
30
+
31
+ One would start by forking or cloning the toto-skeleton repo, to get a basic skeleton:
32
+
33
+ $ git clone git://github.com/cloudhead/toto-skeleton.git
34
+
35
+ One would then edit the template at will, it has the following structure:
36
+
37
+ templates/
38
+ |
39
+ +- layout.rhtml # the main site layout, shared by all pages
40
+ |
41
+ +- feed.builder # the builder template for the atom feed
42
+ |
43
+ +- pages/ # pages, such as home, about, etc go here
44
+ |
45
+ +- index.rhtml # the default page loaded from `/`, it displays the list of articles
46
+ |
47
+ +- article.rhtml # the article (post) partial and page
48
+ |
49
+ +- about.rhtml
50
+
51
+ One could then create a .txt article file in the `articles/` folder, and make sure it has the following format:
52
+
53
+ title: The Wonderful Wizard of Oz
54
+ author: Lyman Frank Baum
55
+ date: 1900/05/17
56
+
57
+ Dorothy lived in the midst of the great Kansas prairies, with Uncle Henry,
58
+ who was a farmer, and Aunt Em, who was the farmer's wife.
59
+
60
+ If one is familiar with webby or aerial, this shouldn't look funny. Basically the top of the file is in YAML format,
61
+ and the rest of it is the blog post. They are delimited by an empty line `/\n\n/`, as you can see above.
62
+ None of the information is compulsory, but it's strongly encouraged you specify it.
63
+ Note that one can also use `rake` to create an article stub, with `rake new`.
64
+
65
+ Once he finishes writing his beautiful tale, one can push to the git repo, as usual:
66
+
67
+ $ git add articles/wizard-of-oz.txt
68
+ $ git commit -m 'wrote the wizard of oz.'
69
+ $ git push remote master
70
+
71
+ Where `remote` is the name of your remote git repository. The article is now published.
72
+
73
+ ### server
74
+
75
+ Toto is built on top of **Rack**, and hence has a **rackup** file: _config.ru_.
76
+
77
+ #### on your own
78
+
79
+ You can run toto with any Rack compliant web server, such as **thin**, **mongrel** or **unicorn**.
80
+
81
+ With thin, you would do something like:
82
+
83
+ $ thin start -R config.ru
84
+
85
+ With unicorn, you can just do:
86
+
87
+ $ unicorn
88
+
89
+ #### on heroku
90
+
91
+ Toto was designed to work well with heroku:(http://heroku.com), it makes the most out of it's state-of-the-art caching,
92
+ by setting the _Cache-Control_ and _Etag_ HTTP headers.
93
+
94
+ ### configuration
95
+
96
+ You can configure toto, by modifying the _config.ru_ file. For example, if you want to set the blog author to 'John Galt',
97
+ you could add `set :author, 'John Galt'` inside the `Toto::Server.new` block. Here's the options hash with the defaults:
98
+
99
+ :author => ENV['USER'], # blog author
100
+ :title => Dir.pwd.split('/').last, # site title
101
+ :root => "index", # page to load on /
102
+ :date => lambda {|now| now.strftime("%d/%m/%Y") }, # date format for articles
103
+ :markdown => :smart, # use markdown + smart-mode
104
+ :disqus => false, # disqus id, or false
105
+ :summary => 150, # length of article summary
106
+ :ext => 'txt' # file extension for articles
107
+
108
+ Copyright (c) 2009 cloudhead. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "toto"
8
+ gem.summary = %Q{the tiniest blog-engine in Oz}
9
+ gem.description = %Q{the tiniest blog-engine in Oz.}
10
+ gem.email = "self@cloudhead.net"
11
+ gem.homepage = "http://github.com/cloudhead/toto"
12
+ gem.authors = ["cloudhead"]
13
+ gem.add_development_dependency "riot"
14
+ gem.add_dependency "builder"
15
+ gem.add_dependency "rack"
16
+ gem.add_dependency "rdiscount"
17
+ end
18
+ Jeweler::GemcutterTasks.new
19
+ rescue LoadError
20
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
21
+ end
22
+
23
+ require 'rake/testtask'
24
+ Rake::TestTask.new(:test) do |test|
25
+ test.libs << 'lib' << 'test'
26
+ test.pattern = 'test/**/*_test.rb'
27
+ test.verbose = true
28
+ end
29
+
30
+ task :test => :check_dependencies
31
+ task :default => :test
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/lib/ext.rb ADDED
@@ -0,0 +1,26 @@
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 Fixnum
10
+ def ordinal
11
+ # 1 => 1st
12
+ # 2 => 2nd
13
+ # 3 => 3rd
14
+ # ...
15
+ case self % 100
16
+ when 11..13; "#{self}th"
17
+ else
18
+ case self % 10
19
+ when 1; "#{self}st"
20
+ when 2; "#{self}nd"
21
+ when 3; "#{self}rd"
22
+ else "#{self}th"
23
+ end
24
+ end
25
+ end
26
+ end
data/lib/toto.rb ADDED
@@ -0,0 +1,276 @@
1
+ require 'yaml'
2
+ require 'time'
3
+ require 'erb'
4
+ require 'rack'
5
+ require 'digest'
6
+
7
+ require 'rdiscount'
8
+ require 'builder'
9
+ require 'ext'
10
+
11
+ module Toto
12
+ Paths = {
13
+ :templates => "templates",
14
+ :pages => "templates/pages",
15
+ :articles => "articles"
16
+ }
17
+
18
+ module Template
19
+ def to_html page, &blk
20
+ path = (page == :layout ? Paths[:templates] : Paths[:pages])
21
+ ERB.new(File.read("#{path}/#{page}.rhtml")).result(binding)
22
+ end
23
+
24
+ def self.included obj
25
+ obj.class_eval do
26
+ define_method(obj.to_s.split('::').last.downcase) { self }
27
+ end
28
+ end
29
+ end
30
+
31
+ class Site
32
+ def initialize config
33
+ @config = config
34
+ end
35
+
36
+ def [] *args
37
+ @config[*args]
38
+ end
39
+
40
+ def []= key, value
41
+ @config.set key, value
42
+ end
43
+
44
+ def index type = :html
45
+ case type
46
+ when :html
47
+ {:articles => self.articles.reverse.map do |article|
48
+ Article.new File.new(article), @config
49
+ end }.merge archives
50
+ when :xml, :json
51
+ return :articles => self.articles.map do |article|
52
+ Article.new File.new(article), @config
53
+ end
54
+ else return {}
55
+ end
56
+ end
57
+
58
+ def archives filter = //
59
+ entries = ! self.articles.empty??
60
+ self.articles.select do |a|
61
+ File.basename(a) =~ /^#{filter}/
62
+ end.reverse.map do |article|
63
+ Article.new File.new(article), @config
64
+ end : []
65
+
66
+ return :archives => Archives.new(entries)
67
+ end
68
+
69
+ def article route
70
+ Article.new(File.new("#{Paths[:articles]}/#{route.join('-')}.#{self[:ext]}")).load
71
+ end
72
+
73
+ def /
74
+ self[:root]
75
+ end
76
+
77
+ def go route, type = :html
78
+ route << self./ if route.empty?
79
+ type = type.to_sym
80
+
81
+ body, status = if Context.new.respond_to?(:"to_#{type}")
82
+ if route.first =~ /\d{4}/
83
+ begin
84
+ case route.size
85
+ when 1..3
86
+ [Context.new(archives(route * '-'), @config).render(:archives, type), 200]
87
+ when 4
88
+ [Context.new(article(route), @config).render(:article, type), 200]
89
+ else http 400
90
+ end
91
+ rescue Errno::ENOENT => e
92
+ $stderr.puts e
93
+ http 401
94
+ end
95
+ elsif respond_to?(route = route.first.to_sym)
96
+ [Context.new(send(route, type), @config).render(route, type), 200]
97
+ else
98
+ http 401
99
+ end
100
+ else
101
+ http 400
102
+ end
103
+
104
+ return :body => body, :type => type, :status => status
105
+ end
106
+
107
+ protected
108
+
109
+ def http code
110
+ return ["<font style='font-size:300%'>toto, we're not in Kansas anymore (#{code})</font>", code]
111
+ end
112
+
113
+ def articles
114
+ Dir["#{Paths[:articles]}/*.#{self[:ext]}"]
115
+ end
116
+
117
+ class Context
118
+ include Template
119
+
120
+ def initialize ctx = {}, config = {}
121
+ @config = config
122
+ ctx.each {|k, v| meta_def(k) { v } }
123
+ end
124
+
125
+ def title
126
+ @config[:title]
127
+ end
128
+
129
+ def render page, type
130
+ type == :html ? to_html(:layout, &Proc.new { to_html page }) : send(:"to_#{type}", :feed)
131
+ end
132
+
133
+ def to_xml page
134
+ xml = Builder::XmlMarkup.new(:indent => 2)
135
+ instance_eval File.read("#{Paths[:templates]}/#{page}.builder")
136
+ end
137
+ alias :to_atom to_xml
138
+ end
139
+ end
140
+
141
+ class Archives < Array
142
+ include Template
143
+
144
+ def initialize articles
145
+ self.replace articles
146
+ end
147
+
148
+ def to_html
149
+ super(:archives)
150
+ end
151
+ alias :to_s to_html
152
+ alias :archive archives
153
+ end
154
+
155
+ class Article < Hash
156
+ include Template
157
+
158
+ def initialize obj, config = {}
159
+ @obj, @config = obj, config
160
+ end
161
+
162
+ def load
163
+ data = if @obj.is_a? File
164
+ meta, self[:body] = @obj.read.split(/\n\n/, 2)
165
+ YAML.load(meta)
166
+ elsif @obj.is_a? Hash
167
+ @obj
168
+ end.inject({}) {|h, (k,v)| h.merge(k.to_sym => v) }
169
+
170
+ self.taint
171
+ self.update data
172
+ self[:date] = Time.parse(self[:date]) rescue Time.now
173
+ self
174
+ end
175
+
176
+ def [] key
177
+ self.load unless self.tainted?
178
+ super
179
+ end
180
+
181
+
182
+ def slug
183
+ self[:slug] ||
184
+ self[:title].downcase.gsub(/&/, 'and').gsub(/\s+/, '-').gsub(/[^a-z0-9-]/, '')
185
+ end
186
+
187
+ def summary length = @config[:summary]
188
+ markdown self[:body].match(/(.{1,#{length}}.*?)(\n|\Z)/m).to_s
189
+ end
190
+
191
+ def url
192
+ "http://#{(@config[:url].sub("http://", '') + self.path).squeeze('/')}"
193
+ end
194
+
195
+ def title() self[:title] || "an article" end
196
+ def body() markdown self[:body] end
197
+ def date() @config[:date, self[:date]] end
198
+ def path() self[:date].strftime("/%Y/%m/%d/#{slug}/") end
199
+ def author() self[:author] || @config[:author] end
200
+ def to_html() self.load; super(:article) end
201
+
202
+ alias :to_s to_html
203
+
204
+ def method_missing m, *args, &blk
205
+ self.keys.include?(m) ? self[m] : super
206
+ end
207
+
208
+ private
209
+
210
+ def markdown text
211
+ if (markdown = @config[:markdown])
212
+ Markdown.new(text.to_s.strip, *(markdown unless markdown.eql?(true))).to_html
213
+ else
214
+ text.strip
215
+ end
216
+ end
217
+ end
218
+
219
+ class Config < Hash
220
+ Defaults = {
221
+ :author => ENV['USER'], # blog author
222
+ :title => Dir.pwd.split('/').last, # site title
223
+ :root => "index", # site index
224
+ :url => "http://127.0.0.1",
225
+ :date => lambda {|now| now.strftime("%d/%m/%Y") }, # date function
226
+ :markdown => :smart, # use markdown
227
+ :disqus => false, # disqus name
228
+ :summary => 150, # length of summary
229
+ :ext => 'txt' # extension for articles
230
+ }
231
+ def initialize obj
232
+ self.update Defaults
233
+ self.update obj
234
+ end
235
+
236
+ alias set :[]=
237
+
238
+ def [] key, *args
239
+ val = super(key)
240
+ val.respond_to?(:call) ? val.call(*args) : val
241
+ end
242
+ end
243
+
244
+ class Server
245
+ attr_reader :config
246
+
247
+ def initialize config = {}, &blk
248
+ @config = config.is_a?(Config) ? config : Config.new(config)
249
+ @config.instance_eval(&blk) if block_given?
250
+ end
251
+
252
+ def call env
253
+ @request = Rack::Request.new env
254
+ @response = Rack::Response.new
255
+
256
+ return [400, {}, []] unless @request.get?
257
+
258
+ path, mime = @request.path_info.split('.')
259
+ route = path.split('/').reject {|i| i.empty? }
260
+
261
+ response = Toto::Site.new(@config).go(route, *mime)
262
+
263
+ @response.body = [response[:body]]
264
+ @response['Content-Length'] = response[:body].length.to_s
265
+ @response['Content-Type'] = Rack::Mime.mime_type(".#{response[:type]}")
266
+
267
+ # Cache for one day
268
+ @response['Cache-Control'] = "public, max-age=86400"
269
+ @response['Etag'] = Digest::SHA1.hexdigest(response[:body])
270
+
271
+ @response.status = response[:status]
272
+ @response.finish
273
+ end
274
+ end
275
+ end
276
+
@@ -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/toto/(.*)\.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,4 @@
1
+ <% for entry in archives %>
2
+ <li class="entry"><%= entry.title %></li>
3
+ <% end %>
4
+
@@ -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,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,31 @@
1
+ require 'riot'
2
+ require 'hpricot'
3
+
4
+ $:.unshift File.dirname(__FILE__)
5
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
6
+
7
+ require 'toto'
8
+
9
+ module Riot
10
+ module AssertionMacros
11
+ def includes(expected)
12
+ actual.include?(expected) || fail("expected #{actual} to include #{expected}")
13
+ end
14
+
15
+ def includes_html(expected)
16
+ doc = Hpricot.parse(actual)
17
+ expected = expected.flatten
18
+ !(doc/expected.first).empty? || fail("expected #{actual} to contain a <#{expected.first}>")
19
+ (doc/expected.first).inner_html.match(expected.last) || fail("expected <#{expected.first}> to contain #{expected.last}")
20
+ end
21
+
22
+ def includes_elements(selector, count)
23
+ doc = Hpricot.parse(actual)
24
+ (doc/selector).size == count || fail("expected #{actual} to contain #{count} #{selector}(s)")
25
+ end
26
+
27
+ def within(expected)
28
+ expected.include?(actual) || fail("expected #{actual} to be within #{expected}")
29
+ end
30
+ end
31
+ end
data/test/toto_test.rb ADDED
@@ -0,0 +1,130 @@
1
+ require 'test/test_helper'
2
+
3
+ URL = "http://toto.oz"
4
+ AUTHOR = "toto"
5
+
6
+ context Toto do
7
+ setup do
8
+ @config = Toto::Config.new(:author => AUTHOR, :url => URL)
9
+ @toto = Rack::MockRequest.new(Toto::Server.new(@config))
10
+ Toto::Paths[:articles] = "test/articles"
11
+ Toto::Paths[:pages] = "test/templates"
12
+ Toto::Paths[:templates] = "test/templates"
13
+ end
14
+
15
+ context "GET /" do
16
+ setup { @toto.get('/') }
17
+
18
+ asserts("returns a 200") { topic.status }.equals 200
19
+ asserts("body is not empty") { not topic.body.empty? }
20
+ asserts("content type is set properly") { topic.content_type }.equals "text/html"
21
+ should("include a couple of article") { topic.body }.includes_elements("#articles li", 3)
22
+ should("include an archive") { topic.body }.includes_elements("#archives li", 2)
23
+
24
+ context "with no articles" do
25
+ setup { Rack::MockRequest.new(Toto::Server.new(@config.merge(:ext => 'oxo'))).get('/') }
26
+
27
+ asserts("body is not empty") { not topic.body.empty? }
28
+ asserts("returns a 200") { topic.status }.equals 200
29
+ end
30
+ end
31
+
32
+ context "GET a single article" do
33
+ setup { @toto.get("/1900/05/17/the-wonderful-wizard-of-oz") }
34
+ asserts("returns a 200") { topic.status }.equals 200
35
+ asserts("content type is set properly") { topic.content_type }.equals "text/html"
36
+ should("contain the article") { topic.body }.includes_html("p" => /Once upon a time/)
37
+ end
38
+
39
+ context "GET to the archive" do
40
+ context "through a year" do
41
+ setup { @toto.get('/2009') }
42
+ asserts("returns a 200") { topic.status }.equals 200
43
+ should("includes the entries for that year") { topic.body }.includes_elements("li.entry", 3)
44
+ end
45
+
46
+ context "through a year & month" do
47
+ setup { @toto.get('/2009/12') }
48
+ asserts("returns a 200") { topic.status }.equals 200
49
+ should("includes the entries for that month") { topic.body }.includes_elements("li.entry", 2)
50
+ end
51
+
52
+ context "through /archive" do
53
+ setup { @toto.get('/archive') }
54
+ end
55
+ end
56
+
57
+ context "GET to an unknown route" do
58
+ setup { @toto.get('/unknown') }
59
+ should("returns a 401") { topic.status }.equals 401
60
+ end
61
+
62
+ context "Request is invalid" do
63
+ setup { @toto.delete('/invalid') }
64
+ should("returns a 400") { topic.status }.equals 400
65
+ end
66
+
67
+ context "GET /index.xml (atom feed)" do
68
+ setup { @toto.get('/index.xml') }
69
+ asserts("content type is set properly") { topic.content_type }.equals "application/xml"
70
+ asserts("body should be valid xml") { topic.body }.includes_html("feed > entry" => /.+/)
71
+ asserts("summary shouldn't be empty") { topic.body }.includes_html("summary" => /.{10,}/)
72
+ end
73
+
74
+ context "creating an article" do
75
+ setup do
76
+ @config[:markdown] = true
77
+ @config[:date] = lambda {|t| "the time is #{t}" }
78
+ @config[:summary] = 50
79
+ end
80
+
81
+ context "with the bare essentials" do
82
+ setup do
83
+ Toto::Article.new({
84
+ :title => "Toto & The Wizard of Oz.",
85
+ :body => "#Chapter I\nhello, *stranger*."
86
+ }, @config)
87
+ end
88
+
89
+ should("have a title") { topic.title }.equals "Toto & The Wizard of Oz."
90
+ should("parse the body as markdown") { topic.body }.equals "<h1>Chapter I</h1>\n\n<p>hello, <em>stranger</em>.</p>\n"
91
+ should("create an appropriate slug") { topic.slug }.equals "toto-and-the-wizard-of-oz"
92
+ should("set the date") { topic.date }.equals "the time is #{Time.now}"
93
+ should("create a summary") { topic.summary == topic.body }
94
+ should("have an author") { topic.author }.equals AUTHOR
95
+ should("have a path") { topic.path }.equals Time.now.strftime("/%Y/%m/%d/toto-and-the-wizard-of-oz/")
96
+ should("have a url") { topic.url }.equals Time.now.strftime("#{URL}/%Y/%m/%d/toto-and-the-wizard-of-oz/")
97
+ end
98
+
99
+ context "with everything specified" do
100
+ setup do
101
+ Toto::Article.new({
102
+ :title => "The Wizard of Oz",
103
+ :body => ("a little bit of text." * 5) + "\n" + "filler" * 10,
104
+ :date => "19/10/1976",
105
+ :slug => "wizard-of-oz",
106
+ :author => "toetoe"
107
+ }, @config)
108
+ end
109
+
110
+ should("parse the date") { [topic[:date].month, topic[:date].year] }.equals [10, 1976]
111
+ should("use the slug") { topic.slug }.equals "wizard-of-oz"
112
+ should("use the author") { topic.author }.equals "toetoe"
113
+
114
+ context "and long first paragraph" do
115
+ should("create a valid summary") { topic.summary }.equals "<p>" + "a little bit of text." * 5 + "</p>\n"
116
+ end
117
+
118
+ context "and a short first paragraph" do
119
+ setup do
120
+ @config[:markdown] = false
121
+ Toto::Article.new({:body => "there ain't such thing as a free lunch\n" * 10}, @config)
122
+ end
123
+
124
+ should("create a valid summary") { topic.summary.size }.within (75..80)
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+
data/toto.gemspec ADDED
@@ -0,0 +1,76 @@
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{toto}
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 = ["cloudhead"]
12
+ s.date = %q{2009-11-24}
13
+ s.description = %q{the tiniest blog-engine in Oz.}
14
+ s.email = %q{self@cloudhead.net}
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.rb",
27
+ "lib/toto.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/archives.rhtml",
35
+ "test/templates/article.rhtml",
36
+ "test/templates/feed.builder",
37
+ "test/templates/index.rhtml",
38
+ "test/templates/layout.rhtml",
39
+ "test/test_helper.rb",
40
+ "test/toto_test.rb",
41
+ "toto.gemspec"
42
+ ]
43
+ s.homepage = %q{http://github.com/cloudhead/toto}
44
+ s.rdoc_options = ["--charset=UTF-8"]
45
+ s.require_paths = ["lib"]
46
+ s.rubygems_version = %q{1.3.5}
47
+ s.summary = %q{the tiniest blog-engine in Oz}
48
+ s.test_files = [
49
+ "test/autotest.rb",
50
+ "test/test_helper.rb",
51
+ "test/toto_test.rb"
52
+ ]
53
+
54
+ if s.respond_to? :specification_version then
55
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
56
+ s.specification_version = 3
57
+
58
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
59
+ s.add_development_dependency(%q<riot>, [">= 0"])
60
+ s.add_runtime_dependency(%q<builder>, [">= 0"])
61
+ s.add_runtime_dependency(%q<rack>, [">= 0"])
62
+ s.add_runtime_dependency(%q<rdiscount>, [">= 0"])
63
+ else
64
+ s.add_dependency(%q<riot>, [">= 0"])
65
+ s.add_dependency(%q<builder>, [">= 0"])
66
+ s.add_dependency(%q<rack>, [">= 0"])
67
+ s.add_dependency(%q<rdiscount>, [">= 0"])
68
+ end
69
+ else
70
+ s.add_dependency(%q<riot>, [">= 0"])
71
+ s.add_dependency(%q<builder>, [">= 0"])
72
+ s.add_dependency(%q<rack>, [">= 0"])
73
+ s.add_dependency(%q<rdiscount>, [">= 0"])
74
+ end
75
+ end
76
+
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: toto
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - cloudhead
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-11-24 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: riot
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: builder
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: rack
37
+ type: :runtime
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0"
44
+ version:
45
+ - !ruby/object:Gem::Dependency
46
+ name: rdiscount
47
+ type: :runtime
48
+ version_requirement:
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ description: the tiniest blog-engine in Oz.
56
+ email: self@cloudhead.net
57
+ executables: []
58
+
59
+ extensions: []
60
+
61
+ extra_rdoc_files:
62
+ - LICENSE
63
+ - README.md
64
+ files:
65
+ - .document
66
+ - .gitignore
67
+ - LICENSE
68
+ - README.md
69
+ - Rakefile
70
+ - VERSION
71
+ - lib/ext.rb
72
+ - lib/toto.rb
73
+ - test/articles/1900-05-17-the-wonderful-wizard-of-oz.txt
74
+ - test/articles/2001-01-01-two-thousand-and-one.txt
75
+ - test/articles/2009-04-01-tilt-factor.txt
76
+ - test/articles/2009-12-04-some-random-article.txt
77
+ - test/articles/2009-12-11-the-dichotomy-of-design.txt
78
+ - test/autotest.rb
79
+ - test/templates/archives.rhtml
80
+ - test/templates/article.rhtml
81
+ - test/templates/feed.builder
82
+ - test/templates/index.rhtml
83
+ - test/templates/layout.rhtml
84
+ - test/test_helper.rb
85
+ - test/toto_test.rb
86
+ - toto.gemspec
87
+ has_rdoc: true
88
+ homepage: http://github.com/cloudhead/toto
89
+ licenses: []
90
+
91
+ post_install_message:
92
+ rdoc_options:
93
+ - --charset=UTF-8
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: "0"
101
+ version:
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: "0"
107
+ version:
108
+ requirements: []
109
+
110
+ rubyforge_project:
111
+ rubygems_version: 1.3.5
112
+ signing_key:
113
+ specification_version: 3
114
+ summary: the tiniest blog-engine in Oz
115
+ test_files:
116
+ - test/autotest.rb
117
+ - test/test_helper.rb
118
+ - test/toto_test.rb