georgi-shinmun 0.3
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/.gitignore +3 -0
- data/LICENSE +18 -0
- data/README.md +361 -0
- data/Rakefile +17 -0
- data/bin/shinmun +12 -0
- data/example/Rakefile +41 -0
- data/example/assets/images/favicon.ico +0 -0
- data/example/assets/images/loading.gif +0 -0
- data/example/assets/javascripts/coderay.js +13 -0
- data/example/assets/javascripts/comments.js +45 -0
- data/example/assets/javascripts/jquery-form.min.js +5 -0
- data/example/assets/javascripts/jquery.min.js +32 -0
- data/example/assets/stylesheets/article.css +15 -0
- data/example/assets/stylesheets/coderay.css +100 -0
- data/example/assets/stylesheets/comments.css +20 -0
- data/example/assets/stylesheets/form.css +12 -0
- data/example/assets/stylesheets/list.css +13 -0
- data/example/assets/stylesheets/print.css +76 -0
- data/example/assets/stylesheets/reset.css +23 -0
- data/example/assets/stylesheets/style.css +83 -0
- data/example/assets/stylesheets/table.css +24 -0
- data/example/assets/stylesheets/typo.css +40 -0
- data/example/assets/wmd/images/bg-fill.png +0 -0
- data/example/assets/wmd/images/bg.png +0 -0
- data/example/assets/wmd/images/blockquote.png +0 -0
- data/example/assets/wmd/images/bold.png +0 -0
- data/example/assets/wmd/images/code.png +0 -0
- data/example/assets/wmd/images/h1.png +0 -0
- data/example/assets/wmd/images/hr.png +0 -0
- data/example/assets/wmd/images/img.png +0 -0
- data/example/assets/wmd/images/italic.png +0 -0
- data/example/assets/wmd/images/link.png +0 -0
- data/example/assets/wmd/images/ol.png +0 -0
- data/example/assets/wmd/images/redo.png +0 -0
- data/example/assets/wmd/images/separator.png +0 -0
- data/example/assets/wmd/images/ul.png +0 -0
- data/example/assets/wmd/images/undo.png +0 -0
- data/example/assets/wmd/images/wmd-on.png +0 -0
- data/example/assets/wmd/images/wmd.png +0 -0
- data/example/assets/wmd/showdown.js +421 -0
- data/example/assets/wmd/wmd-base.js +1799 -0
- data/example/assets/wmd/wmd-plus.js +311 -0
- data/example/assets/wmd/wmd.js +73 -0
- data/example/config.ru +8 -0
- data/example/config/aggregations.yml +1 -0
- data/example/config/assets.yml +13 -0
- data/example/config/blog.yml +10 -0
- data/example/map.rb +100 -0
- data/example/pages/about.md +6 -0
- data/example/password +1 -0
- data/example/templates/_comment_form.rhtml +90 -0
- data/example/templates/_comments.rhtml +11 -0
- data/example/templates/_pagination.rhtml +10 -0
- data/example/templates/admin/commit.rhtml +27 -0
- data/example/templates/admin/commits.rhtml +9 -0
- data/example/templates/admin/edit.rhtml +17 -0
- data/example/templates/admin/pages.rhtml +19 -0
- data/example/templates/admin/posts.rhtml +24 -0
- data/example/templates/category.rhtml +12 -0
- data/example/templates/category.rxml +20 -0
- data/example/templates/index.rhtml +12 -0
- data/example/templates/index.rxml +21 -0
- data/example/templates/layout.rhtml +82 -0
- data/example/templates/page.rhtml +7 -0
- data/example/templates/post.rhtml +48 -0
- data/lib/shinmun.rb +21 -0
- data/lib/shinmun/aggregations/delicious.rb +57 -0
- data/lib/shinmun/aggregations/flickr.rb +81 -0
- data/lib/shinmun/blog.rb +165 -0
- data/lib/shinmun/bluecloth_coderay.rb +21 -0
- data/lib/shinmun/comment.rb +17 -0
- data/lib/shinmun/helpers.rb +64 -0
- data/lib/shinmun/post.rb +161 -0
- data/lib/shinmun/post_handler.rb +17 -0
- data/templates/_comments.rhtml +11 -0
- data/templates/archive.rhtml +6 -0
- data/templates/category.rhtml +6 -0
- data/templates/category.rxml +20 -0
- data/templates/index.rhtml +4 -0
- data/templates/index.rxml +21 -0
- data/templates/layout.rhtml +9 -0
- data/templates/page.rhtml +2 -0
- data/templates/post.rhtml +3 -0
- data/test/blog_spec.rb +177 -0
- data/test/map.rb +51 -0
- metadata +172 -0
data/lib/shinmun.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
|
|
4
|
+
require 'bluecloth'
|
|
5
|
+
require 'rubypants'
|
|
6
|
+
require 'coderay'
|
|
7
|
+
require 'packr'
|
|
8
|
+
require 'grit'
|
|
9
|
+
|
|
10
|
+
begin; require 'redcloth'; rescue LoadError; end
|
|
11
|
+
|
|
12
|
+
require 'kontrol'
|
|
13
|
+
require 'shinmun/bluecloth_coderay'
|
|
14
|
+
require 'shinmun/helpers'
|
|
15
|
+
require 'shinmun/blog'
|
|
16
|
+
require 'shinmun/post'
|
|
17
|
+
require 'shinmun/comment'
|
|
18
|
+
require 'shinmun/post_handler'
|
|
19
|
+
|
|
20
|
+
require 'shinmun/aggregations/delicious'
|
|
21
|
+
require 'shinmun/aggregations/flickr'
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require 'open-uri'
|
|
2
|
+
require 'time'
|
|
3
|
+
require 'rexml/document'
|
|
4
|
+
|
|
5
|
+
class Delicious
|
|
6
|
+
include REXML
|
|
7
|
+
|
|
8
|
+
attr_accessor :url, :items, :link, :title, :days
|
|
9
|
+
|
|
10
|
+
# This object holds given information of an item
|
|
11
|
+
class DeliciousItem < Struct.new(:link, :title, :description, :description_link, :date)
|
|
12
|
+
def to_s; title end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Pass the url to the RSS feed you would like to keep tabs on
|
|
16
|
+
# by default this will request the rss from the server right away and
|
|
17
|
+
# fill the items array
|
|
18
|
+
def initialize(url, refresh = true)
|
|
19
|
+
self.items = []
|
|
20
|
+
self.url = url
|
|
21
|
+
self.days = {}
|
|
22
|
+
self.refresh if refresh
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# This method lets you refresh the items in the items array
|
|
26
|
+
# useful if you keep the object cached in memory and
|
|
27
|
+
def refresh
|
|
28
|
+
open(@url) do |http|
|
|
29
|
+
parse(http.read)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def parse(body)
|
|
36
|
+
|
|
37
|
+
xml = Document.new(body)
|
|
38
|
+
|
|
39
|
+
self.items = []
|
|
40
|
+
self.link = XPath.match(xml, "//channel/link/text()").first.value rescue ""
|
|
41
|
+
self.title = XPath.match(xml, "//channel/title/text()").first.value rescue ""
|
|
42
|
+
|
|
43
|
+
XPath.each(xml, "//item/") do |elem|
|
|
44
|
+
item = DeliciousItem.new
|
|
45
|
+
item.title = XPath.match(elem, "title/text()").first.value rescue ""
|
|
46
|
+
item.link = XPath.match(elem, "link/text()").first.value rescue ""
|
|
47
|
+
item.description = XPath.match(elem, "description/text()").first.value rescue ""
|
|
48
|
+
item.date = Time.mktime(*ParseDate.parsedate(XPath.match(elem, "dc:date/text()").first.value)) rescue Time.now
|
|
49
|
+
|
|
50
|
+
item.description_link = item.description
|
|
51
|
+
item.description.gsub!(/<\/?a\b.*?>/, "") # remove all <a> tags
|
|
52
|
+
items << item
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
self.items = items.sort_by { |item| item.date }.reverse
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
require 'open-uri'
|
|
2
|
+
require 'time'
|
|
3
|
+
require 'rexml/document'
|
|
4
|
+
|
|
5
|
+
# Example:
|
|
6
|
+
#
|
|
7
|
+
# flickr = Flickr.new('http://www.flickr.com/services/feeds/photos_public.gne?id=40235412@N00&format=rss_200')
|
|
8
|
+
# flickr.pics.each do |pic|
|
|
9
|
+
# puts "#{pic.title} @ #{pic.link} updated at #{pic.date}"
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
class FlickrAggregation
|
|
13
|
+
include REXML
|
|
14
|
+
|
|
15
|
+
def choose(num)
|
|
16
|
+
return pics unless pics.size > num
|
|
17
|
+
bag = []
|
|
18
|
+
set = pics.dup
|
|
19
|
+
num.times {|x| bag << set.delete_at(rand(set.size))}
|
|
20
|
+
bag
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
attr_accessor :url, :pics, :link, :title, :description
|
|
24
|
+
|
|
25
|
+
# This object holds given information of a picture
|
|
26
|
+
class Picture
|
|
27
|
+
attr_accessor :link, :title, :date, :description, :thumbnail
|
|
28
|
+
|
|
29
|
+
def to_s
|
|
30
|
+
title
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def date=(value)
|
|
34
|
+
@date = Time.parse(value)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Pass the url to the RSS feed you would like to keep tabs on
|
|
40
|
+
# by default this will request the rss from the server right away and
|
|
41
|
+
# fill the tasks array
|
|
42
|
+
def initialize(url, refresh = true)
|
|
43
|
+
self.pics = []
|
|
44
|
+
self.url = url
|
|
45
|
+
self.refresh if refresh
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# This method lets you refresh the tasks int the tasks array
|
|
49
|
+
# useful if you keep the object cached in memory and
|
|
50
|
+
def refresh
|
|
51
|
+
open(@url) do |http|
|
|
52
|
+
parse(http.read)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def parse(body)
|
|
59
|
+
|
|
60
|
+
xml = Document.new(body)
|
|
61
|
+
|
|
62
|
+
self.pics = []
|
|
63
|
+
self.link = XPath.match(xml, "//channel/link/text()").to_s
|
|
64
|
+
self.title = XPath.match(xml, "//channel/title/text()").to_s
|
|
65
|
+
self.description = XPath.match(xml, "//channel/description/text()").to_s
|
|
66
|
+
|
|
67
|
+
XPath.each(xml, "//item/") do |elem|
|
|
68
|
+
|
|
69
|
+
picture = Picture.new
|
|
70
|
+
picture.title = XPath.match(elem, "title/text()").to_s
|
|
71
|
+
picture.date = XPath.match(elem, "pubDate/text()").to_s
|
|
72
|
+
picture.link = XPath.match(elem, "link/text()").to_s
|
|
73
|
+
picture.description = XPath.match(elem, "description/text()").to_s
|
|
74
|
+
picture.thumbnail = XPath.match(elem, "media:thumbnail/@url").to_s
|
|
75
|
+
|
|
76
|
+
pics << picture
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
|
data/lib/shinmun/blog.rb
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
module Shinmun
|
|
2
|
+
|
|
3
|
+
class Blog < Kontrol::Application
|
|
4
|
+
|
|
5
|
+
EXAMPLE_DIR = File.expand_path(File.dirname(__FILE__) + '/../../example')
|
|
6
|
+
|
|
7
|
+
include Helpers
|
|
8
|
+
|
|
9
|
+
attr_reader :posts, :pages, :aggregations, :categories, :comments
|
|
10
|
+
|
|
11
|
+
config_reader 'blog.yml', :title, :description, :language, :author, :url, :repository, :base_path, :categories
|
|
12
|
+
|
|
13
|
+
# Initialize the blog
|
|
14
|
+
def initialize(&block)
|
|
15
|
+
super
|
|
16
|
+
|
|
17
|
+
@aggregations = {}
|
|
18
|
+
Thread.start do
|
|
19
|
+
loop do
|
|
20
|
+
load_aggregations
|
|
21
|
+
sleep 300
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.init(name)
|
|
27
|
+
Dir.mkdir name
|
|
28
|
+
Dir.chdir name
|
|
29
|
+
FileUtils.cp_r EXAMPLE_DIR + '/.', '.'
|
|
30
|
+
`git init`
|
|
31
|
+
`git add .`
|
|
32
|
+
`git commit -m 'init'`
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def posts
|
|
36
|
+
store['posts'] ||= GitStore::Tree.new
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def pages
|
|
40
|
+
store['pages'] ||= GitStore::Tree.new
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def comments
|
|
44
|
+
store['comments'] ||= GitStore::Tree.new
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def load_aggregations
|
|
48
|
+
config['aggregations.yml'].to_a.each do |c|
|
|
49
|
+
aggregations[c['name']] = Object.const_get(c['class']).new(c['url'])
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def recent_posts
|
|
54
|
+
posts.sort_by { |post| post.date }.reverse[0, 20]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def posts_by_date
|
|
58
|
+
posts.sort_by { |post| post.date }.reverse
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Return all posts for a given month.
|
|
62
|
+
def posts_for_month(year, month)
|
|
63
|
+
posts.select { |p| p.year == year and p.month == month }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Return all posts with any of given tags.
|
|
67
|
+
def posts_with_tags(tags)
|
|
68
|
+
return [] if tags.nil? or tags.empty?
|
|
69
|
+
tags = tags.split(',').map { |t| t.strip } if tags.is_a?(String)
|
|
70
|
+
posts.select do |post|
|
|
71
|
+
tags.any? do |tag|
|
|
72
|
+
post.tag_list.include?(tag)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Return all archives as tuples of [year, month].
|
|
78
|
+
def archives
|
|
79
|
+
posts.map { |p| [p.year, p.month] }.uniq.sort
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def tree_for(post)
|
|
83
|
+
if post.date
|
|
84
|
+
(posts[post.year] ||= GitStore::Tree.new)[post.month] ||= GitStore::Tree.new
|
|
85
|
+
else
|
|
86
|
+
pages
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def symbolize_keys(hash)
|
|
91
|
+
hash.inject({}) do |h, (k, v)|
|
|
92
|
+
h[k.to_sym] = v
|
|
93
|
+
h
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def commit(message)
|
|
98
|
+
store.commit(message)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Create a new post with given title.
|
|
102
|
+
def create_post(atts = {})
|
|
103
|
+
atts = { :type => 'md' }.merge(symbolize_keys(atts))
|
|
104
|
+
atts[:name] = urlify(atts[:title]) or raise "no title given"
|
|
105
|
+
post = Post.new(atts)
|
|
106
|
+
tree_for(post)[post.filename] = post
|
|
107
|
+
commit "created `#{post.title}`"
|
|
108
|
+
tree_for(post)[post.filename]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def update_post(post, data)
|
|
112
|
+
tree_for(post).delete(post.filename)
|
|
113
|
+
post.parse data
|
|
114
|
+
tree_for(post)[post.filename] = post
|
|
115
|
+
commit "updated `#{post.title}`"
|
|
116
|
+
post
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def delete_post(post)
|
|
120
|
+
tree_for(post).delete(post.filename)
|
|
121
|
+
commit "deleted `#{post.title}`"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def comments_for(post)
|
|
125
|
+
comments["#{post.path}.yml"] ||= []
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def post_comment(post, params)
|
|
129
|
+
comments_for(post) << Comment.new(params)
|
|
130
|
+
commit "new comment for `#{post.title}`"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def find_page(name)
|
|
134
|
+
pages.find { |p| p.name == name }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def find_post(year, month, name)
|
|
138
|
+
tree = posts[year, month] and tree.find { |p| p.name == name }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def find_category(permalink)
|
|
142
|
+
name = categories.find { |name| urlify(name) == permalink } or raise "category not found"
|
|
143
|
+
posts = self.posts.select { |p| p.category == name }.sort_by { |p| p.date }.reverse
|
|
144
|
+
{ :name => name, :posts => posts, :permalink => permalink }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def find_by_path(path)
|
|
148
|
+
posts.find { |p| p.path == path } or pages.find { |p| p.path == path }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def write(file, template, vars={})
|
|
152
|
+
file = "public/#{base_path}/#{file}"
|
|
153
|
+
FileUtils.mkdir_p(File.dirname(file))
|
|
154
|
+
open(file, 'wb') do |io|
|
|
155
|
+
io << render(template, vars)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def render(name, vars = {})
|
|
160
|
+
super(name, vars.merge(:blog => self))
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class BlueCloth
|
|
2
|
+
|
|
3
|
+
def transform_code_blocks( str, rs )
|
|
4
|
+
@log.debug " Transforming code blocks"
|
|
5
|
+
|
|
6
|
+
str.gsub(CodeBlockRegexp) {|block|
|
|
7
|
+
codeblock = $1
|
|
8
|
+
remainder = $2
|
|
9
|
+
|
|
10
|
+
# Generate the codeblock
|
|
11
|
+
if codeblock =~ /^(?:[ ]{4}|\t)@@(.*?)\n\n(.*)\n\n/m
|
|
12
|
+
"\n\n<pre class='highlight'>%s</pre>\n\n%s" %
|
|
13
|
+
[CodeRay.scan(outdent($2), $1).html(:css => :class, :line_numbers => :list).delete("\n"), remainder]
|
|
14
|
+
else
|
|
15
|
+
"\n\n<pre><code>%s\n</code></pre>\n\n%s" %
|
|
16
|
+
[encode_code(outdent(codeblock), rs).rstrip, remainder]
|
|
17
|
+
end
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Shinmun
|
|
2
|
+
|
|
3
|
+
class Comment
|
|
4
|
+
|
|
5
|
+
attr_accessor :time, :name, :email, :website, :text
|
|
6
|
+
|
|
7
|
+
def initialize(attributes)
|
|
8
|
+
for k, v in attributes
|
|
9
|
+
send("#{k}=", v) if respond_to?("#{k}=")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
self.time ||= Time.now
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
module Shinmun
|
|
2
|
+
|
|
3
|
+
module Helpers
|
|
4
|
+
|
|
5
|
+
# taken form ActionView::Helpers
|
|
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
|
|
35
|
+
|
|
36
|
+
# Render a link to a post
|
|
37
|
+
def post_link(post)
|
|
38
|
+
link_to post.title, "#{base_path}/#{post.year}/#{post.month}/#{post.name}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Render a link to an archive page.
|
|
42
|
+
def archive_link(year, month)
|
|
43
|
+
link_to "#{Date::MONTHNAMES[month]} #{year}", "#{base_path}/#{year}/#{month}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Render a date or time in a nice human readable format.
|
|
47
|
+
def human_date(time)
|
|
48
|
+
"%s %d, %d" % [Date::MONTHNAMES[time.month], time.day, time.year]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Render a date or time in rfc822 format.
|
|
52
|
+
def rfc822(time)
|
|
53
|
+
time.strftime("%a, %d %b %Y %H:%M:%S %z")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def diff_line_class(line)
|
|
57
|
+
case line[0, 1]
|
|
58
|
+
when '+' then 'added'
|
|
59
|
+
when '-' then 'deleted'
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/shinmun/post.rb
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
module Shinmun
|
|
2
|
+
|
|
3
|
+
# This class represents a post or page.
|
|
4
|
+
# Each post has a header, encoded as YAML and a body.
|
|
5
|
+
#
|
|
6
|
+
# Example:
|
|
7
|
+
# ---
|
|
8
|
+
# category: Ruby
|
|
9
|
+
# date: 2008-09-05
|
|
10
|
+
#
|
|
11
|
+
# BlueCloth, a Markdown library
|
|
12
|
+
# =============================
|
|
13
|
+
#
|
|
14
|
+
# This is the summary, which is by definition the first paragraph of the
|
|
15
|
+
# article. The summary shows up in list views and rss feeds.
|
|
16
|
+
#
|
|
17
|
+
class Post
|
|
18
|
+
|
|
19
|
+
# Define accessor methods for head variable.
|
|
20
|
+
def self.head_accessor(*names)
|
|
21
|
+
names.each do |name|
|
|
22
|
+
name = name.to_s
|
|
23
|
+
define_method(name) { @head[name] }
|
|
24
|
+
define_method("#{name}=") {|v| @head[name] = v }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
attr_accessor :name, :type, :title, :src, :head, :body, :summary, :body_html, :tag_list
|
|
29
|
+
head_accessor :author, :date, :category, :tags, :languages, :header
|
|
30
|
+
|
|
31
|
+
# Initialize empty post and set specified attributes.
|
|
32
|
+
def initialize(attributes={})
|
|
33
|
+
@head = {}
|
|
34
|
+
@body = ''
|
|
35
|
+
|
|
36
|
+
for k, v in attributes
|
|
37
|
+
send "#{k}=", v
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
parse src if src
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def method_missing(id, *args)
|
|
44
|
+
key = id.to_s
|
|
45
|
+
if @head.has_key?(key)
|
|
46
|
+
@head[key]
|
|
47
|
+
else
|
|
48
|
+
raise NoMethodError, "undefined method `#{id}' for #{self}", caller(1)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def date=(d)
|
|
53
|
+
@head['date'] = String === d ? Date.parse(d) : d
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Shortcut for year of date
|
|
57
|
+
def year
|
|
58
|
+
date.year
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Shortcut for month of date
|
|
62
|
+
def month
|
|
63
|
+
date.month
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def filename
|
|
67
|
+
"#{name}.#{type}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def filename=(filename)
|
|
71
|
+
self.name, self.type = filename.split('.')
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def path
|
|
75
|
+
if date
|
|
76
|
+
"#{year}/#{month}/#{name}"
|
|
77
|
+
else
|
|
78
|
+
name
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Split up the source into header and body. Load the header as
|
|
83
|
+
# yaml document. Render body and parse the summary from rendered html.
|
|
84
|
+
def parse(src)
|
|
85
|
+
src = src.delete("\r")
|
|
86
|
+
|
|
87
|
+
# Parse YAML header if present
|
|
88
|
+
if src =~ /\A(---.*?)\n\n(.*)/m
|
|
89
|
+
@head = YAML.load($1)
|
|
90
|
+
@body = $2
|
|
91
|
+
else
|
|
92
|
+
@body = src
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
@title = head['title'] or parse_title
|
|
96
|
+
@body_html = transform(body)
|
|
97
|
+
@summary = body_html.split("\n\n")[0]
|
|
98
|
+
@tag_list = tags.to_s.split(",").map { |s| s.strip }
|
|
99
|
+
|
|
100
|
+
self
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Parse title from different formats
|
|
104
|
+
def parse_title
|
|
105
|
+
lines = body.split("\n")
|
|
106
|
+
|
|
107
|
+
return if lines.empty?
|
|
108
|
+
|
|
109
|
+
case type
|
|
110
|
+
when 'md'
|
|
111
|
+
@title = lines.shift.sub(/(^#+|#+$)/,'').strip
|
|
112
|
+
lines.shift if lines.first.match(/^(=|-)+$/)
|
|
113
|
+
|
|
114
|
+
when 'html'
|
|
115
|
+
@title = lines.shift.sub(/(<h1>|\<\/h1>)/,'').strip
|
|
116
|
+
|
|
117
|
+
when 'tt'
|
|
118
|
+
@title = lines.shift.sub(/(^h1.)/,'').strip
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
@body = lines.join("\n")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Convert to string representation
|
|
125
|
+
def dump
|
|
126
|
+
str = head.empty? ? '' : head.to_yaml + "\n"
|
|
127
|
+
unless head['title']
|
|
128
|
+
str << \
|
|
129
|
+
case type
|
|
130
|
+
when 'md' : "#{title}\n#{'=' * title.size}\n"
|
|
131
|
+
when 'html' : "<h1>#{title}</h1>\n"
|
|
132
|
+
when 'tt' : "h1.#{title}\n"
|
|
133
|
+
else raise
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
str + body
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Transform the body of this post. Defaults to Markdown.
|
|
140
|
+
def transform(src)
|
|
141
|
+
case type
|
|
142
|
+
when 'html'
|
|
143
|
+
RubyPants.new(src).to_html
|
|
144
|
+
when 'tt'
|
|
145
|
+
RubyPants.new(RedCloth.new(src).to_html).to_html
|
|
146
|
+
else
|
|
147
|
+
RubyPants.new(BlueCloth.new(src).to_html).to_html
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def eql?(obj)
|
|
152
|
+
path == obj.path
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def ==(obj)
|
|
156
|
+
path == obj.path
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
end
|