georgi-shinmun 0.4.1 → 0.5

Sign up to get free protection for your applications and to get access to all the features.
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