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