toto-haml 0.3.2

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