shinmun 0.2 → 0.5

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.
@@ -2,85 +2,6 @@ module Shinmun
2
2
 
3
3
  module Helpers
4
4
 
5
- # Render a hash as attributes for a HTML tag.
6
- def attributes(attributes)
7
- attributes.map { |k, v| %Q{#{k}="#{v}"} }.join(' ')
8
- end
9
-
10
- # Render a HTML tag with given name.
11
- # The last argument specifies the attributes of the tag.
12
- # The second argument may be the content of the tag.
13
- def tag(name, *args)
14
- text, attributes = args.first.is_a?(Hash) ? [nil, args.first] : args
15
- "<#{name} #{attributes(attributes)}>#{text}</#{name}>"
16
- end
17
-
18
- # Render stylesheet link tag
19
- def stylesheet_link_tag(*names)
20
- options = names.last.is_a?(Hash) ? names.pop : {}
21
- options[:media] ||= 'screen'
22
- names.map { |name|
23
- mtime = File.mtime("assets/#{blog.stylesheets_path}/#{name}.css").to_i
24
- path = "/#{blog.stylesheets_path}/#{name}.css?#{mtime}"
25
- tag :link, :href => path, :rel => 'stylesheet', :media => options[:media]
26
- }.join("\n")
27
- end
28
-
29
- # Render javascript tag
30
- def javascript_tag(*names)
31
- names.map { |name|
32
- mtime = File.mtime("assets/#{blog.javascripts_path}/#{name}.js").to_i
33
- path = "/#{blog.javascripts_path}/#{name}.js?#{mtime}"
34
- tag :script, :src => path, :type => 'text/javascript'
35
- }.join("\n")
36
- end
37
-
38
- # Render an image tag
39
- def image_tag(file, options = {})
40
- mtime = File.mtime("assets/#{blog.images_path}/#{file}").to_i
41
- path = "/#{blog.images_path}/#{file}?#{mtime}"
42
- tag :img, options.merge(:src => path)
43
- end
44
-
45
- # Render a link
46
- def link_to(text, path, options = {})
47
- tag :a, text, options.merge(:href => path)
48
- end
49
-
50
- # Render a link to a post
51
- def post_link(post)
52
- link_to post.title, "#{blog.base_path}/#{post.path}.html"
53
- end
54
-
55
- # Render a link to an archive page.
56
- def archive_link(year, month)
57
- link_to "#{Date::MONTHNAMES[month]} #{year}", "#{blog.base_path}/#{year}/#{month}/index.html"
58
- end
59
-
60
- # Render a date or time in a nice human readable format.
61
- def date(time)
62
- "%s %d, %d" % [Date::MONTHNAMES[time.month], time.day, time.year]
63
- end
64
-
65
- # Render a date or time in rfc822 format. This will be used for rss rendering.
66
- def rfc822(time)
67
- time.strftime("%a, %d %b %Y %H:%M:%S %z")
68
- end
69
-
70
- def markdown(text, *args)
71
- BlueCloth.new(text, *args).to_html
72
- rescue => e
73
- "#{text}<br/><br/><strong style='color:red'>#{e.message}</strong>"
74
- end
75
-
76
- def strip_tags(str)
77
- str.gsub(/<\/?[^>]*>/, "")
78
- end
79
-
80
- def urlify(string)
81
- string.downcase.gsub(/[ -]+/, '-').gsub(/[^-a-z0-9_]+/, '')
82
- end
83
-
84
5
  # taken form ActionView::Helpers
85
6
  def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false)
86
7
  from_time = from_time.to_time if from_time.respond_to?(:to_time)
@@ -111,6 +32,51 @@ module Shinmun
111
32
  else "over #{(distance_in_minutes / 525600).round} years"
112
33
  end
113
34
  end
35
+
36
+ def post_path(post)
37
+ "#{base_path}/#{post.year}/#{post.month}/#{post.name}"
38
+ end
39
+
40
+ def archive_path(year, month)
41
+ "#{base_path}/#{year}/#{month}"
42
+ end
43
+
44
+ # Render a link to a post
45
+ def post_link(post)
46
+ link_to post.title, post_path(post)
47
+ end
48
+
49
+ # Render a link to an archive page.
50
+ def archive_link(year, month)
51
+ link_to "#{Date::MONTHNAMES[month]} #{year}", archive_path(year, month)
52
+ end
53
+
54
+ # Render a date or time in a nice human readable format.
55
+ def human_date(time)
56
+ "%s %d, %d" % [Date::MONTHNAMES[time.month], time.day, time.year]
57
+ end
58
+
59
+ # Render a date or time in rfc822 format.
60
+ def rfc822(time)
61
+ time.strftime("%a, %d %b %Y %H:%M:%S %z")
62
+ end
63
+
64
+ # Render a link for the navigation bar. If the text of the link
65
+ # matches the @header variable, the css class will be set to acitve.
66
+ def navi_link(text, path)
67
+ link_to text, path, :class => (request.path_info == path) ? 'active' : nil
68
+ end
69
+
70
+ def html_escape(s)
71
+ s.to_s.gsub(/>/, '&gt;').gsub(/</n, '&lt;')
72
+ end
73
+
74
+ def diff_line_class(line)
75
+ case line[0, 1]
76
+ when '+' then 'added'
77
+ when '-' then 'deleted'
78
+ end
79
+ end
80
+
114
81
  end
115
-
116
82
  end
data/lib/shinmun/post.rb CHANGED
@@ -7,35 +7,50 @@ module Shinmun
7
7
  # ---
8
8
  # category: Ruby
9
9
  # date: 2008-09-05
10
- #
11
- # BlueCloth, a Markdown library
12
- # =============================
13
- #
10
+ # title: BlueCloth, a Markdown library
11
+ # ---
14
12
  # This is the summary, which is by definition the first paragraph of the
15
13
  # article. The summary shows up in list views and rss feeds.
16
14
  #
17
15
  class Post
18
16
 
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
17
+ %w[title author date category tags].each do |name|
18
+ define_method(name) { head[name] }
19
+ define_method("#{name}=") {|v| head[name] = v }
26
20
  end
27
21
 
28
- attr_accessor :prefix, :path, :type, :title, :head, :body, :summary, :body_html
29
- head_accessor :author, :date, :category, :tags, :languages, :header
22
+ attr_accessor :dirname, :name, :type, :src, :head, :body, :summary, :body_html, :tag_list
30
23
 
31
24
  # Initialize empty post and set specified attributes.
32
25
  def initialize(attributes={})
33
26
  @head = {}
34
27
  @body = ''
35
28
 
36
- for k, v in attributes
29
+ attributes.each do |k, v|
37
30
  send "#{k}=", v
38
31
  end
32
+
33
+ @type ||= 'md'
34
+
35
+ parse(src) if src
36
+
37
+ raise "post without a title" if title.nil?
38
+
39
+ @name ||= title.downcase.gsub(/[ -]+/, '-').gsub(/[^-a-z0-9_]+/, '')
40
+ @dirname = date ? "posts/#{year}/#{month}" : 'pages'
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=(date)
53
+ @head['date'] = String === date ? Date.parse(date) : date
39
54
  end
40
55
 
41
56
  # Shortcut for year of date
@@ -46,111 +61,51 @@ module Shinmun
46
61
  # Shortcut for month of date
47
62
  def month
48
63
  date.month
49
- end
64
+ end
50
65
 
51
66
  def filename
52
- "#{prefix}/#{path}.#{type}"
67
+ "#{name}.#{type}"
53
68
  end
54
69
 
55
- # Strips off extension and prefix.
56
- def filename=(file)
57
- if match = file.match(/^(.*?)\/(.*)\.(.*)/)
58
- @prefix = match[1]
59
- @path = match[2]
60
- @type = match[3]
61
- else
62
- raise "incorrect filename: #{file}"
63
- end
70
+ def filename=(filename)
71
+ self.name, self.type = filename.split('.')
72
+ end
73
+
74
+ def path
75
+ dirname.to_s.empty? ? filename : "#{dirname}/#{filename}"
76
+ end
77
+
78
+ def path=(path)
79
+ list = path.split('/')
80
+ self.dirname = list[0..-2].join('/')
81
+ self.filename = list[-1]
64
82
  end
65
83
 
66
84
  # Split up the source into header and body. Load the header as
67
- # yaml document. Render body and parse the summary from rendered html.
85
+ # yaml document.
68
86
  def parse(src)
69
- # Parse YAML header if present
70
- if src =~ /---.*?\n(.*?)\n\n(.*)/m
87
+ if src =~ /\A---(.*?)---(.*)/m
71
88
  @head = YAML.load($1)
72
89
  @body = $2
73
90
  else
74
- @body = src
91
+ raise ArgumentError, "yaml header not found in src"
75
92
  end
76
93
 
77
- @title = head['title'] or parse_title
78
- @body_html = transform(body, type)
94
+ @body_html = transform(@body)
79
95
  @summary = body_html.split("\n\n")[0]
96
+ @tag_list = tags.to_s.split(",").map { |s| s.strip }
97
+ @dirname = date ? "posts/#{year}/#{month}" : 'pages'
80
98
 
81
99
  self
82
100
  end
83
101
 
84
- # Parse title from different formats
85
- def parse_title
86
- lines = body.split("\n")
87
-
88
- case type
89
- when 'md'
90
- @title = lines.shift.sub(/(^#+|#+$)/,'').strip
91
- lines.shift if lines.first.match(/^(=|-)+$/)
92
-
93
- when 'html'
94
- @title = lines.shift.sub(/(<h1>|\<\/h1>)/,'').strip
95
-
96
- when 'tt'
97
- @title = lines.shift.sub(/(^h1.)/,'').strip
98
- end
99
-
100
- @body = lines.join("\n")
101
- end
102
-
103
- # Convert to yaml for caching.
104
- def to_yaml
105
- head.merge('author' => author,
106
- 'path' => path,
107
- 'type' => type,
108
- 'title' => title,
109
- 'summary' => summary,
110
- 'body_html' => body_html).to_yaml
111
- end
112
-
113
- # Convert to string representation, used to create new posts.
102
+ # Convert to string representation
114
103
  def dump
115
- head = self.head.dup
116
- body = self.body.dup
117
-
118
- if type == 'md'
119
- body = title + "\n" + ("=" * title.size) + "\n\n" + body
120
- end
121
-
122
- head.each do |k, v|
123
- head.delete(k) if v.nil? || v.empty?
124
- end
125
-
126
- if head.empty?
127
- body
128
- else
129
- head.to_yaml + "\n\n" + body
130
- end
104
+ head.to_yaml + "---" + body
131
105
  end
132
106
 
133
- def load
134
- parse(File.read(filename))
135
- end
136
-
137
- def save
138
- FileUtils.mkdir_p(File.dirname(filename))
139
- File.open(filename, "w") { |io| io << dump }
140
- self
141
- end
142
-
143
- # Variables used for templates.
144
- def variables
145
- head.merge(:author => author,
146
- :path => path,
147
- :title => title,
148
- :body => body_html)
149
- end
150
-
151
- # Transform the body of this post according to given type.
152
- # Defaults to Markdown.
153
- def transform(src, type)
107
+ # Transform the body of this post. Defaults to Markdown.
108
+ def transform(src)
154
109
  case type
155
110
  when 'html'
156
111
  RubyPants.new(src).to_html
@@ -161,6 +116,20 @@ module Shinmun
161
116
  end
162
117
  end
163
118
 
119
+ def eql?(obj)
120
+ self == obj
121
+ end
122
+
123
+ def ==(obj)
124
+ if Post === obj
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
131
+ end
132
+
164
133
  end
165
134
 
166
135
  end
@@ -0,0 +1,61 @@
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
+
7
+ category '/categories/(.*)' do |category|
8
+ render 'category.rhtml', find_category(category)
9
+ end
10
+
11
+ tag '/tags/(.*)' do |tag|
12
+ render 'category.rhtml', :name => "Tag: #{tag}", :posts => posts_with_tags(tag)
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
25
+ end
26
+
27
+ post '/(\d+)/(\d+)/(.*)' do |year, month, name|
28
+ if post = find_post(year.to_i, month.to_i, name)
29
+ render 'post.rhtml', :post => post, :comments => comments_for(post)
30
+ else
31
+ render '404.rhtml', :path => request.path_info
32
+ end
33
+ end
34
+
35
+ archive '/(\d+)/(\d+)' do |year, month|
36
+ render 'archive.rhtml', :year => year.to_i, :month => month.to_i, :posts => posts_for_month(year.to_i, month.to_i)
37
+ end
38
+
39
+ feed '/index\.rss' do
40
+ render 'index.rxml', :layout => false
41
+ end
42
+
43
+ index '/$' do
44
+ render 'index.rhtml'
45
+ end
46
+
47
+ page '/(.*)' do |path|
48
+ path = path.gsub('..', '')
49
+ page = find_page(path)
50
+
51
+ if page
52
+ render 'page.rhtml', :page => page
53
+
54
+ elsif file = store[path]
55
+ text file
56
+ else
57
+ render '404.rhtml', :path => path
58
+ end
59
+ end
60
+
61
+ end
data/lib/shinmun.rb CHANGED
@@ -1,23 +1,17 @@
1
1
  require 'rubygems'
2
2
  require 'fileutils'
3
- require 'erb'
4
- require 'yaml'
5
- require 'time'
6
-
7
3
  require 'bluecloth'
8
4
  require 'rubypants'
5
+ require 'coderay'
6
+ require 'kontrol'
7
+ require 'git_store'
9
8
 
10
9
  begin; require 'redcloth'; rescue LoadError; end
11
- begin; require 'packr'; rescue LoadError; end
12
10
 
13
- require 'shinmun/cache'
14
- require 'shinmun/post'
15
- require 'shinmun/comment'
16
- require 'shinmun/template'
11
+ require 'shinmun/bluecloth_coderay'
17
12
  require 'shinmun/helpers'
13
+ require 'shinmun/handlers'
18
14
  require 'shinmun/blog'
19
-
20
- require 'shinmun/aggregations/audioscrobbler'
21
- require 'shinmun/aggregations/delicious'
22
- require 'shinmun/aggregations/flickr'
23
- require 'shinmun/aggregations/fortythree'
15
+ require 'shinmun/routes'
16
+ require 'shinmun/post'
17
+ require 'shinmun/comment'
@@ -0,0 +1,4 @@
1
+ <h2>Not Found</h2>
2
+ <p>
3
+ The page <%= @path %> was not found.
4
+ </p>
@@ -0,0 +1,21 @@
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>
@@ -0,0 +1,11 @@
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 %>
@@ -0,0 +1,11 @@
1
+ <h1><%= "#{Date::MONTHNAMES[@month]} #{@year}" %></h1>
2
+
3
+ <div class="articles">
4
+ <% for post in @blog.posts_for_month(@year, @month) %>
5
+ <div class="article">
6
+ <div class="date"><%= human_date post.date %></div>
7
+ <h2><%= post_link post %></h2>
8
+ <%= post.summary %>
9
+ </div>
10
+ <% end %>
11
+ </div>
@@ -0,0 +1,11 @@
1
+ <h1><%= @name %></h1>
2
+
3
+ <div class="articles">
4
+ <% for post in @posts %>
5
+ <div class="article">
6
+ <div class="date"><%= human_date post.date %></div>
7
+ <h2><%= post_link post %></h2>
8
+ <%= post.summary %>
9
+ </div>
10
+ <% end %>
11
+ </div>
@@ -0,0 +1,20 @@
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>
@@ -0,0 +1,11 @@
1
+ <h1>Home</h1>
2
+
3
+ <div class="articles">
4
+ <% for post in @blog.recent_posts %>
5
+ <div class="article">
6
+ <div class="date"><%= human_date post.date %></div>
7
+ <h2><%= post_link post %></h2>
8
+ <%= post.summary %>
9
+ </div>
10
+ <% end %>
11
+ </div>
@@ -0,0 +1,21 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <rss version="2.0">
3
+ <channel>
4
+ <title><%= @blog.title %></title>
5
+ <link><%= @blog.url %></link>
6
+ <description><%= @blog.description %></description>
7
+ <language><%= @blog.language %></language>
8
+ <copyright><%= @blog.author %></copyright>
9
+ <pubDate><%= rfc822 Time.now %></pubDate>
10
+ <% for post in @blog.recent_posts %>
11
+ <item>
12
+ <title><%= post.title %></title>
13
+ <category><%= post.category %></category>
14
+ <description><%= strip_tags post.summary %></description>
15
+ <author><%= @author || @blog.author %></author>
16
+ <link><%= @blog.url %><%= post_path post %></link>
17
+ <pubDate><%= rfc822 post.date %></pubDate>
18
+ </item>
19
+ <% end %>
20
+ </channel>
21
+ </rss>
@@ -0,0 +1,44 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2
+ <html xmlns="http://www.w3.org/1999/xhtml">
3
+ <head>
4
+ <title><%= @blog.title %></title>
5
+ <meta http-equiv="Content-Language" content="English" />
6
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
7
+ <link rel="alternate" type="application/rss+xml" title="RSS" href="/index.rss" />
8
+ <link rel="stylesheet" media="screen" type="text/css" href="/assets/styles.css" />
9
+ <link rel="stylesheet" media="print" type="text/css" href="/assets/print.css" />
10
+ </head>
11
+ <body>
12
+ <div class="container">
13
+ <h3><a href="/"><%= @blog.title %></a> - <%= @blog.description %></h3>
14
+
15
+ <div class="menu">
16
+ <%= link_to 'Home', '/' %> -
17
+ <%= link_to 'About', '/about' %> -
18
+ <%= link_to 'Subscribe RSS', '/index.rss' %>
19
+ </div>
20
+
21
+ <%= @content %>
22
+
23
+ <hr/>
24
+
25
+ <p>
26
+ <h4>Categories</h4>
27
+ <ul>
28
+ <li><%= link_to 'Ruby', '/categories/ruby' %></li>
29
+ <li><%= link_to 'Javascript', '/categories/javascript' %></li>
30
+ </ul>
31
+ </p>
32
+
33
+ <p>
34
+ <h4>Archive</h4>
35
+ <ul>
36
+ <% for year, month in @blog.archives %>
37
+ <li><%= archive_link year, month %></li>
38
+ <% end %>
39
+ </ul>
40
+ </p>
41
+
42
+ </div>
43
+ </body>
44
+ </html>
@@ -0,0 +1,5 @@
1
+ <h1><%= @page.title %></h1>
2
+
3
+ <div class="article">
4
+ <%= @page.body_html %>
5
+ </div>
@@ -0,0 +1,33 @@
1
+ <div class="article">
2
+
3
+ <h1><%= @post.title %></h1>
4
+
5
+ <div class="date">
6
+ <%= human_date @post.date %>
7
+ </div>
8
+
9
+ <%= @post.body_html %>
10
+
11
+ <p>
12
+ <% if @post.category %>
13
+ Posted in category <%= link_to @post.category, "/categories/#{urlify @post.category}" %>.
14
+ <% end %>
15
+
16
+ <% unless @post.tag_list.empty? %>
17
+ Tagged with <%= @post.tag_list.map {|tag| link_to tag, "/tags/#{tag}" }.join(', ') %>.
18
+ <% end %>
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
+
33
+ </div>