shinmun 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. data/.gitignore +2 -0
  2. data/README.md +127 -62
  3. data/Rakefile +23 -11
  4. data/bin/shinmun +6 -1
  5. data/lib/shinmun/admin_controller.rb +161 -0
  6. data/lib/shinmun/aggregations/delicious.rb +57 -0
  7. data/lib/shinmun/aggregations/flickr.rb +81 -0
  8. data/lib/shinmun/blog.rb +319 -0
  9. data/lib/shinmun/cache.rb +59 -0
  10. data/lib/shinmun/comment.rb +44 -0
  11. data/lib/shinmun/controller.rb +135 -0
  12. data/lib/shinmun/helpers.rb +116 -0
  13. data/lib/shinmun/post.rb +166 -0
  14. data/lib/shinmun/template.rb +39 -0
  15. data/lib/shinmun.rb +14 -411
  16. metadata +15 -81
  17. data/example/posts/2008/9/example.md +0 -19
  18. data/example/posts/blog.yml +0 -10
  19. data/example/posts/uuid.state +0 -3
  20. data/example/public/controllers/comments.php +0 -56
  21. data/example/public/images/loading.gif +0 -0
  22. data/example/public/javascripts/comments.js +0 -60
  23. data/example/public/javascripts/highlight.js +0 -505
  24. data/example/public/javascripts/images/bg-fill.png +0 -0
  25. data/example/public/javascripts/images/bg.png +0 -0
  26. data/example/public/javascripts/images/blockquote.png +0 -0
  27. data/example/public/javascripts/images/bold.png +0 -0
  28. data/example/public/javascripts/images/code.png +0 -0
  29. data/example/public/javascripts/images/h1.png +0 -0
  30. data/example/public/javascripts/images/hr.png +0 -0
  31. data/example/public/javascripts/images/img.png +0 -0
  32. data/example/public/javascripts/images/italic.png +0 -0
  33. data/example/public/javascripts/images/link.png +0 -0
  34. data/example/public/javascripts/images/ol.png +0 -0
  35. data/example/public/javascripts/images/redo.png +0 -0
  36. data/example/public/javascripts/images/separator.png +0 -0
  37. data/example/public/javascripts/images/ul.png +0 -0
  38. data/example/public/javascripts/images/undo.png +0 -0
  39. data/example/public/javascripts/images/wmd-on.png +0 -0
  40. data/example/public/javascripts/images/wmd.png +0 -0
  41. data/example/public/javascripts/jquery-form.js +0 -869
  42. data/example/public/javascripts/jquery.js +0 -3383
  43. data/example/public/javascripts/languages/1c.js +0 -82
  44. data/example/public/javascripts/languages/axapta.js +0 -52
  45. data/example/public/javascripts/languages/bash.js +0 -80
  46. data/example/public/javascripts/languages/diff.js +0 -64
  47. data/example/public/javascripts/languages/dos.js +0 -33
  48. data/example/public/javascripts/languages/dynamic.js +0 -460
  49. data/example/public/javascripts/languages/ini.js +0 -36
  50. data/example/public/javascripts/languages/javascript.js +0 -38
  51. data/example/public/javascripts/languages/lisp.js +0 -86
  52. data/example/public/javascripts/languages/mel.js +0 -50
  53. data/example/public/javascripts/languages/profile.js +0 -50
  54. data/example/public/javascripts/languages/renderman.js +0 -71
  55. data/example/public/javascripts/languages/smalltalk.js +0 -53
  56. data/example/public/javascripts/languages/sql.js +0 -50
  57. data/example/public/javascripts/languages/static.js +0 -175
  58. data/example/public/javascripts/languages/vbscript.js +0 -25
  59. data/example/public/javascripts/languages/www.js +0 -245
  60. data/example/public/javascripts/prettyDate.js +0 -36
  61. data/example/public/javascripts/showdown.js +0 -421
  62. data/example/public/javascripts/template.js +0 -165
  63. data/example/public/javascripts/wmd-base.js +0 -1799
  64. data/example/public/javascripts/wmd-plus.js +0 -311
  65. data/example/public/javascripts/wmd.js +0 -73
  66. data/example/public/stylesheets/grid.css +0 -243
  67. data/example/public/stylesheets/grid.png +0 -0
  68. data/example/public/stylesheets/highlight/ascetic.css +0 -38
  69. data/example/public/stylesheets/highlight/dark.css +0 -96
  70. data/example/public/stylesheets/highlight/default.css +0 -91
  71. data/example/public/stylesheets/highlight/far.css +0 -95
  72. data/example/public/stylesheets/highlight/idea.css +0 -75
  73. data/example/public/stylesheets/highlight/sunburst.css +0 -112
  74. data/example/public/stylesheets/highlight/zenburn.css +0 -108
  75. data/example/public/stylesheets/print.css +0 -76
  76. data/example/public/stylesheets/reset.css +0 -45
  77. data/example/public/stylesheets/style.css +0 -141
  78. data/example/public/stylesheets/typography.css +0 -59
  79. data/example/templates/feed.rxml +0 -21
  80. data/example/templates/layout.rhtml +0 -54
  81. data/example/templates/page.rhtml +0 -4
  82. data/example/templates/post.rhtml +0 -57
  83. data/example/templates/posts.rhtml +0 -10
@@ -0,0 +1,319 @@
1
+ module Shinmun
2
+
3
+ class Blog
4
+
5
+ # Define reader methods for configuration.
6
+ def self.config_reader(file, *names)
7
+ names.each do |name|
8
+ name = name.to_s
9
+ define_method(name) { @config[file][name] }
10
+ end
11
+ end
12
+
13
+ attr_reader :root, :posts, :pages, :aggregations
14
+
15
+ config_reader 'config/assets.yml', :javascript_files, :stylesheet_files, :base_path, :images_path, :javascripts_path, :stylesheets_path
16
+ config_reader 'config/blog.yml', :title, :description, :language, :author, :url, :repository
17
+ config_reader 'config/categories.yml', :categories
18
+
19
+ # Initialize the blog, load the config file and write the index files.
20
+ def initialize
21
+ @config = Cache.new do |file|
22
+ YAML.load(File.read(file))
23
+ end
24
+
25
+ @stylesheets = Cache.new
26
+
27
+ @templates = Cache.new do |file|
28
+ ERB.new(File.read(file))
29
+ end
30
+
31
+ @javascripts = Cache.new do |file|
32
+ script = File.read(file)
33
+ script = Packr.pack(script) if defined?(Packr)
34
+ "/* #{file} */\n #{script}\n"
35
+ end
36
+
37
+ @posts_cache = Cache.new do |file|
38
+ Post.new(:filename => file).load
39
+ end
40
+
41
+ @pages_cache = Cache.new do |file|
42
+ Post.new(:filename => file).load
43
+ end
44
+
45
+ Dir['pages/**/*'].each do |file|
46
+ @pages_cache.load(file) if File.file?(file) and file[-1, 1] != '~'
47
+ end
48
+
49
+ Dir['posts/**/*'].each do |file|
50
+ @posts_cache.load(file) if File.file?(file) and file[-1, 1] != '~'
51
+ end
52
+
53
+ @config.load('config/aggregations.yml')
54
+ @config.load('config/assets.yml')
55
+ @config.load('config/blog.yml')
56
+ @config.load('config/categories.yml')
57
+
58
+ @aggregations = {}
59
+
60
+ load_aggregations
61
+
62
+ Thread.start do
63
+ sleep 300
64
+ load_aggregations
65
+ end
66
+ end
67
+
68
+ def load_aggregations
69
+ @config['config/aggregations.yml'].to_a.each do |c|
70
+ @aggregations[c['name']] = Object.const_get(c['class']).new(c['url'])
71
+ end
72
+ end
73
+
74
+ # Reload config, assets and posts.
75
+ def reload
76
+ if @config.dirty? || @templates.dirty?
77
+ @config.reload!
78
+ @templates.reload!
79
+ @posts_cache.reload!
80
+ @pages_cache.reload!
81
+ else
82
+ @config.reload_dirty!
83
+ @templates.reload_dirty!
84
+ @posts_cache.reload_dirty!
85
+ @pages_cache.reload_dirty!
86
+ end
87
+
88
+ @posts = @posts_cache.values.sort_by { |p| p.date }.reverse
89
+ @pages = @pages_cache.values
90
+
91
+ pack_javascripts if @javascripts.dirty? or @javascripts.empty?
92
+ pack_stylesheets if @stylesheets.dirty? or @stylesheets.empty?
93
+
94
+ load 'templates/helpers.rb'
95
+ end
96
+
97
+ # Use rsync to synchronize the rendered blog to web server.
98
+ def push
99
+ system "rsync -avz public/ #{repository}"
100
+ end
101
+
102
+ def urlify(string)
103
+ string.downcase.gsub(/[ -]+/, '-').gsub(/[^-a-z0-9_]+/, '')
104
+ end
105
+
106
+ # Compress the javascripts using PackR and write them to one file called 'all.js'.
107
+ def pack_javascripts
108
+ @javascripts.reload_dirty!
109
+ File.open("assets/#{javascripts_path}/all.js", "wb") do |io|
110
+ for file in javascript_files
111
+ io << @javascripts["assets/#{javascripts_path}/#{file.strip}.js"] << "\n\n"
112
+ end
113
+ end
114
+ end
115
+
116
+ # Pack the stylesheets and write them to one file called 'all.css'.
117
+ def pack_stylesheets
118
+ @stylesheets.reload_dirty!
119
+ File.open("assets/#{stylesheets_path}/all.css", "wb") do |io|
120
+ for file in stylesheet_files
121
+ io << @stylesheets["assets/#{stylesheets_path}/#{file.strip}.css"] << "\n\n"
122
+ end
123
+ end
124
+ end
125
+
126
+ # Write a file to output directory.
127
+ def write_file(path, data)
128
+ FileUtils.mkdir_p("public/#{base_path}/#{File.dirname path}")
129
+
130
+ open("public/#{base_path}/#{path}", 'wb') do |file|
131
+ file << data
132
+ end
133
+ end
134
+
135
+ # Return all posts for a given month.
136
+ def posts_for_month(year, month)
137
+ posts.select { |p| p.year == year and p.month == month }
138
+ end
139
+
140
+ # Return all posts in given category.
141
+ def posts_for_category(category)
142
+ name = category['name']
143
+ posts.select { |p| p.category == name }
144
+ end
145
+
146
+ # Return all posts with any of given tags.
147
+ def posts_with_tags(tags)
148
+ return [] if tags.nil?
149
+ tags = tags.split(',').map { |t| t.strip } if tags.is_a?(String)
150
+ posts.select do |post|
151
+ tags.any? do |tag|
152
+ post.tags.to_s.include?(tag)
153
+ end
154
+ end
155
+ end
156
+
157
+ # Return all months as tuples of [year, month].
158
+ def months
159
+ posts.map { |p| [p.year, p.month] }.uniq.sort
160
+ end
161
+
162
+ # Create a new post with given title.
163
+ def create_post(title)
164
+ date = Date.today
165
+ name = urlify(title)
166
+ path = "#{date.year}/#{date.month}/#{name}.md"
167
+
168
+ if File.exist?(file)
169
+ raise "#{file} exists"
170
+ else
171
+ Post.new(:path => path,
172
+ :title => title,
173
+ :date => date).save
174
+ end
175
+ end
176
+
177
+ def find_page(path)
178
+ pages.find { |p| p.path == path }
179
+ end
180
+
181
+ def find_post(path)
182
+ posts.find { |p| p.path == path }
183
+ end
184
+
185
+ def find_category(category)
186
+ category = urlify(category)
187
+ categories.find { |c| urlify(c['name']) == category }
188
+ end
189
+
190
+ # Render template with given variables.
191
+ def render_template(name, vars)
192
+ template = Template.new(@templates["templates/#{name}"], name)
193
+ template.set_variables(vars)
194
+ template.render
195
+ end
196
+
197
+ def render_layout(vars)
198
+ render_template("layout.rhtml", vars.merge(:blog => self))
199
+ end
200
+
201
+ # Render named template and insert into layout with given variables.
202
+ def render(name, vars)
203
+ render_layout(vars.merge(:content => render_template(name, vars.merge(:blog => self))))
204
+ end
205
+
206
+ # Render given post using the post template and the layout template.
207
+ def render_post(post)
208
+ render('post.rhtml', post.variables.merge(:header => post.category))
209
+ end
210
+
211
+ # Render given page using only the layout template.
212
+ def render_page(page)
213
+ render_layout(page.variables.merge(:content => page.body_html))
214
+ end
215
+
216
+ # Render comments.
217
+ def render_comments(comments)
218
+ render_template('comments.rhtml', :comments => comments)
219
+ end
220
+
221
+ # Render index page using the index and the layout template.
222
+ def render_index_page
223
+ render('index.rhtml',
224
+ :header => 'Home',
225
+ :posts => posts)
226
+ end
227
+
228
+ # Render the category summary for given category.
229
+ def render_category(category)
230
+ posts = posts_for_category(category)
231
+ render("category.rhtml",
232
+ :header => category['name'],
233
+ :category => category,
234
+ :posts => posts)
235
+ end
236
+
237
+ # Render the archive summary for given month.
238
+ def render_month(year, month)
239
+ path = "#{year}/#{month}"
240
+ month_name = Date::MONTHNAMES[month]
241
+ posts = posts_for_month(year, month)
242
+ render("month.rhtml",
243
+ :header => "#{month_name} #{year}",
244
+ :year => year,
245
+ :month => month_name,
246
+ :posts => posts)
247
+ end
248
+
249
+ # Render index feed using the feed template.
250
+ def render_index_feed
251
+ render_template("index.rxml",
252
+ :blog => self,
253
+ :posts => posts)
254
+ end
255
+
256
+ # Render category feed for given category using the feed template .
257
+ def render_category_feed(category)
258
+ render_template("category.rxml",
259
+ :blog => self,
260
+ :category => category,
261
+ :posts => posts_for_category(category))
262
+ end
263
+
264
+ def write_index_page
265
+ write_file("index.html", render_index_page)
266
+ end
267
+
268
+ # Write all pages.
269
+ def write_pages
270
+ for page in pages
271
+ write_file("#{page.path}.html", render_page(page))
272
+ end
273
+ end
274
+
275
+ # Write all posts.
276
+ def write_posts
277
+ for post in posts
278
+ write_file("#{post.path}.html", render_post(post))
279
+ end
280
+ end
281
+
282
+ # Write archive summaries.
283
+ def write_archives
284
+ for year, month in months
285
+ write_file("#{year}/#{month}/index.html", render_month(year, month))
286
+ end
287
+ end
288
+
289
+ # Write category summaries.
290
+ def write_categories
291
+ for category in categories
292
+ write_file("categories/#{urlify category['name']}.html", render_category(category))
293
+ end
294
+ end
295
+
296
+ # Write rss feeds for index page and categories.
297
+ def write_feeds
298
+ write_file("index.rss", render_index_feed)
299
+ for category in categories
300
+ write_file("categories/#{urlify category['name']}.rss", render_category_feed(category))
301
+ end
302
+ end
303
+
304
+ # Render everything to public folder.
305
+ def write_all
306
+ load_aggregations
307
+ reload
308
+
309
+ write_index_page
310
+ write_pages
311
+ write_posts
312
+ write_archives
313
+ write_categories
314
+ write_feeds
315
+ end
316
+
317
+ end
318
+
319
+ end
@@ -0,0 +1,59 @@
1
+ module Shinmun
2
+
3
+ # A simple hashtable, which loads only changed files by calling reload.
4
+ class Cache
5
+
6
+ # Call with a block to specify how the data is loaded.
7
+ # This is the default behaviour: Cache.new {|file| File.read(file) }
8
+ def initialize(&block)
9
+ @map = {}
10
+ @callback = block || proc { |file| File.read(file) }
11
+ end
12
+
13
+ # Load a file into the cache, transform it according to callback
14
+ # and remember the modification time.
15
+ def load(file)
16
+ data = @callback.call(file)
17
+ @map[file] = [data, File.mtime(file)]
18
+ data
19
+ end
20
+
21
+ def remove(file)
22
+ @map.delete(file)
23
+ end
24
+
25
+ def dirty_files
26
+ @map.map { |file, (data, mtime)| mtime != File.mtime(file) ? file : nil }.compact
27
+ end
28
+
29
+ def reload!
30
+ @map.keys.each { |file| load file }
31
+ end
32
+
33
+ def reload_dirty!
34
+ dirty_files.each { |file| load file }
35
+ end
36
+
37
+ # Access the cache by filename.
38
+ def [](file)
39
+ data, mtime = @map[file]
40
+ data or load(file)
41
+ end
42
+
43
+ def values
44
+ @map.values.map { |data, | data }
45
+ end
46
+
47
+ # Are there any files loaded?
48
+ def empty?
49
+ @map.empty?
50
+ end
51
+
52
+ # Is there any file in this cache, which has changed?
53
+ def dirty?
54
+ dirty_files.size > 0
55
+ end
56
+
57
+ end
58
+
59
+ end
@@ -0,0 +1,44 @@
1
+ module Shinmun
2
+
3
+ class Comment
4
+
5
+ attr_accessor :time, :name, :email, :website, :text
6
+
7
+ def initialize(attributes)
8
+ for k, v in attributes
9
+ send "#{k}=", v
10
+ end
11
+ end
12
+
13
+ def self.read(path)
14
+ file = "comments/#{path}"
15
+ comments = []
16
+
17
+ if File.exist?(file)
18
+ File.open(file, "r") do |io|
19
+ io.flock(File::LOCK_SH)
20
+ YAML.each_document(io) do |comment|
21
+ comments << comment
22
+ end
23
+ io.flock(File::LOCK_UN)
24
+ end
25
+ end
26
+
27
+ comments
28
+ end
29
+
30
+ def self.write(path, comment)
31
+ file = "comments/#{path}"
32
+
33
+ FileUtils.mkdir_p(File.dirname(file))
34
+
35
+ File.open(file, "a") do |io|
36
+ io.flock(File::LOCK_EX)
37
+ io.puts(comment.to_yaml)
38
+ io.flock(File::LOCK_UN)
39
+ end
40
+ end
41
+
42
+ end
43
+
44
+ end
@@ -0,0 +1,135 @@
1
+ require 'rack'
2
+
3
+ module Shinmun
4
+
5
+ class RackAdapter
6
+
7
+ def initialize(blog)
8
+ @blog = blog
9
+ @routing = []
10
+
11
+ map /\.rss$/, FeedController
12
+ map /^\/\d\d\d\d\/\d+/, PostController
13
+ map /^\/categories/, CategoryController
14
+ map /^\/comments/, CommentsController
15
+ map //, PageController
16
+ end
17
+
18
+ def map(pattern, controller)
19
+ @routing << [pattern, controller]
20
+ end
21
+
22
+ def call(env)
23
+ request = Rack::Request.new(env)
24
+ response = Rack::Response.new
25
+
26
+ @blog.reload
27
+
28
+ klass = find_controller(request.path_info)
29
+
30
+ controller = klass.new(@blog, request, response)
31
+ controller.handle_request
32
+
33
+ response.status ||= 200
34
+ response.finish
35
+ end
36
+
37
+ def find_controller(path)
38
+ for pattern, klass in @routing
39
+ return klass if pattern.match(path)
40
+ end
41
+ end
42
+
43
+ end
44
+
45
+ class Controller
46
+ attr_reader :blog, :request, :response, :path, :extname
47
+
48
+ def initialize(blog, request, response)
49
+ @blog = blog
50
+ @request = request
51
+ @response = response
52
+ @extname = File.extname(request.path_info)
53
+ @path = request.path_info[1..-1].chomp(@extname)
54
+ end
55
+
56
+ def params
57
+ request.params
58
+ end
59
+
60
+ def redirect_to(location)
61
+ response.headers["Location"] = location
62
+ response.status = 302
63
+ end
64
+
65
+ def handle_request
66
+ action = request.request_method.downcase
67
+ response.body = send(action) if self.class.public_instance_methods.include?(action)
68
+ end
69
+ end
70
+
71
+ class PageController < Controller
72
+ def get
73
+ if path.empty?
74
+ blog.render_index_page
75
+ else
76
+ page = blog.find_page(path) or raise "#{path} not found"
77
+ blog.render_page(page)
78
+ end
79
+ end
80
+ end
81
+
82
+ class PostController < PageController
83
+ def get
84
+ if post = blog.find_post(path)
85
+ blog.render_post(post)
86
+ else
87
+ year, month, = path.split('/')
88
+ blog.render_month(year.to_i, month.to_i)
89
+ end
90
+ end
91
+ end
92
+
93
+ class FeedController < Controller
94
+ def get
95
+ path_list = path.split('/')
96
+ case path_list[0]
97
+ when 'categories'
98
+ category = blog.find_category(path_list[1].chomp('.rss'))
99
+ blog.render_category_feed(category)
100
+ when 'index'
101
+ blog.render_index_feed
102
+ end
103
+ end
104
+ end
105
+
106
+ class CategoryController < Controller
107
+ def get
108
+ category = blog.find_category(path.split('/')[1].chomp('.html'))
109
+ blog.render_category(category)
110
+ end
111
+ end
112
+
113
+ class CommentsController < Controller
114
+
115
+ def post
116
+ params['path'].include?('..') and raise 'invalid path'
117
+
118
+ comment = Comment.new(:time => Time.now,
119
+ :name => params['name'],
120
+ :email => params['email'],
121
+ :website => params['website'],
122
+ :text => params['text'])
123
+
124
+ if params['preview'] == 'true'
125
+ blog.render_comments([comment])
126
+ else
127
+ Comment.write(params['path'], comment)
128
+ blog.render_comments(Comment.read(params['path']))
129
+ end
130
+ end
131
+
132
+ end
133
+
134
+ end
135
+
@@ -0,0 +1,116 @@
1
+ module Shinmun
2
+
3
+ module Helpers
4
+
5
+ # Render a hash as attributes for a HTML tag.
6
+ def attributes(attributes)
7
+ attributes.map { |k, v| %Q{#{k}="#{v}"} }.join(' ')
8
+ end
9
+
10
+ # Render a HTML tag with given name.
11
+ # The last argument specifies the attributes of the tag.
12
+ # The second argument may be the content of the tag.
13
+ def tag(name, *args)
14
+ text, attributes = args.first.is_a?(Hash) ? [nil, args.first] : args
15
+ "<#{name} #{attributes(attributes)}>#{text}</#{name}>"
16
+ end
17
+
18
+ # Render stylesheet link tag
19
+ def stylesheet_link_tag(*names)
20
+ options = names.last.is_a?(Hash) ? names.pop : {}
21
+ options[:media] ||= 'screen'
22
+ names.map { |name|
23
+ mtime = File.mtime("assets/#{blog.stylesheets_path}/#{name}.css").to_i
24
+ path = "/#{blog.stylesheets_path}/#{name}.css?#{mtime}"
25
+ tag :link, :href => path, :rel => 'stylesheet', :media => options[:media]
26
+ }.join("\n")
27
+ end
28
+
29
+ # Render javascript tag
30
+ def javascript_tag(*names)
31
+ names.map { |name|
32
+ mtime = File.mtime("assets/#{blog.javascripts_path}/#{name}.js").to_i
33
+ path = "/#{blog.javascripts_path}/#{name}.js?#{mtime}"
34
+ tag :script, :src => path, :type => 'text/javascript'
35
+ }.join("\n")
36
+ end
37
+
38
+ # Render an image tag
39
+ def image_tag(file, options = {})
40
+ mtime = File.mtime("assets/#{blog.images_path}/#{file}").to_i
41
+ path = "/#{blog.images_path}/#{file}?#{mtime}"
42
+ tag :img, options.merge(:src => path)
43
+ end
44
+
45
+ # Render a link
46
+ def link_to(text, path, options = {})
47
+ tag :a, text, options.merge(:href => path)
48
+ end
49
+
50
+ # Render a link to a post
51
+ def post_link(post)
52
+ link_to post.title, "#{blog.base_path}/#{post.path}.html"
53
+ end
54
+
55
+ # Render a link to an archive page.
56
+ def archive_link(year, month)
57
+ link_to "#{Date::MONTHNAMES[month]} #{year}", "#{blog.base_path}/#{year}/#{month}/index.html"
58
+ end
59
+
60
+ # Render a date or time in a nice human readable format.
61
+ def date(time)
62
+ "%s %d, %d" % [Date::MONTHNAMES[time.month], time.day, time.year]
63
+ end
64
+
65
+ # Render a date or time in rfc822 format. This will be used for rss rendering.
66
+ def rfc822(time)
67
+ time.strftime("%a, %d %b %Y %H:%M:%S %z")
68
+ end
69
+
70
+ def markdown(text, *args)
71
+ BlueCloth.new(text, *args).to_html
72
+ rescue => e
73
+ "#{text}<br/><br/><strong style='color:red'>#{e.message}</strong>"
74
+ end
75
+
76
+ def strip_tags(str)
77
+ str.gsub(/<\/?[^>]*>/, "")
78
+ end
79
+
80
+ def urlify(string)
81
+ string.downcase.gsub(/[ -]+/, '-').gsub(/[^-a-z0-9_]+/, '')
82
+ end
83
+
84
+ # taken form ActionView::Helpers
85
+ def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false)
86
+ from_time = from_time.to_time if from_time.respond_to?(:to_time)
87
+ to_time = to_time.to_time if to_time.respond_to?(:to_time)
88
+ distance_in_minutes = (((to_time - from_time).abs)/60).round
89
+ distance_in_seconds = ((to_time - from_time).abs).round
90
+
91
+ case distance_in_minutes
92
+ when 0..1
93
+ return (distance_in_minutes == 0) ? 'less than a minute' : '1 minute' unless include_seconds
94
+ case distance_in_seconds
95
+ when 0..4 then 'less than 5 seconds'
96
+ when 5..9 then 'less than 10 seconds'
97
+ when 10..19 then 'less than 20 seconds'
98
+ when 20..39 then 'half a minute'
99
+ when 40..59 then 'less than a minute'
100
+ else '1 minute'
101
+ end
102
+
103
+ when 2..44 then "#{distance_in_minutes} minutes"
104
+ when 45..89 then 'about 1 hour'
105
+ when 90..1439 then "about #{(distance_in_minutes.to_f / 60.0).round} hours"
106
+ when 1440..2879 then '1 day'
107
+ when 2880..43199 then "#{(distance_in_minutes / 1440).round} days"
108
+ when 43200..86399 then 'about 1 month'
109
+ when 86400..525599 then "#{(distance_in_minutes / 43200).round} months"
110
+ when 525600..1051199 then 'about 1 year'
111
+ else "over #{(distance_in_minutes / 525600).round} years"
112
+ end
113
+ end
114
+ end
115
+
116
+ end