shinmun 0.1 → 0.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.
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