georgi-shinmun 0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. data/.gitignore +3 -0
  2. data/LICENSE +18 -0
  3. data/README.md +361 -0
  4. data/Rakefile +17 -0
  5. data/bin/shinmun +12 -0
  6. data/example/Rakefile +41 -0
  7. data/example/assets/images/favicon.ico +0 -0
  8. data/example/assets/images/loading.gif +0 -0
  9. data/example/assets/javascripts/coderay.js +13 -0
  10. data/example/assets/javascripts/comments.js +45 -0
  11. data/example/assets/javascripts/jquery-form.min.js +5 -0
  12. data/example/assets/javascripts/jquery.min.js +32 -0
  13. data/example/assets/stylesheets/article.css +15 -0
  14. data/example/assets/stylesheets/coderay.css +100 -0
  15. data/example/assets/stylesheets/comments.css +20 -0
  16. data/example/assets/stylesheets/form.css +12 -0
  17. data/example/assets/stylesheets/list.css +13 -0
  18. data/example/assets/stylesheets/print.css +76 -0
  19. data/example/assets/stylesheets/reset.css +23 -0
  20. data/example/assets/stylesheets/style.css +83 -0
  21. data/example/assets/stylesheets/table.css +24 -0
  22. data/example/assets/stylesheets/typo.css +40 -0
  23. data/example/assets/wmd/images/bg-fill.png +0 -0
  24. data/example/assets/wmd/images/bg.png +0 -0
  25. data/example/assets/wmd/images/blockquote.png +0 -0
  26. data/example/assets/wmd/images/bold.png +0 -0
  27. data/example/assets/wmd/images/code.png +0 -0
  28. data/example/assets/wmd/images/h1.png +0 -0
  29. data/example/assets/wmd/images/hr.png +0 -0
  30. data/example/assets/wmd/images/img.png +0 -0
  31. data/example/assets/wmd/images/italic.png +0 -0
  32. data/example/assets/wmd/images/link.png +0 -0
  33. data/example/assets/wmd/images/ol.png +0 -0
  34. data/example/assets/wmd/images/redo.png +0 -0
  35. data/example/assets/wmd/images/separator.png +0 -0
  36. data/example/assets/wmd/images/ul.png +0 -0
  37. data/example/assets/wmd/images/undo.png +0 -0
  38. data/example/assets/wmd/images/wmd-on.png +0 -0
  39. data/example/assets/wmd/images/wmd.png +0 -0
  40. data/example/assets/wmd/showdown.js +421 -0
  41. data/example/assets/wmd/wmd-base.js +1799 -0
  42. data/example/assets/wmd/wmd-plus.js +311 -0
  43. data/example/assets/wmd/wmd.js +73 -0
  44. data/example/config.ru +8 -0
  45. data/example/config/aggregations.yml +1 -0
  46. data/example/config/assets.yml +13 -0
  47. data/example/config/blog.yml +10 -0
  48. data/example/map.rb +100 -0
  49. data/example/pages/about.md +6 -0
  50. data/example/password +1 -0
  51. data/example/templates/_comment_form.rhtml +90 -0
  52. data/example/templates/_comments.rhtml +11 -0
  53. data/example/templates/_pagination.rhtml +10 -0
  54. data/example/templates/admin/commit.rhtml +27 -0
  55. data/example/templates/admin/commits.rhtml +9 -0
  56. data/example/templates/admin/edit.rhtml +17 -0
  57. data/example/templates/admin/pages.rhtml +19 -0
  58. data/example/templates/admin/posts.rhtml +24 -0
  59. data/example/templates/category.rhtml +12 -0
  60. data/example/templates/category.rxml +20 -0
  61. data/example/templates/index.rhtml +12 -0
  62. data/example/templates/index.rxml +21 -0
  63. data/example/templates/layout.rhtml +82 -0
  64. data/example/templates/page.rhtml +7 -0
  65. data/example/templates/post.rhtml +48 -0
  66. data/lib/shinmun.rb +21 -0
  67. data/lib/shinmun/aggregations/delicious.rb +57 -0
  68. data/lib/shinmun/aggregations/flickr.rb +81 -0
  69. data/lib/shinmun/blog.rb +165 -0
  70. data/lib/shinmun/bluecloth_coderay.rb +21 -0
  71. data/lib/shinmun/comment.rb +17 -0
  72. data/lib/shinmun/helpers.rb +64 -0
  73. data/lib/shinmun/post.rb +161 -0
  74. data/lib/shinmun/post_handler.rb +17 -0
  75. data/templates/_comments.rhtml +11 -0
  76. data/templates/archive.rhtml +6 -0
  77. data/templates/category.rhtml +6 -0
  78. data/templates/category.rxml +20 -0
  79. data/templates/index.rhtml +4 -0
  80. data/templates/index.rxml +21 -0
  81. data/templates/layout.rhtml +9 -0
  82. data/templates/page.rhtml +2 -0
  83. data/templates/post.rhtml +3 -0
  84. data/test/blog_spec.rb +177 -0
  85. data/test/map.rb +51 -0
  86. 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
+
@@ -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
@@ -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