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.
- data/.gitignore +2 -0
- data/README.md +127 -62
- data/Rakefile +23 -11
- data/bin/shinmun +6 -1
- data/lib/shinmun/admin_controller.rb +161 -0
- data/lib/shinmun/aggregations/delicious.rb +57 -0
- data/lib/shinmun/aggregations/flickr.rb +81 -0
- data/lib/shinmun/blog.rb +319 -0
- data/lib/shinmun/cache.rb +59 -0
- data/lib/shinmun/comment.rb +44 -0
- data/lib/shinmun/controller.rb +135 -0
- data/lib/shinmun/helpers.rb +116 -0
- data/lib/shinmun/post.rb +166 -0
- data/lib/shinmun/template.rb +39 -0
- data/lib/shinmun.rb +14 -411
- metadata +15 -81
- data/example/posts/2008/9/example.md +0 -19
- data/example/posts/blog.yml +0 -10
- data/example/posts/uuid.state +0 -3
- data/example/public/controllers/comments.php +0 -56
- data/example/public/images/loading.gif +0 -0
- data/example/public/javascripts/comments.js +0 -60
- data/example/public/javascripts/highlight.js +0 -505
- data/example/public/javascripts/images/bg-fill.png +0 -0
- data/example/public/javascripts/images/bg.png +0 -0
- data/example/public/javascripts/images/blockquote.png +0 -0
- data/example/public/javascripts/images/bold.png +0 -0
- data/example/public/javascripts/images/code.png +0 -0
- data/example/public/javascripts/images/h1.png +0 -0
- data/example/public/javascripts/images/hr.png +0 -0
- data/example/public/javascripts/images/img.png +0 -0
- data/example/public/javascripts/images/italic.png +0 -0
- data/example/public/javascripts/images/link.png +0 -0
- data/example/public/javascripts/images/ol.png +0 -0
- data/example/public/javascripts/images/redo.png +0 -0
- data/example/public/javascripts/images/separator.png +0 -0
- data/example/public/javascripts/images/ul.png +0 -0
- data/example/public/javascripts/images/undo.png +0 -0
- data/example/public/javascripts/images/wmd-on.png +0 -0
- data/example/public/javascripts/images/wmd.png +0 -0
- data/example/public/javascripts/jquery-form.js +0 -869
- data/example/public/javascripts/jquery.js +0 -3383
- data/example/public/javascripts/languages/1c.js +0 -82
- data/example/public/javascripts/languages/axapta.js +0 -52
- data/example/public/javascripts/languages/bash.js +0 -80
- data/example/public/javascripts/languages/diff.js +0 -64
- data/example/public/javascripts/languages/dos.js +0 -33
- data/example/public/javascripts/languages/dynamic.js +0 -460
- data/example/public/javascripts/languages/ini.js +0 -36
- data/example/public/javascripts/languages/javascript.js +0 -38
- data/example/public/javascripts/languages/lisp.js +0 -86
- data/example/public/javascripts/languages/mel.js +0 -50
- data/example/public/javascripts/languages/profile.js +0 -50
- data/example/public/javascripts/languages/renderman.js +0 -71
- data/example/public/javascripts/languages/smalltalk.js +0 -53
- data/example/public/javascripts/languages/sql.js +0 -50
- data/example/public/javascripts/languages/static.js +0 -175
- data/example/public/javascripts/languages/vbscript.js +0 -25
- data/example/public/javascripts/languages/www.js +0 -245
- data/example/public/javascripts/prettyDate.js +0 -36
- data/example/public/javascripts/showdown.js +0 -421
- data/example/public/javascripts/template.js +0 -165
- data/example/public/javascripts/wmd-base.js +0 -1799
- data/example/public/javascripts/wmd-plus.js +0 -311
- data/example/public/javascripts/wmd.js +0 -73
- data/example/public/stylesheets/grid.css +0 -243
- data/example/public/stylesheets/grid.png +0 -0
- data/example/public/stylesheets/highlight/ascetic.css +0 -38
- data/example/public/stylesheets/highlight/dark.css +0 -96
- data/example/public/stylesheets/highlight/default.css +0 -91
- data/example/public/stylesheets/highlight/far.css +0 -95
- data/example/public/stylesheets/highlight/idea.css +0 -75
- data/example/public/stylesheets/highlight/sunburst.css +0 -112
- data/example/public/stylesheets/highlight/zenburn.css +0 -108
- data/example/public/stylesheets/print.css +0 -76
- data/example/public/stylesheets/reset.css +0 -45
- data/example/public/stylesheets/style.css +0 -141
- data/example/public/stylesheets/typography.css +0 -59
- data/example/templates/feed.rxml +0 -21
- data/example/templates/layout.rhtml +0 -54
- data/example/templates/page.rhtml +0 -4
- data/example/templates/post.rhtml +0 -57
- data/example/templates/posts.rhtml +0 -10
data/lib/shinmun/blog.rb
ADDED
@@ -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
|