shinmun 0.2 → 0.5

Sign up to get free protection for your applications and to get access to all the features.
data/assets/styles.css ADDED
@@ -0,0 +1,91 @@
1
+ html, body, div, span, object, iframe, h1, h2, h3, h4, h5, h6, p,
2
+ blockquote, pre, a, abbr, acronym, address, del, dfn, img,
3
+ q, fieldset, form, label, legend, table,
4
+ caption, tbody, tfoot, thead, tr, th, td {
5
+ margin: 0;
6
+ padding: 0;
7
+ border: 0;
8
+ font-weight: inherit;
9
+ font-style: inherit;
10
+ font-size: 100%;
11
+ font-family: inherit;
12
+ vertical-align: baseline;
13
+ }
14
+
15
+ body {
16
+ line-height: 1.5;
17
+ background: #fff;
18
+ margin:0.5em 0;
19
+ font-size: 80%;
20
+ color: #222;
21
+ font-family: Arial, sans-serif;
22
+ }
23
+
24
+ p {
25
+ margin-bottom: 1em;
26
+ }
27
+
28
+ a {
29
+ color: #444;
30
+ }
31
+
32
+ a:visited {
33
+ color: #444;
34
+ }
35
+
36
+ h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
37
+ text-decoration: none;
38
+ }
39
+
40
+ h1 { font-size: 2em; line-height: 1; margin-top: 1em; margin-bottom: 1em; }
41
+ h2 { font-size: 1.5em; margin-top: 0.75em; margin-bottom: 0.75em; }
42
+ h3 { font-size: 1.3em; line-height: 1; margin-top: 1.7em; margin-bottom: 1em; }
43
+ h4 { font-size: 1.2em; line-height: 1.25; margin-top: 1.25em; margin-bottom: 1.25em; }
44
+ h5 { font-size: 1em; font-weight: bold; margin-bottom: 1.5em; }
45
+ h6 { font-size: 1em; font-weight: bold; }
46
+
47
+ hr {
48
+ height: 1px;
49
+ color: #ccc;
50
+ }
51
+
52
+ .container {
53
+ width: 600px;
54
+ margin: 0 auto;
55
+ }
56
+
57
+ .article {
58
+ }
59
+
60
+ .article h2 {
61
+ margin-top:0px;
62
+ }
63
+
64
+ .article .date {
65
+ color: #666;
66
+ }
67
+
68
+ .article .tags a {
69
+ color: #69c;
70
+ text-decoration: none;
71
+ }
72
+
73
+ .comments {
74
+ margin-bottom: 2em;
75
+ }
76
+
77
+ .comments .comment, .preview .comment {
78
+ margin-top: 2em;
79
+ border: 1px solid #ccc;
80
+ }
81
+
82
+ .comments .comment .top, .preview .comment .top {
83
+ background: #F0F0F0;
84
+ color: #333;
85
+ padding: 3px 5px;
86
+ }
87
+
88
+ .comments .comment .body, .preview .comment .body {
89
+ background: #F8F8F8;
90
+ padding: 3px 5px;
91
+ }
data/bin/shinmun CHANGED
@@ -2,16 +2,33 @@
2
2
 
3
3
  require 'shinmun'
4
4
 
5
- blog = Shinmun::Blog.new
5
+ ENV['RACK_ENV'] = 'production'
6
6
 
7
7
  case ARGV[0]
8
- when 'new'
9
- blog.create_post(ARGV[1])
8
+ when 'init'
9
+ Shinmun::Blog.init ARGV[1]
10
10
 
11
- when 'render'
12
- blog.write_all
11
+ when 'post'
12
+ blog = Shinmun::Blog.new('.')
13
+ post = blog.create_post(:title => ARGV[1], :date => Date.today)
14
+ path = blog.post_file(post)
15
+
16
+ `git checkout master posts`
17
+
18
+ exec "#{ENV['EDITOR']} #{path}"
13
19
 
14
- when 'push'
15
- blog.push
20
+ when 'page'
21
+ blog = Shinmun::Blog.new('.')
22
+ post = blog.create_page(:title => ARGV[1])
23
+ path = blog.post_file(post)
16
24
 
25
+ `git checkout master pages`
26
+
27
+ exec "#{ENV['EDITOR']} #{path}"
28
+
29
+ else
30
+ puts "Usage:"
31
+ puts " shinmun init dir - creates a new blog"
32
+ puts " shinmun post 'Title of the post' - create a new post"
33
+ puts " shinmun page 'Title of the page' - create a new page"
17
34
  end
data/config.ru ADDED
@@ -0,0 +1,16 @@
1
+ require 'shinmun'
2
+
3
+ use Rack::Session::Cookie
4
+ use Rack::Reloader
5
+
6
+ blog = Shinmun::Blog.new(File.dirname(__FILE__))
7
+
8
+ blog.config = {
9
+ :language => 'en',
10
+ :title => "Blog Title",
11
+ :author => "The Author",
12
+ :categories => ["Ruby", "Javascript"],
13
+ :description => "Blog description"
14
+ }
15
+
16
+ run blog
data/lib/shinmun/blog.rb CHANGED
@@ -1,319 +1,181 @@
1
1
  module Shinmun
2
+ ROOT = File.expand_path(File.dirname(__FILE__) + '/../..')
2
3
 
3
- class Blog
4
+ class Blog < Kontrol::Application
5
+ include Helpers
4
6
 
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')
7
+ attr_accessor :config, :store, :posts, :pages
57
8
 
58
- @aggregations = {}
59
-
60
- load_aggregations
61
-
62
- Thread.start do
63
- sleep 300
64
- load_aggregations
65
- end
9
+ %w[ base_path title description language author categories ].each do |name|
10
+ define_method(name) { @config[name.to_sym] }
66
11
  end
67
12
 
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
13
+ # Initialize the blog
14
+ def initialize(path)
15
+ super
90
16
 
91
- pack_javascripts if @javascripts.dirty? or @javascripts.empty?
92
- pack_stylesheets if @stylesheets.dirty? or @stylesheets.empty?
93
-
94
- load 'templates/helpers.rb'
17
+ @config = {}
18
+ @store = GitStore.new(path)
19
+ @store.handler['md'] = PostHandler.new
20
+ @store.handler['rhtml'] = ERBHandler.new
21
+ @store.handler['rxml'] = ERBHandler.new
95
22
  end
96
23
 
97
- # Use rsync to synchronize the rendered blog to web server.
98
- def push
99
- system "rsync -avz public/ #{repository}"
100
- end
24
+ def self.init(path)
25
+ path = File.expand_path(path)
26
+ Dir.mkdir(path)
101
27
 
102
- def urlify(string)
103
- string.downcase.gsub(/[ -]+/, '-').gsub(/[^-a-z0-9_]+/, '')
104
- end
28
+ FileUtils.cp_r "#{ROOT}/assets", path
29
+ FileUtils.cp_r "#{ROOT}/templates", path
30
+ FileUtils.cp "#{ROOT}/config.ru", path
105
31
 
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
32
+ Dir.mkdir("#{path}/posts")
33
+ Dir.mkdir("#{path}/pages")
34
+ Dir.mkdir("#{path}/comments")
35
+ Dir.mkdir("#{path}/public")
125
36
 
126
- # Write a file to output directory.
127
- def write_file(path, data)
128
- FileUtils.mkdir_p("public/#{base_path}/#{File.dirname path}")
37
+ FileUtils.ln_s("../assets", "#{path}/public/assets")
129
38
 
130
- open("public/#{base_path}/#{path}", 'wb') do |file|
131
- file << data
132
- end
39
+ Dir.chdir(path) do
40
+ `git init`
41
+ `git add .`
42
+ `git commit -m 'init'`
43
+ end
133
44
  end
134
45
 
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 }
46
+ def load_template(file)
47
+ store['templates/' + file]
138
48
  end
139
49
 
140
- # Return all posts in given category.
141
- def posts_for_category(category)
142
- name = category['name']
143
- posts.select { |p| p.category == name }
50
+ def render(name, vars = {})
51
+ super(name, vars.merge(:blog => self))
144
52
  end
145
53
 
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
54
+ def pages
55
+ store.tree('pages').values.select { |v| Post === v }
155
56
  end
156
57
 
157
- # Return all months as tuples of [year, month].
158
- def months
159
- posts.map { |p| [p.year, p.month] }.uniq.sort
58
+ def posts
59
+ store.tree('posts').values.select { |v| Post === v }.sort_by { |p| p.date.to_s }.reverse
160
60
  end
161
61
 
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"
62
+ def call(env)
63
+ if ENV['RACK_ENV'] == 'production'
64
+ store.load if store.changed?
170
65
  else
171
- Post.new(:path => path,
172
- :title => title,
173
- :date => date).save
66
+ store.load(true)
174
67
  end
68
+
69
+ super
175
70
  end
176
71
 
177
- def find_page(path)
178
- pages.find { |p| p.path == path }
72
+ def url
73
+ "http://#{request.host}"
179
74
  end
180
75
 
181
- def find_post(path)
182
- posts.find { |p| p.path == path }
76
+ def symbolize_keys(hash)
77
+ hash.inject({}) do |h, (k, v)|
78
+ h[k.to_sym] = v
79
+ h
80
+ end
183
81
  end
184
82
 
185
- def find_category(category)
186
- category = urlify(category)
187
- categories.find { |c| urlify(c['name']) == category }
83
+ def transaction(message, &block)
84
+ store.transaction(message, &block)
188
85
  end
189
86
 
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
87
+ def post_file(post)
88
+ 'posts' + post_path(post) + '.' + post.type
195
89
  end
196
90
 
197
- def render_layout(vars)
198
- render_template("layout.rhtml", vars.merge(:blog => self))
91
+ def page_file(post)
92
+ 'pages' + page_path(post) + '.' + post.type
199
93
  end
200
94
 
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))))
95
+ def comment_file(post)
96
+ 'comments/' + post_path(post) + '.yml'
204
97
  end
205
98
 
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
99
+ def create_post(attr)
100
+ post = Post.new(attr)
101
+ path = post_file(post)
210
102
 
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
103
+ transaction "create post `#{post.title}'" do
104
+ store[path] = post
105
+ end
215
106
 
216
- # Render comments.
217
- def render_comments(comments)
218
- render_template('comments.rhtml', :comments => comments)
107
+ post
219
108
  end
220
109
 
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
110
+ def create_page(attr)
111
+ post = Post.new(attr)
112
+ path = page_file(post)
227
113
 
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
114
+ transaction "create page `#{post.title}'" do
115
+ store[path] = post
116
+ end
236
117
 
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)
118
+ post
247
119
  end
248
120
 
249
- # Render index feed using the feed template.
250
- def render_index_feed
251
- render_template("index.rxml",
252
- :blog => self,
253
- :posts => posts)
121
+ def comments_for(post)
122
+ store[comment_file post] || []
254
123
  end
255
124
 
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))
125
+ def create_comment(post, params)
126
+ path = comment_file(post)
127
+ comments = comments_for(post)
128
+ comment = Comment.new(params)
129
+
130
+ transaction "new comment for `#{post.title}'" do
131
+ store[path] = comments + [comment]
132
+ end
133
+
134
+ comment
262
135
  end
263
136
 
264
- def write_index_page
265
- write_file("index.html", render_index_page)
137
+ def find_page(name)
138
+ pages.find { |p| p.name == name }
266
139
  end
267
140
 
268
- # Write all pages.
269
- def write_pages
270
- for page in pages
271
- write_file("#{page.path}.html", render_page(page))
272
- end
141
+ def find_post(year, month, name)
142
+ posts.find { |p| p.year == year and p.month == month and p.name == name }
273
143
  end
274
144
 
275
- # Write all posts.
276
- def write_posts
277
- for post in posts
278
- write_file("#{post.path}.html", render_post(post))
279
- end
145
+ def find_category(permalink)
146
+ name = categories.find { |name| urlify(name) == permalink }
147
+
148
+ { :name => name,
149
+ :posts => posts.select { |p| p.category == name },
150
+ :permalink => permalink }
280
151
  end
281
152
 
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
153
+ def recent_posts
154
+ posts[0, 20]
287
155
  end
288
156
 
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
157
+ # Return all posts for a given month.
158
+ def posts_for_month(year, month)
159
+ posts.select { |p| p.year == year and p.month == month }
294
160
  end
295
161
 
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))
162
+ # Return all posts with any of given tags.
163
+ def posts_with_tags(tags)
164
+ return [] if tags.nil? or tags.empty?
165
+ tags = tags.split(',').map { |t| t.strip } if tags.is_a?(String)
166
+
167
+ posts.select do |post|
168
+ tags.any? do |tag|
169
+ post.tag_list.include?(tag)
170
+ end
301
171
  end
302
172
  end
303
173
 
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
174
+ # Return all archives as tuples of [year, month].
175
+ def archives
176
+ posts.map { |p| [p.year, p.month] }.uniq.sort
315
177
  end
316
178
 
317
179
  end
318
-
180
+
319
181
  end
@@ -0,0 +1,21 @@
1
+ class BlueCloth
2
+
3
+ def transform_code_blocks( str, rs )
4
+ @log.debug " Transforming code blocks"
5
+
6
+ str.gsub(CodeBlockRegexp) {|block|
7
+ codeblock = $1
8
+ remainder = $2
9
+
10
+ # Generate the codeblock
11
+ if codeblock =~ /^(?:[ ]{4}|\t)@@(.*?)\n\n(.*)\n\n/m
12
+ "\n\n<pre class='highlight'>%s</pre>\n\n%s" %
13
+ [CodeRay.scan(outdent($2), $1).html(:css => :style, :line_numbers => :list).delete("\n"), remainder]
14
+ else
15
+ "\n\n<pre><code>%s\n</code></pre>\n\n%s" %
16
+ [encode_code(outdent(codeblock), rs).rstrip, remainder]
17
+ end
18
+ }
19
+ end
20
+
21
+ end
@@ -1,42 +1,15 @@
1
1
  module Shinmun
2
2
 
3
3
  class Comment
4
-
4
+
5
5
  attr_accessor :time, :name, :email, :website, :text
6
6
 
7
7
  def initialize(attributes)
8
8
  for k, v in attributes
9
- send "#{k}=", v
9
+ send("#{k}=", v) if respond_to?("#{k}=")
10
10
  end
11
- end
12
-
13
- def self.read(path)
14
- file = "comments/#{path}"
15
- comments = []
16
11
 
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
12
+ self.time ||= Time.now
40
13
  end
41
14
 
42
15
  end
@@ -0,0 +1,19 @@
1
+ module Shinmun
2
+
3
+ class ERBHandler
4
+ def read(data)
5
+ ERB.new(data)
6
+ end
7
+ end
8
+
9
+ class PostHandler
10
+ def read(data)
11
+ Post.new(:src => data)
12
+ end
13
+
14
+ def write(post)
15
+ post.dump
16
+ end
17
+ end
18
+
19
+ end