shinmun 0.2 → 0.5

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