shinmun 0.2 → 0.5

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/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