toto 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,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