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.
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