georgi-shinmun 0.4.1 → 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.
Files changed (58) hide show
  1. data/README.md +23 -31
  2. data/assets/print.css +76 -0
  3. data/assets/styles.css +91 -0
  4. data/bin/shinmun +16 -8
  5. data/config.ru +16 -0
  6. data/lib/shinmun.rb +1 -4
  7. data/lib/shinmun/blog.rb +110 -100
  8. data/lib/shinmun/comment.rb +1 -1
  9. data/lib/shinmun/handlers.rb +19 -0
  10. data/lib/shinmun/helpers.rb +1 -1
  11. data/lib/shinmun/routes.rb +25 -36
  12. data/templates/404.rhtml +4 -0
  13. data/templates/_comment_form.rhtml +21 -0
  14. data/{test/templates → templates}/_comments.rhtml +0 -0
  15. data/{example/templates → templates}/archive.rhtml +0 -1
  16. data/{example/templates → templates}/category.rhtml +1 -2
  17. data/{example/templates → templates}/category.rxml +0 -0
  18. data/{example/templates → templates}/index.rhtml +0 -1
  19. data/{example/templates → templates}/index.rxml +0 -0
  20. data/templates/layout.rhtml +44 -0
  21. data/{example/templates → templates}/page.rhtml +0 -2
  22. data/{example/templates → templates}/post.rhtml +6 -21
  23. data/test/blog_spec.rb +48 -81
  24. metadata +17 -44
  25. data/example/Rakefile +0 -41
  26. data/example/assets/images/favicon.ico +0 -0
  27. data/example/assets/images/loading.gif +0 -0
  28. data/example/assets/javascripts/1-jquery.min.js +0 -32
  29. data/example/assets/javascripts/2-jquery-form.min.js +0 -5
  30. data/example/assets/javascripts/3-comments.js +0 -45
  31. data/example/assets/javascripts/4-coderay.js +0 -13
  32. data/example/assets/print.css +0 -76
  33. data/example/assets/stylesheets/1-reset.css +0 -23
  34. data/example/assets/stylesheets/2-typo.css +0 -40
  35. data/example/assets/stylesheets/3-table.css +0 -23
  36. data/example/assets/stylesheets/4-article.css +0 -15
  37. data/example/assets/stylesheets/5-comments.css +0 -20
  38. data/example/assets/stylesheets/6-diff.css +0 -25
  39. data/example/assets/stylesheets/7-blog.css +0 -33
  40. data/example/config.ru +0 -6
  41. data/example/config/blog.yml +0 -10
  42. data/example/pages/about.md +0 -7
  43. data/example/templates/_comment_form.rhtml +0 -90
  44. data/example/templates/_comments.rhtml +0 -11
  45. data/example/templates/_pagination.rhtml +0 -10
  46. data/example/templates/comments.rhtml +0 -1
  47. data/example/templates/layout.rhtml +0 -82
  48. data/lib/shinmun/aggregations/delicious.rb +0 -57
  49. data/lib/shinmun/aggregations/flickr.rb +0 -81
  50. data/lib/shinmun/post_handler.rb +0 -16
  51. data/test/templates/archive.rhtml +0 -6
  52. data/test/templates/category.rhtml +0 -6
  53. data/test/templates/category.rxml +0 -20
  54. data/test/templates/index.rhtml +0 -4
  55. data/test/templates/index.rxml +0 -21
  56. data/test/templates/layout.rhtml +0 -9
  57. data/test/templates/page.rhtml +0 -2
  58. data/test/templates/post.rhtml +0 -3
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  Shinmun - a git-based blog engine
2
- ==========================================
2
+ =================================
3
3
 
4
4
  Shinmun is a small git-based blog engine. Write posts in your favorite
5
5
  editor, git-push it and serve your blog straight from a repository.
@@ -11,7 +11,6 @@ editor, git-push it and serve your blog straight from a repository.
11
11
  * Deploy via [git-push][11]
12
12
  * Index, category and archive listings
13
13
  * RSS feeds
14
- * Flickr and Delicious aggregations
15
14
  * Syntax highlighting provided by [CodeRay][4]
16
15
  * AJAX comment system with Markdown preview
17
16
 
@@ -21,9 +20,9 @@ editor, git-push it and serve your blog straight from a repository.
21
20
  Install the gems:
22
21
 
23
22
  $ gem sources -a http://gems.github.com
24
- $ gem install rack BlueCloth rubypants coderay mojombo-grit georgi-git_store georgi-kontrol georgi-shinmun
23
+ $ gem install rack BlueCloth rubypants coderay georgi-git_store georgi-kontrol georgi-shinmun
25
24
 
26
- Create a sample blog (this step requires the git executable):
25
+ Create a sample blog:
27
26
 
28
27
  $ shinmun init myblog
29
28
 
@@ -48,9 +47,7 @@ folder:
48
47
  shinmun post 'The title of the post'
49
48
 
50
49
  Shinmun will then create a post file in the right place, for example
51
- in `posts/2008/9/the-title-of-the-post.md`. After creating you will
52
- probably open the file, set the category and tags and start writing
53
- your new article.
50
+ in `posts/2008/9/the-title-of-the-post.md` and open it with $EDITOR.
54
51
 
55
52
 
56
53
  ### Post Format
@@ -141,23 +138,15 @@ An example tree:
141
138
 
142
139
  ### Blog configuation
143
140
 
144
- Inside `config/blog.yml` you set the properties of your blog:
141
+ Inside `config.ru` you can set the properties of your blog:
145
142
 
146
- * title: the title of your blog, used inside templates
147
- * description: used for RSS
148
- * language: used for RSS
149
- * author: used for RSS
150
- * url: used for RSS
151
- * categories: a list of categories
152
-
153
-
154
- ### Assets
155
-
156
- Shinmun serves asset files from your assets directory. Files in the
157
- directories `assets/stylesheets` and `assets/javascripts` will be
158
- served as one file each under the URLs `assets/stylesheets.css` and
159
- `assets/javascripts.css`. You have to name them accordingly like
160
- `1-reset.css` and `2-typo.css` to define the order.
143
+ blog.config = {
144
+ :language => 'en',
145
+ :title => "Blog Title",
146
+ :author => "The Author",
147
+ :categories => ["Ruby", "Javascript"],
148
+ :description => "Blog description"
149
+ }
161
150
 
162
151
 
163
152
  ### Templates
@@ -176,17 +165,20 @@ Layout and templates are rendered by *ERB*. The layout is defined in
176
165
  </body>
177
166
  </html>
178
167
 
179
- The attributes of a post are accessible as instance variables in a
180
- template:
168
+ The attributes of a post are accessible via the @post variable:
181
169
 
182
- <div class="article">
170
+ <div class="article">
171
+
172
+ <h1><%= @post.title %></h1>
173
+
183
174
  <div class="date">
184
- <%= date @date %>
175
+ <%= human_date @post.date %>
185
176
  </div>
186
- <h2><%= @title %></h2>
187
- <%= @body %>
188
- <h3>Comments</h3>
189
- <!-- comment form -->
177
+
178
+ <%= @post.body_html %>
179
+
180
+ ...
181
+
190
182
  </div>
191
183
 
192
184
 
data/assets/print.css ADDED
@@ -0,0 +1,76 @@
1
+ body {
2
+ line-height:1.5;
3
+ font-family:"Helvetica Neue", "Lucida Grande", Arial, Verdana, sans-serif;
4
+ color:#000;
5
+ background:none;
6
+ font-size:10pt;
7
+ }
8
+
9
+ .container {
10
+ background:none;
11
+ }
12
+
13
+ h1,h2,h3,h4,h5,h6 {
14
+ font-family:"Helvetica Neue", Arial, "Lucida Grande", sans-serif;
15
+ }
16
+
17
+ code {
18
+ font:.9em "Courier New", Monaco, Courier, monospace;
19
+ }
20
+
21
+ img {
22
+ float:left;
23
+ margin:1.5em 1.5em 1.5em 0;
24
+ }
25
+
26
+ a img {
27
+ border:none;
28
+ }
29
+
30
+ p img.top {
31
+ margin-top:0;
32
+ }
33
+
34
+ hr {
35
+ background:#ccc;
36
+ color:#ccc;
37
+ width:100%;
38
+ height:2px;
39
+ border:none;
40
+ margin:2em 0;
41
+ padding:0;
42
+ }
43
+
44
+ blockquote {
45
+ font-style:italic;
46
+ font-size:.9em;
47
+ margin:1.5em;
48
+ padding:1em;
49
+ }
50
+
51
+ .small {
52
+ font-size:.9em;
53
+ }
54
+
55
+ .large {
56
+ font-size:1.1em;
57
+ }
58
+
59
+ .quiet {
60
+ color:#999;
61
+ }
62
+
63
+ .hide {
64
+ display:none;
65
+ }
66
+
67
+ a:link,a:visited {
68
+ background:transparent;
69
+ font-weight:700;
70
+ text-decoration:underline;
71
+ }
72
+
73
+ a:link:after,a:visited:after {
74
+ content:" (" attr(href) ") ";
75
+ font-size:90%;
76
+ }
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,21 +2,29 @@
2
2
 
3
3
  require 'shinmun'
4
4
 
5
+ ENV['RACK_ENV'] = 'production'
6
+
5
7
  case ARGV[0]
6
8
  when 'init'
7
9
  Shinmun::Blog.init ARGV[1]
8
10
 
9
11
  when 'post'
10
- post = Shinmun::Post.new(:title => ARGV[1], :date => Date.today)
11
- FileUtils.mkpath(File.dirname(post.path))
12
- open(post.path, 'w') { |io| io.write post.dump }
13
- puts "created #{post.path}"
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}"
14
19
 
15
20
  when 'page'
16
- post = Shinmun::Post.new(:title => ARGV[1])
17
- FileUtils.mkpath(File.dirname(post.path))
18
- open(post.path, 'w') { |io| io.write post.dump }
19
- puts "created #{post.path}"
21
+ blog = Shinmun::Blog.new('.')
22
+ post = blog.create_page(:title => ARGV[1])
23
+ path = blog.post_file(post)
24
+
25
+ `git checkout master pages`
26
+
27
+ exec "#{ENV['EDITOR']} #{path}"
20
28
 
21
29
  else
22
30
  puts "Usage:"
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.rb CHANGED
@@ -10,11 +10,8 @@ begin; require 'redcloth'; rescue LoadError; end
10
10
 
11
11
  require 'shinmun/bluecloth_coderay'
12
12
  require 'shinmun/helpers'
13
+ require 'shinmun/handlers'
13
14
  require 'shinmun/blog'
14
15
  require 'shinmun/routes'
15
16
  require 'shinmun/post'
16
17
  require 'shinmun/comment'
17
- require 'shinmun/post_handler'
18
-
19
- require 'shinmun/aggregations/delicious'
20
- require 'shinmun/aggregations/flickr'
data/lib/shinmun/blog.rb CHANGED
@@ -1,104 +1,76 @@
1
1
  module Shinmun
2
+ ROOT = File.expand_path(File.dirname(__FILE__) + '/../..')
2
3
 
3
4
  class Blog < Kontrol::Application
4
-
5
- EXAMPLE_DIR = File.expand_path(File.dirname(__FILE__) + '/../../example')
6
-
7
5
  include Helpers
8
6
 
9
- attr_reader :aggregations, :categories, :comments, :repo, :store
10
-
11
- %w[ assets comments config posts pages templates ].each do |name|
12
- define_method(name) { store.root.tree(name) }
13
- end
7
+ attr_accessor :config, :store, :posts, :pages
14
8
 
15
- %w[ title description language author url base_path categories ].each do |name|
16
- define_method(name) { config['blog.yml'][name] }
9
+ %w[ base_path title description language author categories ].each do |name|
10
+ define_method(name) { @config[name.to_sym] }
17
11
  end
18
12
 
19
13
  # Initialize the blog
20
14
  def initialize(path)
21
15
  super
22
16
 
23
- @aggregations = {}
24
-
25
- if ENV['RACK_ENV'] == 'production'
26
- @store = GitStore.new(path)
27
- else
28
- @store = GitStore::FileStore.new(path)
29
- end
30
-
31
- @store.load
32
-
33
- @repo = Grit::Repo.new(path) if defined?(Grit)
34
-
35
- Thread.start do
36
- loop do
37
- load_aggregations
38
- sleep 300
39
- end
40
- end
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
41
22
  end
42
23
 
43
- def self.init(name)
44
- Dir.mkdir name
45
- Dir.chdir name
46
- FileUtils.cp_r EXAMPLE_DIR + '/.', '.'
47
- `git init`
48
- `git add .`
49
- `git commit -m 'init'`
50
- end
24
+ def self.init(path)
25
+ path = File.expand_path(path)
26
+ Dir.mkdir(path)
51
27
 
52
- def load_template(file)
53
- templates[file] or raise "template #{file} not found"
54
- end
28
+ FileUtils.cp_r "#{ROOT}/assets", path
29
+ FileUtils.cp_r "#{ROOT}/templates", path
30
+ FileUtils.cp "#{ROOT}/config.ru", path
55
31
 
56
- def render(name, vars = {})
57
- super(name, vars.merge(:blog => self))
58
- end
32
+ Dir.mkdir("#{path}/posts")
33
+ Dir.mkdir("#{path}/pages")
34
+ Dir.mkdir("#{path}/comments")
35
+ Dir.mkdir("#{path}/public")
59
36
 
60
- def call(env)
61
- store.refresh!
62
- super
63
- end
37
+ FileUtils.ln_s("../assets", "#{path}/public/assets")
64
38
 
65
- def load_aggregations
66
- config['aggregations.yml'].to_a.each do |c|
67
- aggregations[c['name']] = Object.const_get(c['class']).new(c['url'])
39
+ Dir.chdir(path) do
40
+ `git init`
41
+ `git add .`
42
+ `git commit -m 'init'`
68
43
  end
69
44
  end
70
45
 
71
- def posts_by_date
72
- posts.sort_by { |post| post.date.to_s }.reverse
46
+ def load_template(file)
47
+ store['templates/' + file]
73
48
  end
74
49
 
75
- def recent_posts
76
- posts_by_date[0, 20]
50
+ def render(name, vars = {})
51
+ super(name, vars.merge(:blog => self))
77
52
  end
78
53
 
79
- # Return all posts for a given month.
80
- def posts_for_month(year, month)
81
- posts_by_date.select { |p| p.year == year and p.month == month }
54
+ def pages
55
+ store.tree('pages').values
82
56
  end
83
57
 
84
- # Return all posts with any of given tags.
85
- def posts_with_tags(tags)
86
- return [] if tags.nil? or tags.empty?
87
- tags = tags.split(',').map { |t| t.strip } if tags.is_a?(String)
88
- posts.select do |post|
89
- tags.any? do |tag|
90
- post.tag_list.include?(tag)
91
- end
92
- end
58
+ def posts
59
+ store.tree('posts').values.sort_by { |post| post.date.to_s }.reverse
93
60
  end
94
61
 
95
- # Return all archives as tuples of [year, month].
96
- def archives
97
- posts.map { |p| [p.year, p.month] }.uniq.sort
62
+ def call(env)
63
+ if ENV['RACK_ENV'] == 'production'
64
+ store.load if store.changed?
65
+ else
66
+ store.load(true)
67
+ end
68
+
69
+ super
98
70
  end
99
71
 
100
- def tree(post)
101
- post.date ? posts.tree(post.year).tree(post.month) : pages
72
+ def url
73
+ "http://#{request.host}"
102
74
  end
103
75
 
104
76
  def symbolize_keys(hash)
@@ -112,36 +84,54 @@ module Shinmun
112
84
  store.transaction(message, &block)
113
85
  end
114
86
 
115
- # Create a new post with given attributes.
116
- def create_post(atts)
117
- post = Post.new(atts)
118
- transaction "create '#{post.title}'" do
119
- store[post.path] = post
120
- end
87
+ def post_file(post)
88
+ 'posts' + post_path(post) + '.' + post.type
89
+ end
90
+
91
+ def page_file(post)
92
+ 'pages' + page_path(post) + '.' + post.type
93
+ end
94
+
95
+ def comment_file(post)
96
+ 'comments/' + post_path(post) + '.yml'
121
97
  end
122
98
 
123
- def update_post(post, data)
124
- transaction "update '#{post.title}'" do
125
- store.delete(post.path)
126
- post.parse data
127
- store[post.path] = post
99
+ def create_post(attr)
100
+ post = Post.new(attr)
101
+ path = post_file(post)
102
+
103
+ transaction "create post `#{post.title}'" do
104
+ store[path] = post
128
105
  end
106
+
107
+ post
129
108
  end
130
109
 
131
- def delete_post(post)
132
- transaction "delete '#{post.title}'" do
133
- store.delete(post.path)
110
+ def create_page(attr)
111
+ post = Post.new(attr)
112
+ path = page_file(post)
113
+
114
+ transaction "create page `#{post.title}'" do
115
+ store[path] = post
134
116
  end
117
+
118
+ post
135
119
  end
136
120
 
137
- def comments_for(path)
138
- comments[path + '.yml'] || []
121
+ def comments_for(post)
122
+ store[comment_file post] || []
139
123
  end
140
124
 
141
- def post_comment(path, params)
142
- transaction "new comment for '#{path}'" do
143
- comments[path + '.yml'] = comments[path + '.yml'].to_a + [ Comment.new(params) ]
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]
144
132
  end
133
+
134
+ comment
145
135
  end
146
136
 
147
137
  def find_page(name)
@@ -149,23 +139,43 @@ module Shinmun
149
139
  end
150
140
 
151
141
  def find_post(year, month, name)
152
- tree = posts[year, month] and tree.find { |p| p.name == name }
142
+ posts.find { |p| p.year == year and p.month == month and p.name == name }
153
143
  end
154
144
 
155
145
  def find_category(permalink)
156
- name = categories.find { |name| urlify(name) == permalink } or raise "category not found"
157
- posts = self.posts.select { |p| p.category == name }.sort_by { |p| p.date }.reverse
158
- { :name => name, :posts => posts, :permalink => permalink }
146
+ name = categories.find { |name| urlify(name) == permalink }
147
+
148
+ { :name => name,
149
+ :posts => posts.select { |p| p.category == name },
150
+ :permalink => permalink }
151
+ end
152
+
153
+ def recent_posts
154
+ posts[0, 20]
159
155
  end
160
156
 
161
- def write(file, template, vars={})
162
- file = "public/#{base_path}/#{file}"
163
- FileUtils.mkdir_p(File.dirname(file))
164
- open(file, 'wb') do |io|
165
- io << render(template, vars)
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 }
160
+ end
161
+
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
166
171
  end
167
172
  end
168
-
169
- end
173
+
174
+ # Return all archives as tuples of [year, month].
175
+ def archives
176
+ posts.map { |p| [p.year, p.month] }.uniq.sort
177
+ end
178
+
179
+ end
170
180
 
171
181
  end