shinmun 0.5.2 → 0.9
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/.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>
|