shinmun 0.5.2 → 0.9
Sign up to get free protection for your applications and to get access to all the features.
- data/.gems +1 -0
- data/LICENSE +18 -0
- data/bin/shinmun +10 -13
- data/lib/shinmun/blog.rb +56 -109
- data/lib/shinmun/helpers.rb +12 -35
- data/lib/shinmun/post.rb +39 -36
- data/lib/shinmun/routes.rb +19 -30
- data/lib/shinmun.rb +2 -2
- data/{assets → public/stylesheets}/print.css +0 -0
- data/{assets → public/stylesheets}/styles.css +0 -0
- data/templates/archive.rhtml +1 -1
- data/templates/category.rhtml +1 -1
- data/templates/index.rhtml +1 -1
- data/templates/post.rhtml +1 -13
- data/test/blog_spec.rb +26 -54
- data/test/post_spec.rb +72 -9
- metadata +9 -21
- data/lib/shinmun/handlers.rb +0 -19
- data/templates/_comment_form.rhtml +0 -21
- data/templates/_comments.rhtml +0 -11
- data/templates/category.rxml +0 -20
data/.gems
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
shinmun --version 0.9
|
data/LICENSE
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Copyright (c) 2008 Matthias Georgi <http://www.matthias-georgi.de>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to
|
5
|
+
deal in the Software without restriction, including without limitation the
|
6
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
7
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
16
|
+
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/bin/shinmun
CHANGED
@@ -9,26 +9,23 @@ when 'init'
|
|
9
9
|
Shinmun::Blog.init ARGV[1]
|
10
10
|
|
11
11
|
when 'post'
|
12
|
-
|
13
|
-
post
|
14
|
-
path = blog.post_file(post)
|
15
|
-
|
16
|
-
`git checkout master posts`
|
17
|
-
|
18
|
-
exec "#{ENV['EDITOR']} #{path}"
|
12
|
+
post = Shinmun::Post.new(:title => ARGV[1], :date => Date.today)
|
13
|
+
post.save
|
19
14
|
|
20
|
-
|
21
|
-
blog = Shinmun::Blog.new('.')
|
22
|
-
post = blog.create_page(:title => ARGV[1])
|
23
|
-
path = blog.post_file(post)
|
15
|
+
puts "Created post '#{post.path}'"
|
24
16
|
|
25
|
-
|
17
|
+
when 'page'
|
18
|
+
post = Shinmun::Post.new(:title => ARGV[1])
|
19
|
+
post.save
|
26
20
|
|
27
|
-
|
21
|
+
puts "Created page '#{post.path}'"
|
28
22
|
|
29
23
|
else
|
30
24
|
puts "Usage:"
|
31
25
|
puts " shinmun init dir - creates a new blog"
|
32
26
|
puts " shinmun post 'Title of the post' - create a new post"
|
33
27
|
puts " shinmun page 'Title of the page' - create a new page"
|
28
|
+
exit
|
34
29
|
end
|
30
|
+
|
31
|
+
|
data/lib/shinmun/blog.rb
CHANGED
@@ -4,7 +4,8 @@ module Shinmun
|
|
4
4
|
class Blog < Kontrol::Application
|
5
5
|
include Helpers
|
6
6
|
|
7
|
-
attr_accessor :config
|
7
|
+
attr_accessor :config
|
8
|
+
attr_reader :posts, :pages, :posts_by_date, :posts_by_category, :posts_by_tag
|
8
9
|
|
9
10
|
%w[ base_path title description language author categories ].each do |name|
|
10
11
|
define_method(name) { @config[name.to_sym] }
|
@@ -15,163 +16,109 @@ module Shinmun
|
|
15
16
|
super
|
16
17
|
|
17
18
|
@config = {}
|
18
|
-
@
|
19
|
-
|
20
|
-
|
21
|
-
|
19
|
+
@templates = {}
|
20
|
+
|
21
|
+
load_posts
|
22
|
+
load_pages
|
23
|
+
sort_posts
|
22
24
|
end
|
23
25
|
|
24
26
|
def self.init(path)
|
25
27
|
path = File.expand_path(path)
|
26
28
|
Dir.mkdir(path)
|
27
29
|
|
28
|
-
FileUtils.cp_r "#{ROOT}/
|
30
|
+
FileUtils.cp_r "#{ROOT}/public", path
|
29
31
|
FileUtils.cp_r "#{ROOT}/templates", path
|
30
32
|
FileUtils.cp "#{ROOT}/config.ru", path
|
33
|
+
FileUtils.cp "#{ROOT}/.gems", path
|
31
34
|
|
32
35
|
Dir.mkdir("#{path}/posts")
|
33
36
|
Dir.mkdir("#{path}/pages")
|
34
|
-
Dir.mkdir("#{path}/comments")
|
35
|
-
Dir.mkdir("#{path}/public")
|
36
|
-
|
37
|
-
FileUtils.ln_s("../assets", "#{path}/public/assets")
|
38
|
-
|
39
|
-
Dir.chdir(path) do
|
40
|
-
`git init`
|
41
|
-
`git add .`
|
42
|
-
`git commit -m 'init'`
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def load_template(file)
|
47
|
-
store['templates/' + file]
|
48
37
|
end
|
49
38
|
|
50
39
|
def render(name, vars = {})
|
51
40
|
super(name, vars.merge(:blog => self))
|
52
41
|
end
|
53
42
|
|
54
|
-
def
|
55
|
-
|
56
|
-
|
43
|
+
def reload_changed_files
|
44
|
+
(@posts + @pages.values).each do |post|
|
45
|
+
if post.changed?
|
46
|
+
post.load
|
47
|
+
@changed = true
|
48
|
+
end
|
49
|
+
end
|
57
50
|
|
58
|
-
|
59
|
-
store.tree('posts').values.select { |v| Post === v }.sort_by { |p| p.date.to_s }.reverse
|
51
|
+
sort_posts if @changed
|
60
52
|
end
|
61
53
|
|
62
54
|
def call(env)
|
63
|
-
if ENV['RACK_ENV']
|
64
|
-
store.load if store.changed?
|
65
|
-
else
|
66
|
-
store.load(true)
|
67
|
-
end
|
68
|
-
|
55
|
+
reload_changed_files # if ENV['RACK_ENV'] != 'production'
|
69
56
|
super
|
70
57
|
end
|
71
58
|
|
72
|
-
def
|
73
|
-
|
74
|
-
end
|
59
|
+
def load_pages
|
60
|
+
@pages = {}
|
75
61
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
h
|
62
|
+
Dir["#{ path }/pages/*.md"].each do |file|
|
63
|
+
page = Post.new(:file => file)
|
64
|
+
@pages[page.name] = page
|
80
65
|
end
|
81
66
|
end
|
82
67
|
|
83
|
-
def
|
84
|
-
|
85
|
-
|
86
|
-
|
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'
|
97
|
-
end
|
98
|
-
|
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
|
68
|
+
def load_posts
|
69
|
+
@posts = Dir["#{ path }/posts/**/*.md"].map do |file|
|
70
|
+
Post.new(:file => file)
|
105
71
|
end
|
106
|
-
|
107
|
-
post
|
108
72
|
end
|
109
|
-
|
110
|
-
def
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
store[path] = post
|
73
|
+
|
74
|
+
def sort_posts
|
75
|
+
@posts = @posts.sort_by { |post| post.date.to_s }.reverse
|
76
|
+
|
77
|
+
@posts_by_category = Hash.new do |hash, category|
|
78
|
+
hash[category] = []
|
116
79
|
end
|
117
|
-
|
118
|
-
post
|
119
|
-
end
|
120
|
-
|
121
|
-
def comments_for(post)
|
122
|
-
store[comment_file post] || []
|
123
|
-
end
|
124
|
-
|
125
|
-
def create_comment(post, params)
|
126
|
-
path = comment_file(post)
|
127
|
-
comments = comments_for(post)
|
128
|
-
comment = Comment.new(params)
|
129
80
|
|
130
|
-
|
131
|
-
|
81
|
+
@posts_by_tag = Hash.new do |hash, tag|
|
82
|
+
hash[tag] = []
|
83
|
+
end
|
84
|
+
|
85
|
+
@posts_by_date = Hash.new do |hash, year|
|
86
|
+
hash[year] = Hash.new do |hash_months, month|
|
87
|
+
hash_months[month] = Hash.new
|
88
|
+
end
|
132
89
|
end
|
133
90
|
|
134
|
-
|
91
|
+
@posts.each do |post|
|
92
|
+
post.tag_list.each { |tag| @posts_by_tag[tag] << post }
|
93
|
+
@posts_by_category[post.category] << post if post.category
|
94
|
+
@posts_by_date[post.year][post.month][post.name] = post
|
95
|
+
end
|
135
96
|
end
|
136
97
|
|
137
|
-
def
|
138
|
-
|
98
|
+
def url
|
99
|
+
"http://#{request.host}"
|
139
100
|
end
|
140
101
|
|
141
|
-
def
|
142
|
-
|
102
|
+
def symbolize_keys(hash)
|
103
|
+
hash.inject({}) do |h, (k, v)|
|
104
|
+
h[k.to_sym] = v
|
105
|
+
h
|
106
|
+
end
|
143
107
|
end
|
144
108
|
|
145
109
|
def find_category(permalink)
|
146
|
-
|
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]
|
155
|
-
end
|
156
|
-
|
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 }
|
110
|
+
categories.find { |name| urlify(name) == permalink }
|
160
111
|
end
|
161
112
|
|
162
113
|
# Return all posts with any of given tags.
|
163
114
|
def posts_with_tags(tags)
|
164
115
|
return [] if tags.nil? or tags.empty?
|
165
116
|
tags = tags.split(',').map { |t| t.strip } if tags.is_a?(String)
|
166
|
-
|
167
|
-
|
168
|
-
tags.any? do |tag|
|
169
|
-
post.tag_list.include?(tag)
|
170
|
-
end
|
171
|
-
end
|
117
|
+
|
118
|
+
tags.map { |tag| posts_by_tag[tag] }.flatten.uniq
|
172
119
|
end
|
173
120
|
|
174
|
-
# Return all archives as tuples of [year, month].
|
121
|
+
# Return all archives as tuples of [year, month, posts].
|
175
122
|
def archives
|
176
123
|
posts.map { |p| [p.year, p.month] }.uniq.sort
|
177
124
|
end
|
data/lib/shinmun/helpers.rb
CHANGED
@@ -2,43 +2,14 @@ module Shinmun
|
|
2
2
|
|
3
3
|
module Helpers
|
4
4
|
|
5
|
-
|
6
|
-
def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false)
|
7
|
-
from_time = from_time.to_time if from_time.respond_to?(:to_time)
|
8
|
-
to_time = to_time.to_time if to_time.respond_to?(:to_time)
|
9
|
-
distance_in_minutes = (((to_time - from_time).abs)/60).round
|
10
|
-
distance_in_seconds = ((to_time - from_time).abs).round
|
11
|
-
|
12
|
-
case distance_in_minutes
|
13
|
-
when 0..1
|
14
|
-
return (distance_in_minutes == 0) ? 'less than a minute' : '1 minute' unless include_seconds
|
15
|
-
case distance_in_seconds
|
16
|
-
when 0..4 then 'less than 5 seconds'
|
17
|
-
when 5..9 then 'less than 10 seconds'
|
18
|
-
when 10..19 then 'less than 20 seconds'
|
19
|
-
when 20..39 then 'half a minute'
|
20
|
-
when 40..59 then 'less than a minute'
|
21
|
-
else '1 minute'
|
22
|
-
end
|
23
|
-
|
24
|
-
when 2..44 then "#{distance_in_minutes} minutes"
|
25
|
-
when 45..89 then 'about 1 hour'
|
26
|
-
when 90..1439 then "about #{(distance_in_minutes.to_f / 60.0).round} hours"
|
27
|
-
when 1440..2879 then '1 day'
|
28
|
-
when 2880..43199 then "#{(distance_in_minutes / 1440).round} days"
|
29
|
-
when 43200..86399 then 'about 1 month'
|
30
|
-
when 86400..525599 then "#{(distance_in_minutes / 43200).round} months"
|
31
|
-
when 525600..1051199 then 'about 1 year'
|
32
|
-
else "over #{(distance_in_minutes / 525600).round} years"
|
33
|
-
end
|
34
|
-
end
|
5
|
+
Kontrol::Template.send(:include, self)
|
35
6
|
|
36
7
|
def post_path(post)
|
37
|
-
"
|
8
|
+
"/#{post.year}/#{post.month}/#{post.name}"
|
38
9
|
end
|
39
10
|
|
40
11
|
def archive_path(year, month)
|
41
|
-
"
|
12
|
+
"/#{year}/#{month}"
|
42
13
|
end
|
43
14
|
|
44
15
|
# Render a link to a post
|
@@ -61,10 +32,16 @@ module Shinmun
|
|
61
32
|
time.strftime("%a, %d %b %Y %H:%M:%S %z")
|
62
33
|
end
|
63
34
|
|
64
|
-
# Render a link for the navigation bar.
|
65
|
-
# matches the @header variable, the css class will be set to acitve.
|
35
|
+
# Render a link for the navigation bar.
|
66
36
|
def navi_link(text, path)
|
67
|
-
|
37
|
+
if path.match(/categories\/(.*)/)
|
38
|
+
active = $1 == urlify(@category) if @category
|
39
|
+
active = $1 == urlify(@post.category) if @post
|
40
|
+
else
|
41
|
+
active = request.path_info == path
|
42
|
+
end
|
43
|
+
|
44
|
+
link_to text, path, :class => active ? 'active' : nil
|
68
45
|
end
|
69
46
|
|
70
47
|
def html_escape(s)
|
data/lib/shinmun/post.rb
CHANGED
@@ -19,25 +19,24 @@ module Shinmun
|
|
19
19
|
define_method("#{name}=") {|v| head[name] = v }
|
20
20
|
end
|
21
21
|
|
22
|
-
|
22
|
+
attr_writer :name
|
23
|
+
attr_accessor :src, :type, :head, :body, :file, :mtime
|
23
24
|
|
24
25
|
# Initialize empty post and set specified attributes.
|
25
26
|
def initialize(attributes={})
|
26
27
|
@head = {}
|
27
28
|
@body = ''
|
29
|
+
@type = 'md'
|
28
30
|
|
29
31
|
attributes.each do |k, v|
|
30
32
|
send "#{k}=", v
|
31
33
|
end
|
32
|
-
|
33
|
-
@type ||= 'md'
|
34
34
|
|
35
|
-
|
35
|
+
load if file
|
36
|
+
end
|
36
37
|
|
37
|
-
|
38
|
-
|
39
|
-
@name ||= title.downcase.gsub(/[ -]+/, '-').gsub(/[^-a-z0-9_]+/, '')
|
40
|
-
@dirname = date ? "posts/#{year}/#{month}" : 'pages'
|
38
|
+
def name
|
39
|
+
@name ||= title.to_s.downcase.gsub(/[ -]+/, '-').gsub(/[^-a-z0-9_]+/, '')
|
41
40
|
end
|
42
41
|
|
43
42
|
def method_missing(id, *args)
|
@@ -49,10 +48,6 @@ module Shinmun
|
|
49
48
|
end
|
50
49
|
end
|
51
50
|
|
52
|
-
def date=(date)
|
53
|
-
@head['date'] = String === date ? Date.parse(date) : date
|
54
|
-
end
|
55
|
-
|
56
51
|
# Shortcut for year of date
|
57
52
|
def year
|
58
53
|
date.year
|
@@ -63,22 +58,39 @@ module Shinmun
|
|
63
58
|
date.month
|
64
59
|
end
|
65
60
|
|
66
|
-
def
|
67
|
-
"
|
61
|
+
def tag_list
|
62
|
+
@tag_list ||= tags.to_s.split(",").map { |s| s.strip }
|
68
63
|
end
|
69
64
|
|
70
|
-
def
|
71
|
-
|
65
|
+
def body_html
|
66
|
+
@body_html ||= transform(@body)
|
67
|
+
end
|
68
|
+
|
69
|
+
def summary
|
70
|
+
@summary ||= body_html.split("\n\n")[0]
|
72
71
|
end
|
73
72
|
|
74
73
|
def path
|
75
|
-
|
74
|
+
folder = date ? "posts/#{year}/#{month}" : 'pages'
|
75
|
+
"#{folder}/#{name}.#{type}"
|
76
76
|
end
|
77
77
|
|
78
|
-
def
|
79
|
-
|
80
|
-
self.
|
81
|
-
self.
|
78
|
+
def load
|
79
|
+
self.type = File.extname(file)[1..-1]
|
80
|
+
self.name = File.basename(file).chomp(".#{type}")
|
81
|
+
self.mtime = File.mtime(file)
|
82
|
+
|
83
|
+
parse(File.read(file))
|
84
|
+
end
|
85
|
+
|
86
|
+
def changed?
|
87
|
+
File.mtime(file) != mtime
|
88
|
+
end
|
89
|
+
|
90
|
+
def save
|
91
|
+
File.open(path, 'w') do |io|
|
92
|
+
io << dump
|
93
|
+
end
|
82
94
|
end
|
83
95
|
|
84
96
|
# Split up the source into header and body. Load the header as
|
@@ -88,15 +100,12 @@ module Shinmun
|
|
88
100
|
@head = YAML.load($1)
|
89
101
|
@body = $2
|
90
102
|
else
|
91
|
-
|
103
|
+
@body = src
|
92
104
|
end
|
93
|
-
|
94
|
-
@body_html =
|
95
|
-
@
|
96
|
-
@
|
97
|
-
@dirname = date ? "posts/#{year}/#{month}" : 'pages'
|
98
|
-
|
99
|
-
self
|
105
|
+
|
106
|
+
@body_html = nil
|
107
|
+
@tag_list = nil
|
108
|
+
@summary = nil
|
100
109
|
end
|
101
110
|
|
102
111
|
# Convert to string representation
|
@@ -121,13 +130,7 @@ module Shinmun
|
|
121
130
|
end
|
122
131
|
|
123
132
|
def ==(obj)
|
124
|
-
|
125
|
-
if date
|
126
|
-
year == obj.year and month == obj.month and name == obj.name
|
127
|
-
else
|
128
|
-
name == obj.name
|
129
|
-
end
|
130
|
-
end
|
133
|
+
Post === obj and file == obj.file
|
131
134
|
end
|
132
135
|
|
133
136
|
end
|
data/lib/shinmun/routes.rb
CHANGED
@@ -1,39 +1,28 @@
|
|
1
1
|
Shinmun::Blog.map do
|
2
|
-
|
3
|
-
category_feed '/categories/(.*)\.rss' do |category|
|
4
|
-
render 'category.rxml', find_category(category).merge(:layout => false)
|
5
|
-
end
|
6
2
|
|
7
|
-
category '/categories/(.*)' do |
|
8
|
-
|
3
|
+
category '/categories/(.*)' do |name|
|
4
|
+
category = find_category(name)
|
5
|
+
|
6
|
+
render 'category.rhtml', :category => category, :posts => posts_by_category[category]
|
9
7
|
end
|
10
8
|
|
11
9
|
tag '/tags/(.*)' do |tag|
|
12
|
-
render 'category.rhtml', :name => "Tag: #{tag}", :posts =>
|
13
|
-
end
|
14
|
-
|
15
|
-
comments '/(\d+)/(\d+)/(.*)/comments' do |year, month, name|
|
16
|
-
post = find_post(year.to_i, month.to_i, name) or raise "post not found #{request.path_info}"
|
17
|
-
|
18
|
-
if params['preview']
|
19
|
-
comments = comments_for(post).push(Shinmun::Comment.new(params))
|
20
|
-
render 'post.rhtml', :post => post, :comments => comments
|
21
|
-
else
|
22
|
-
create_comment(post, params)
|
23
|
-
redirect post_path(post)
|
24
|
-
end
|
10
|
+
render 'category.rhtml', :name => "Tag: #{tag}", :posts => posts_by_tag[tag]
|
25
11
|
end
|
26
12
|
|
27
13
|
post '/(\d+)/(\d+)/(.*)' do |year, month, name|
|
28
|
-
if post =
|
29
|
-
render 'post.rhtml', :post => post
|
14
|
+
if post = posts_by_date[year.to_i][month.to_i][name]
|
15
|
+
render 'post.rhtml', :post => post
|
30
16
|
else
|
31
17
|
render '404.rhtml', :path => request.path_info
|
32
18
|
end
|
33
19
|
end
|
34
20
|
|
35
21
|
archive '/(\d+)/(\d+)' do |year, month|
|
36
|
-
render
|
22
|
+
render('archive.rhtml',
|
23
|
+
:year => year.to_i,
|
24
|
+
:month => month.to_i,
|
25
|
+
:posts => posts_by_date[year.to_i][month.to_i].values)
|
37
26
|
end
|
38
27
|
|
39
28
|
feed '/index\.rss' do
|
@@ -41,18 +30,18 @@ Shinmun::Blog.map do
|
|
41
30
|
end
|
42
31
|
|
43
32
|
index '/$' do
|
44
|
-
render 'index.rhtml'
|
33
|
+
render 'index.rhtml', :posts => posts[0, 20]
|
45
34
|
end
|
46
35
|
|
47
36
|
page '/(.*)' do |path|
|
48
37
|
path = path.gsub('..', '')
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
38
|
+
|
39
|
+
if page = pages[path]
|
40
|
+
render 'page.rhtml', :page => page
|
41
|
+
elsif File.exist?("public/#{path}")
|
42
|
+
file = Rack::File.new(nil)
|
43
|
+
file.path = "public/#{path}"
|
44
|
+
response.body = file
|
56
45
|
else
|
57
46
|
render '404.rhtml', :path => path
|
58
47
|
end
|
data/lib/shinmun.rb
CHANGED
@@ -1,16 +1,16 @@
|
|
1
|
+
$:.unshift '../../kontrol/lib'
|
2
|
+
|
1
3
|
require 'rubygems'
|
2
4
|
require 'fileutils'
|
3
5
|
require 'bluecloth'
|
4
6
|
require 'rubypants'
|
5
7
|
require 'coderay'
|
6
8
|
require 'kontrol'
|
7
|
-
require 'git_store'
|
8
9
|
|
9
10
|
begin; require 'redcloth'; rescue LoadError; end
|
10
11
|
|
11
12
|
require 'shinmun/bluecloth_coderay'
|
12
13
|
require 'shinmun/helpers'
|
13
|
-
require 'shinmun/handlers'
|
14
14
|
require 'shinmun/blog'
|
15
15
|
require 'shinmun/routes'
|
16
16
|
require 'shinmun/post'
|
File without changes
|
File without changes
|
data/templates/archive.rhtml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
<h1><%= "#{Date::MONTHNAMES[@month]} #{@year}" %></h1>
|
2
2
|
|
3
3
|
<div class="articles">
|
4
|
-
<% for post in @
|
4
|
+
<% for post in @posts %>
|
5
5
|
<div class="article">
|
6
6
|
<div class="date"><%= human_date post.date %></div>
|
7
7
|
<h2><%= post_link post %></h2>
|
data/templates/category.rhtml
CHANGED
data/templates/index.rhtml
CHANGED
data/templates/post.rhtml
CHANGED
@@ -17,17 +17,5 @@
|
|
17
17
|
Tagged with <%= @post.tag_list.map {|tag| link_to tag, "/tags/#{tag}" }.join(', ') %>.
|
18
18
|
<% end %>
|
19
19
|
</p>
|
20
|
-
|
21
|
-
<% if @comments %>
|
22
|
-
<h2>Comments</h2>
|
23
|
-
|
24
|
-
<div class="comments">
|
25
|
-
<%= render '_comments.rhtml', :comments => @comments %>
|
26
|
-
</div>
|
27
|
-
<% end %>
|
28
|
-
|
29
|
-
<h2>Leave a comment</h2>
|
30
|
-
|
31
|
-
<%= render '_comment_form.rhtml', :post => @post %>
|
32
|
-
|
20
|
+
|
33
21
|
</div>
|
data/test/blog_spec.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
$:.unshift "#{File.dirname __FILE__}/../lib"
|
2
|
+
|
1
3
|
require 'shinmun'
|
2
4
|
require 'rack/mock'
|
3
5
|
require 'rexml/document'
|
@@ -27,14 +29,19 @@ describe Shinmun::Blog do
|
|
27
29
|
:categories => ['Ruby', 'Javascript']
|
28
30
|
}
|
29
31
|
|
30
|
-
@posts = [
|
31
|
-
|
32
|
-
|
32
|
+
@posts = [Shinmun::Post.new(:title => 'New post', :date => Date.new(2008,10,10), :category => 'Ruby', :body => 'Body1'),
|
33
|
+
Shinmun::Post.new(:title => 'And this', :date => Date.new(2008,10,11), :category => 'Ruby', :body => 'Body2'),
|
34
|
+
Shinmun::Post.new(:title => 'Again', :date => Date.new(2008,11,10), :category => 'Javascript', :body => 'Body3')]
|
35
|
+
|
36
|
+
@pages = {
|
37
|
+
'page-1' => Shinmun::Post.new(:title => 'Page 1', :body => 'Body1'),
|
38
|
+
'page-2' => Shinmun::Post.new(:title => 'Page 2', :body => 'Body2')
|
39
|
+
}
|
33
40
|
|
34
|
-
|
35
|
-
|
41
|
+
blog.instance_variable_set('@posts', @posts)
|
42
|
+
blog.instance_variable_set('@pages', @pages)
|
36
43
|
|
37
|
-
blog.
|
44
|
+
blog.sort_posts
|
38
45
|
end
|
39
46
|
|
40
47
|
def request(method, uri, options={})
|
@@ -73,54 +80,29 @@ describe Shinmun::Blog do
|
|
73
80
|
end
|
74
81
|
|
75
82
|
it "should find posts for a category" do
|
76
|
-
|
77
|
-
category[:name].should == 'Ruby'
|
83
|
+
blog.find_category('ruby').should == 'Ruby'
|
78
84
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
category = blog.find_category('javascript')
|
83
|
-
category[:name].should == 'Javascript'
|
84
|
-
category[:posts].should include(@posts[2])
|
85
|
-
end
|
86
|
-
|
87
|
-
it "should create a post" do
|
88
|
-
post = blog.create_post(:title => 'New post', :date => '2008-10-10')
|
89
|
-
blog.store.load
|
85
|
+
blog.posts_by_category['Ruby'].should include(@posts[0])
|
86
|
+
blog.posts_by_category['Ruby'].should include(@posts[1])
|
90
87
|
|
91
|
-
|
92
|
-
|
93
|
-
post.title.should == 'New post'
|
94
|
-
post.date.should == Date.new(2008, 10, 10)
|
95
|
-
post.name.should == 'new-post'
|
88
|
+
blog.find_category('javascript').should == 'Javascript'
|
89
|
+
blog.posts_by_category['Javascript'].should include(@posts[2])
|
96
90
|
end
|
97
91
|
|
98
92
|
it "should render posts" do
|
99
93
|
xml = get('/2008/10/new-post').body
|
100
|
-
|
94
|
+
|
101
95
|
xpath(xml, "//h1")[0].text.should == 'New post'
|
102
96
|
xpath(xml, "//p")[0].text.should == 'Body1'
|
103
97
|
end
|
104
98
|
|
105
|
-
it "should render categories" do
|
106
|
-
get('/categories/ruby.rss')['Content-Type'].should == 'application/rss+xml'
|
107
|
-
|
108
|
-
xml = get('/categories/ruby.rss').body
|
109
|
-
|
110
|
-
xpath(xml, '/rss/channel/title')[0].text.should == 'Ruby'
|
111
|
-
xpath(xml, '/rss/channel/item/title')[0].text.should == 'And this'
|
112
|
-
xpath(xml, '/rss/channel/item/pubDate')[0].text.should == "Sat, 11 Oct 2008 00:00:00 +0000"
|
113
|
-
xpath(xml, '/rss/channel/item/link')[0].text.should == "http://example.org/2008/10/and-this"
|
114
|
-
xpath(xml, '/rss/channel/item/title')[1].text.should == 'New post'
|
115
|
-
xpath(xml, '/rss/channel/item/pubDate')[1].text.should == "Fri, 10 Oct 2008 00:00:00 +0000"
|
116
|
-
xpath(xml, '/rss/channel/item/link')[1].text.should == "http://example.org/2008/10/new-post"
|
117
|
-
|
99
|
+
it "should render categories" do
|
118
100
|
assert_listing(get('/categories/ruby').body, [['And this', 'Body2'], ['New post', 'Body1']])
|
119
101
|
end
|
120
102
|
|
121
103
|
it "should render index and archives" do
|
122
|
-
blog.
|
123
|
-
blog.
|
104
|
+
blog.posts_by_date[2008][10].should_not be_empty
|
105
|
+
blog.posts_by_date[2008][11].should_not be_empty
|
124
106
|
|
125
107
|
assert_listing(get('/2008/10').body, [['And this', 'Body2'], ['New post', 'Body1']])
|
126
108
|
assert_listing(get('/').body, [['Again', 'Body3'], ['And this', 'Body2'], ['New post', 'Body1']])
|
@@ -136,21 +118,11 @@ describe Shinmun::Blog do
|
|
136
118
|
xpath(xml, "//p")[0].text.should == 'Body2'
|
137
119
|
end
|
138
120
|
|
139
|
-
it "should
|
140
|
-
|
141
|
-
post "/2008/10/new-post/comments?name=Peter&text=Servus"
|
142
|
-
|
143
|
-
blog.store.load
|
144
|
-
|
145
|
-
comments = blog.comments_for(@posts[0])
|
146
|
-
|
147
|
-
comments[0].should_not be_nil
|
148
|
-
comments[0].name.should == 'Hans'
|
149
|
-
comments[0].text.should == 'Hallo'
|
121
|
+
it "should render a post" do
|
122
|
+
xml = get('/2008/10/new-post').body
|
150
123
|
|
151
|
-
|
152
|
-
|
153
|
-
comments[1].text.should == 'Servus'
|
124
|
+
xpath(xml, "//h1")[0].text.should == 'New post'
|
125
|
+
xpath(xml, "//div[@class='date']")[0].text.strip.should == 'October 10, 2008'
|
154
126
|
end
|
155
127
|
|
156
128
|
end
|
data/test/post_spec.rb
CHANGED
@@ -1,7 +1,15 @@
|
|
1
|
+
$:.unshift '../lib'
|
2
|
+
|
1
3
|
require 'shinmun'
|
2
4
|
|
3
5
|
describe Shinmun::Post do
|
4
6
|
|
7
|
+
BODY = "Patroon is a template engine written in Javascript in about 100 lines
|
8
|
+
of code. It takes existing DOM nodes annotated with CSS classes and
|
9
|
+
expand a data object according to simple rules. Additionally you may
|
10
|
+
use traditional string interpolation inside attribute values and text
|
11
|
+
nodes."
|
12
|
+
|
5
13
|
POST = <<-END
|
6
14
|
---
|
7
15
|
category: Javascript
|
@@ -9,24 +17,79 @@ date: 2008-09-09
|
|
9
17
|
tags: template, engine, json
|
10
18
|
title: Patroon - a Javascript Template Engine
|
11
19
|
---
|
12
|
-
|
13
|
-
of code. It takes existing DOM nodes annotated with CSS classes and
|
14
|
-
expand a data object according to simple rules. Additionally you may
|
15
|
-
use traditional string interpolation inside attribute values and text
|
16
|
-
nodes.
|
20
|
+
#{BODY}
|
17
21
|
END
|
18
22
|
|
19
|
-
it
|
20
|
-
Shinmun::Post.new(:
|
23
|
+
it "should infer the name from title" do
|
24
|
+
post = Shinmun::Post.new(:title => 'Patroon - a Javascript Template Engine')
|
25
|
+
post.name.should == 'patroon-a-javascript-template-engine'
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should return tags as list" do
|
29
|
+
post = Shinmun::Post.new
|
30
|
+
post.tags = 'a,b,c,d'
|
31
|
+
post.tag_list.should == ['a', 'b', 'c', 'd']
|
21
32
|
end
|
22
33
|
|
34
|
+
it "should infer path from date and name for posts" do
|
35
|
+
post = Shinmun::Post.new
|
36
|
+
post.name = 'post'
|
37
|
+
post.date = Date.new(2010,1,1)
|
38
|
+
post.path.should == 'posts/2010/1/post.md'
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should infer path from name for pages" do
|
42
|
+
post = Shinmun::Post.new
|
43
|
+
post.name = 'page'
|
44
|
+
post.path.should == 'pages/page.md'
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should set type, name and mtime upon loading a post" do
|
48
|
+
File.stub!(:mtime).and_return(Time.local(2010, 1, 1))
|
49
|
+
File.stub!(:read).and_return(POST)
|
50
|
+
|
51
|
+
post = Shinmun::Post.new(:file => 'posts/2010/01/a-post.md')
|
52
|
+
post.load
|
53
|
+
post.type.should == 'md'
|
54
|
+
post.name.should == 'a-post'
|
55
|
+
post.mtime.should == Time.local(2010, 1, 1)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should detect a changed file" do
|
59
|
+
File.should_receive(:mtime).with('file').and_return(Time.local(2009, 1, 1))
|
60
|
+
File.should_receive(:mtime).with('file').and_return(Time.local(2010, 1, 1))
|
61
|
+
|
62
|
+
post = Shinmun::Post.new
|
63
|
+
post.file = 'file'
|
64
|
+
post.mtime = Time.local(2009, 1, 1)
|
65
|
+
post.changed?.should be_false
|
66
|
+
post.changed?.should be_true
|
67
|
+
end
|
68
|
+
|
23
69
|
it "should parse the yaml header" do
|
24
|
-
post = Shinmun::Post.new(:type => 'md'
|
70
|
+
post = Shinmun::Post.new(:type => 'md')
|
71
|
+
post.parse(POST)
|
25
72
|
post.title.should == 'Patroon - a Javascript Template Engine'
|
26
73
|
post.category.should == 'Javascript'
|
27
74
|
post.date.should == Date.new(2008,9,9)
|
28
75
|
post.tags.should == 'template, engine, json'
|
29
|
-
post.
|
76
|
+
post.body.chop == BODY
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should not parse the yaml header if not present" do
|
80
|
+
post = Shinmun::Post.new
|
81
|
+
post.parse('just the body')
|
82
|
+
post.body.should == 'just the body'
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should transform the body" do
|
86
|
+
post = Shinmun::Post.new(:title => 'test', :body => '**bold**')
|
87
|
+
post.body_html.should == '<p><strong>bold</strong></p>'
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should transform according to type" do
|
91
|
+
post = Shinmun::Post.new(:title => 'test', :type => 'html', :body => '**bold**')
|
92
|
+
post.body_html.should_not == '<p><strong>bold</strong></p>'
|
30
93
|
end
|
31
94
|
|
32
95
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shinmun
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: "0.9"
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matthias Georgi
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2010-02-
|
12
|
+
date: 2010-02-08 00:00:00 +01:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -40,7 +40,7 @@ dependencies:
|
|
40
40
|
requirements:
|
41
41
|
- - ">="
|
42
42
|
- !ruby/object:Gem::Version
|
43
|
-
version: "0"
|
43
|
+
version: "1.0"
|
44
44
|
version:
|
45
45
|
- !ruby/object:Gem::Dependency
|
46
46
|
name: coderay
|
@@ -50,7 +50,7 @@ dependencies:
|
|
50
50
|
requirements:
|
51
51
|
- - ">="
|
52
52
|
- !ruby/object:Gem::Version
|
53
|
-
version:
|
53
|
+
version: 0.9.1
|
54
54
|
version:
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: kontrol
|
@@ -60,17 +60,7 @@ dependencies:
|
|
60
60
|
requirements:
|
61
61
|
- - ">="
|
62
62
|
- !ruby/object:Gem::Version
|
63
|
-
version:
|
64
|
-
version:
|
65
|
-
- !ruby/object:Gem::Dependency
|
66
|
-
name: git_store
|
67
|
-
type: :runtime
|
68
|
-
version_requirement:
|
69
|
-
version_requirements: !ruby/object:Gem::Requirement
|
70
|
-
requirements:
|
71
|
-
- - ">="
|
72
|
-
- !ruby/object:Gem::Version
|
73
|
-
version: "0"
|
63
|
+
version: 0.3.1
|
74
64
|
version:
|
75
65
|
description: git-based blog engine.
|
76
66
|
email: matti.georgi@gmail.com
|
@@ -81,29 +71,27 @@ extensions: []
|
|
81
71
|
extra_rdoc_files:
|
82
72
|
- README.md
|
83
73
|
files:
|
74
|
+
- .gems
|
84
75
|
- README.md
|
76
|
+
- LICENSE
|
85
77
|
- Rakefile
|
86
|
-
-
|
87
|
-
-
|
78
|
+
- public/stylesheets/print.css
|
79
|
+
- public/stylesheets/styles.css
|
88
80
|
- bin/shinmun
|
89
81
|
- config.ru
|
90
82
|
- lib/shinmun.rb
|
91
83
|
- lib/shinmun/blog.rb
|
92
84
|
- lib/shinmun/bluecloth_coderay.rb
|
93
85
|
- lib/shinmun/comment.rb
|
94
|
-
- lib/shinmun/handlers.rb
|
95
86
|
- lib/shinmun/helpers.rb
|
96
87
|
- lib/shinmun/post.rb
|
97
88
|
- lib/shinmun/routes.rb
|
98
89
|
- templates/index.rhtml
|
99
90
|
- templates/page.rhtml
|
100
91
|
- templates/404.rhtml
|
101
|
-
- templates/_comments.rhtml
|
102
92
|
- templates/category.rhtml
|
103
|
-
- templates/_comment_form.rhtml
|
104
93
|
- templates/post.rhtml
|
105
94
|
- templates/index.rxml
|
106
|
-
- templates/category.rxml
|
107
95
|
- templates/archive.rhtml
|
108
96
|
- templates/layout.rhtml
|
109
97
|
- test/blog_spec.rb
|
data/lib/shinmun/handlers.rb
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
<div class="comment-form">
|
2
|
-
<form action="<%= post_path @post %>/comments" method="POST">
|
3
|
-
<p>
|
4
|
-
<label>Name</label><br/>
|
5
|
-
<input type="text" name="name" size="40" value="<%= @params['name'] %>"/>
|
6
|
-
</p>
|
7
|
-
<p>
|
8
|
-
<label>Website</label><br/>
|
9
|
-
<input type="text" name="website" size="40" value="<%= @params['website'] %>"/>
|
10
|
-
</p>
|
11
|
-
<p>
|
12
|
-
<label>Comment</label><br/>
|
13
|
-
<textarea name="text" cols="60" rows="10"><%= @params['text'] %></textarea>
|
14
|
-
</p>
|
15
|
-
<p>
|
16
|
-
<input type="submit" value="Post comment"/>
|
17
|
-
<input type="checkbox" name="preview" value="1"/>
|
18
|
-
Preview
|
19
|
-
</p>
|
20
|
-
</form>
|
21
|
-
</div>
|
data/templates/_comments.rhtml
DELETED
@@ -1,11 +0,0 @@
|
|
1
|
-
<% for comment in @comments %>
|
2
|
-
<div class="comment">
|
3
|
-
<div class="top">
|
4
|
-
<%= comment.website.to_s.empty? ? comment.name : link_to(comment.name, comment.website) %> said
|
5
|
-
<%= distance_of_time_in_words(comment.time, Time.now, true) %> ago:
|
6
|
-
</div>
|
7
|
-
<div class="body">
|
8
|
-
<%= markdown(comment.text, :filter_html, :filter_styles) %>
|
9
|
-
</div>
|
10
|
-
</div>
|
11
|
-
<% end %>
|
data/templates/category.rxml
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
<?xml version="1.0" encoding="utf-8"?>
|
2
|
-
<rss version="2.0">
|
3
|
-
<channel>
|
4
|
-
<title><%= @name %></title>
|
5
|
-
<link><%= @blog.url %>/categories/<%= urlify @name %></link>
|
6
|
-
<language><%= @blog.language %></language>
|
7
|
-
<copyright><%= @blog.author %></copyright>
|
8
|
-
<pubDate><%= rfc822 Time.now %></pubDate>
|
9
|
-
<% for post in @posts[0, 20] %>
|
10
|
-
<item>
|
11
|
-
<title><%= post.title %></title>
|
12
|
-
<category><%= post.category %></category>
|
13
|
-
<description><%= strip_tags post.summary %></description>
|
14
|
-
<author><%= @author || @blog.author %></author>
|
15
|
-
<link><%= @blog.url %><%= post_path post %></link>
|
16
|
-
<pubDate><%= rfc822 post.date %></pubDate>
|
17
|
-
</item>
|
18
|
-
<% end %>
|
19
|
-
</channel>
|
20
|
-
</rss>
|