aerial 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. data/.gitignore +4 -1
  2. data/README.md +23 -28
  3. data/Rakefile +27 -52
  4. data/VERSION +1 -1
  5. data/aerial.gemspec +134 -0
  6. data/articles/congratulations/congratulations-aerial-is-configured-correctly.article +6 -0
  7. data/bin/aerial +5 -0
  8. data/config/config.sample.ru +19 -0
  9. data/config/config.sample.yml +29 -0
  10. data/config/config.test.yml +49 -0
  11. data/config/config.yml +3 -3
  12. data/config/thin.sample.yml +13 -0
  13. data/examples/articles/congratulations/congratulations-aerial-is-configured-correctly.article +6 -0
  14. data/{lib/spec/fixtures → examples}/public/javascripts/application.js +0 -0
  15. data/{lib/spec/fixtures → examples}/public/javascripts/jquery-1.3.1.min.js +0 -0
  16. data/{lib/spec/fixtures → examples}/public/javascripts/jquery.template.js +0 -0
  17. data/{lib/spec/fixtures → examples}/views/article.haml +1 -1
  18. data/{lib/spec/fixtures → examples}/views/articles.haml +0 -0
  19. data/{lib/spec/fixtures → examples}/views/comment.haml +1 -1
  20. data/{lib/spec/fixtures → examples}/views/home.haml +0 -0
  21. data/{lib/spec/fixtures → examples}/views/layout.haml +1 -1
  22. data/examples/views/not_found.haml +1 -0
  23. data/{lib/spec/fixtures → examples}/views/post.haml +0 -0
  24. data/{lib/spec/fixtures → examples}/views/rss.haml +2 -2
  25. data/{lib/spec/fixtures → examples}/views/sidebar.haml +8 -0
  26. data/{lib/spec/fixtures → examples}/views/style.sass +0 -0
  27. data/{lib/features → features}/article.feature +0 -0
  28. data/features/home.feature +16 -0
  29. data/{lib/features → features}/step_definitions/article_steps.rb +0 -0
  30. data/{lib/features → features}/step_definitions/home_steps.rb +0 -0
  31. data/{lib/features → features}/support/env.rb +0 -0
  32. data/{lib/features → features}/support/pages/article.rb +0 -0
  33. data/{lib/features → features}/support/pages/homepage.rb +0 -0
  34. data/index.html +164 -0
  35. data/lib/aerial.rb +38 -87
  36. data/lib/aerial/app.rb +93 -0
  37. data/lib/aerial/article.rb +28 -30
  38. data/lib/aerial/base.rb +24 -32
  39. data/lib/aerial/comment.rb +10 -19
  40. data/lib/aerial/content.rb +12 -21
  41. data/lib/aerial/installer.rb +119 -0
  42. data/public/javascripts/application.js +109 -0
  43. data/public/javascripts/jquery-1.3.1.min.js +19 -0
  44. data/public/javascripts/jquery.template.js +255 -0
  45. data/spec/aerial_spec.rb +19 -0
  46. data/{lib/spec/aerial_spec.rb → spec/app_spec.rb} +32 -40
  47. data/{lib/spec → spec}/article_spec.rb +21 -21
  48. data/{lib/spec → spec}/base_spec.rb +1 -5
  49. data/{lib/spec → spec}/comment_spec.rb +8 -8
  50. data/{lib/spec → spec}/config_spec.rb +0 -0
  51. data/spec/fixtures/articles/congratulations-aerial-is-configured-correctly/congratulations-aerial-is-configured-correctly.article +6 -0
  52. data/spec/fixtures/articles/sample-article/sample-article.article +6 -0
  53. data/spec/fixtures/articles/test-article-one/test-article.article +7 -0
  54. data/spec/fixtures/articles/test-article-three/test-article.article +7 -0
  55. data/spec/fixtures/articles/test-article-two/comment-missing-fields.comment +8 -0
  56. data/spec/fixtures/articles/test-article-two/test-article.article +7 -0
  57. data/spec/fixtures/articles/test-article-two/test-comment.comment +10 -0
  58. data/{lib/spec → spec}/fixtures/config.yml +0 -0
  59. data/spec/fixtures/public/javascripts/application.js +109 -0
  60. data/spec/fixtures/public/javascripts/jquery-1.3.1.min.js +19 -0
  61. data/spec/fixtures/public/javascripts/jquery.template.js +255 -0
  62. data/spec/fixtures/views/article.haml +19 -0
  63. data/spec/fixtures/views/articles.haml +2 -0
  64. data/spec/fixtures/views/comment.haml +8 -0
  65. data/spec/fixtures/views/home.haml +2 -0
  66. data/spec/fixtures/views/layout.haml +22 -0
  67. data/spec/fixtures/views/not_found.haml +1 -0
  68. data/spec/fixtures/views/post.haml +27 -0
  69. data/spec/fixtures/views/rss.haml +15 -0
  70. data/spec/fixtures/views/sidebar.haml +29 -0
  71. data/spec/fixtures/views/style.sass +163 -0
  72. data/{lib/spec → spec}/spec_helper.rb +15 -3
  73. data/views/article.haml +19 -0
  74. data/views/articles.haml +2 -0
  75. data/views/comment.haml +8 -0
  76. data/views/home.haml +2 -0
  77. data/views/layout.haml +22 -0
  78. data/views/not_found.haml +1 -0
  79. data/views/post.haml +27 -0
  80. data/views/rss.haml +15 -0
  81. data/views/sidebar.haml +29 -0
  82. data/views/style.sass +163 -0
  83. metadata +86 -40
  84. data/lib/features/home.feature +0 -16
  85. data/lib/spec/fixtures/articles/sample-article/sample-article.article +0 -6
  86. data/lib/spec/fixtures/articles/test-article-one/test-article.article +0 -7
  87. data/lib/spec/fixtures/articles/test-article-three/test-article.article +0 -7
  88. data/lib/spec/fixtures/articles/test-article-two/comment-missing-fields.comment +0 -8
  89. data/lib/spec/fixtures/articles/test-article-two/test-article.article +0 -7
  90. data/lib/spec/fixtures/articles/test-article-two/test-comment.comment +0 -10
@@ -0,0 +1,93 @@
1
+ module Aerial
2
+ class App < Sinatra::Default
3
+ include Aerial
4
+
5
+ before do
6
+ # kill trailing slashes for all requests except '/'
7
+ request.env['PATH_INFO'].gsub!(/\/$/, '') if request.env['PATH_INFO'] != '/'
8
+ end
9
+
10
+ # Helpers
11
+ helpers do
12
+ include Rack::Utils
13
+ include Sinatra::Cache::Helpers
14
+ include Aerial::Helper
15
+ alias_method :h, :escape_html
16
+ end
17
+
18
+ # Homepage
19
+ get '/' do
20
+ @articles = Aerial::Article.recent(:limit => 10)
21
+ cache haml(Aerial.config.views.default.to_sym)
22
+ end
23
+
24
+ # Articles
25
+ get '/articles' do
26
+ @content_for_sidebar = partial(:sidebar)
27
+ @articles = Aerial::Article.recent(:limit => 10)
28
+ cache haml(:articles)
29
+ end
30
+
31
+ get '/feed*' do
32
+ content_type 'text/xml', :charset => 'utf-8'
33
+ @articles = Aerial::Article.all
34
+ cache haml(:rss, :layout => false)
35
+ end
36
+
37
+ # Sassy!
38
+ get '/style.css' do
39
+ content_type 'text/css', :charset => 'utf-8'
40
+ cache sass(:style)
41
+ end
42
+
43
+ # Single page
44
+ get '/:page' do
45
+ cache haml(params[:page])
46
+ end
47
+
48
+ # Single article page
49
+ get '/:year/:month/:day/:article' do
50
+ link = [params[:year], params[:month], params[:day], params[:article]].join("/")
51
+ @content_for_sidebar = partial(:sidebar)
52
+ @article = Aerial::Article.with_permalink("/#{link}")
53
+ throw :halt, [404, not_found ] unless @article
54
+ @page_title = @article.title
55
+ cache haml(:post)
56
+ end
57
+
58
+ # Article tags
59
+ get '/tags/:tag' do
60
+ @content_for_sidebar = partial(:sidebar)
61
+ @articles = Aerial::Article.with_tag(params[:tag])
62
+ cache haml(:articles)
63
+ end
64
+
65
+ # Article archives
66
+ get '/archives/:year/:month' do
67
+ @content_for_sidebar = partial(:sidebar)
68
+ @articles = Aerial::Article.with_date(params[:year], params[:month])
69
+ cache haml(:articles)
70
+ end
71
+
72
+ # Add comment
73
+ post '/article/:id/comments' do
74
+ @article = Aerial::Article.find(params[:id])
75
+ throw :halt, [404, not_found ] unless @article
76
+
77
+ comment = Aerial::Comment.new(params.merge!({:referrer => request.referrer,
78
+ :user_agent => request.user_agent,
79
+ :ip => request.ip
80
+ }))
81
+
82
+ @article.comments << comment.save(@article.archive_name)
83
+ cache_expire( @article.permalink )
84
+ status 204
85
+ end
86
+
87
+ not_found do
88
+ @content_for_sidebar = partial(:sidebar)
89
+ cache haml(:not_found)
90
+ end
91
+
92
+ end
93
+ end
@@ -3,7 +3,7 @@ module Aerial
3
3
  class Article < Content
4
4
 
5
5
  attr_reader :comments, :id, :tags, :archive_name, :body_html,
6
- :meta, :updated_on, :published, :file_name
6
+ :meta, :updated_on, :publish_date, :file_name
7
7
 
8
8
  # =============================================================================================
9
9
  # PUBLIC CLASS METHODS
@@ -14,19 +14,19 @@ module Aerial
14
14
  self.find_all
15
15
  end
16
16
 
17
- # A quick way to load an article
17
+ # A quick way to load an article by blob id
18
18
  # +id+ of the blob
19
19
  def self.open(id, options = {})
20
20
  self.find_by_blob_id(id, options)
21
21
  end
22
22
 
23
- # Find a single article
23
+ # Find a single article by id
24
24
  # +id+ of the blob
25
25
  def self.find(id, options={})
26
26
  self.find_by_id(id, options)
27
27
  end
28
28
 
29
- # Find a single article
29
+ # Find a single article by name
30
30
  # +name+ of the article file
31
31
  def self.with_name(name, options={})
32
32
  self.find_by_name(name, options)
@@ -58,6 +58,7 @@ module Aerial
58
58
  end
59
59
 
60
60
  # Return true if the article file exists
61
+ # +id+
61
62
  def self.exists?(id)
62
63
  self.find_by_name(id) ? true : false
63
64
  end
@@ -67,7 +68,7 @@ module Aerial
67
68
  self.find_tags
68
69
  end
69
70
 
70
- # Calculate the ar
71
+ # Calculate the archives
71
72
  def self.archives
72
73
  self.find_archives
73
74
  end
@@ -79,21 +80,21 @@ module Aerial
79
80
  # Add a comment to the list of this Article's comments
80
81
  # +comment new comment
81
82
  def add_comment(comment)
82
- self.comments << comment.save(self.archive_name) # should we overload the << method?
83
+ self.comments << comment.save(self.archive_name) # TODO: should we overload the << method?
83
84
  end
84
85
 
85
- # Permanent link for the article
86
+ # Make a permanent link for the article
86
87
  def permalink
87
- link = self.file_name.gsub('.article', '')
88
- "/#{published_at.year}/#{published_at.month}/#{published_at.day}/#{escape(link)}"
88
+ link = self.file_name.gsub(/\.article$|\.markdown$|\.md$|\.mdown$|\.mkd$|\.mkdn$/, '')
89
+ "/#{publish_date.year}/#{publish_date.month}/#{publish_date.day}/#{escape(link)}"
89
90
  end
90
91
 
91
- # Returns the absolute path of the Article's file
92
+ # Returns the absolute path to the article file
92
93
  def expand_path
93
94
  return "#{self.archive_expand_path}/#{self.file_name}"
94
95
  end
95
96
 
96
- # Returns the full path of the article's archive
97
+ # Returns the full path to the article archive (directory)
97
98
  def archive_expand_path
98
99
  return unless archive = self.archive_name
99
100
  return "#{Aerial.repo.working_dir}/#{Aerial.config.articles.dir}/#{archive}"
@@ -113,7 +114,7 @@ module Aerial
113
114
  end
114
115
  end
115
116
 
116
- # Find the single article given the id
117
+ # Find a single article by id
117
118
  # +id+ the blob id
118
119
  # +options+
119
120
  def self.find_by_id(article_id, options = {})
@@ -126,8 +127,8 @@ module Aerial
126
127
  raise "Article not found"
127
128
  end
128
129
 
129
- # Find the article given the blob id.
130
- # This is a more efficient way of find and Article
130
+ # Find an article by blob id
131
+ # This is a more efficient way of finding an article
131
132
  # However, we won't know anything else about the article such as the filename, tree, etc
132
133
  # +id+ of the blob
133
134
  def self.find_by_blob_id(id, options = {})
@@ -139,7 +140,7 @@ module Aerial
139
140
  raise "Article doesn't exists"
140
141
  end
141
142
 
142
- # Returns the articles with the given tag
143
+ # Returns all articles by tag
143
144
  # +tag+ the article category
144
145
  def self.find_by_tag(tag, options = {})
145
146
  articles = []
@@ -151,7 +152,8 @@ module Aerial
151
152
  return articles
152
153
  end
153
154
 
154
- # Find a single article given the article's permalink value
155
+ # Find a single article by permalink
156
+ # +link+
155
157
  def self.find_by_permalink(link, options={})
156
158
  if blog = Aerial.repo.tree/"#{Aerial.config.articles.dir}/"
157
159
  blog.contents.each do |entry|
@@ -162,19 +164,19 @@ module Aerial
162
164
  return false
163
165
  end
164
166
 
165
- # Find all the articles with the given month and date
167
+ # Find all the articles by year and month
166
168
  def self.find_by_date(year, month, options ={})
167
169
  articles = []
168
170
  self.find_all.each do |article|
169
- if article.published_at.year == year.to_i &&
170
- article.published_at.month == month.to_i
171
+ if article.publish_date.year == year.to_i &&
172
+ article.publish_date.month == month.to_i
171
173
  articles << article
172
174
  end
173
175
  end
174
176
  return articles
175
177
  end
176
178
 
177
- # Find all the articles in the reposiotory
179
+ # Find all the articles in the repository
178
180
  def self.find_all(options={})
179
181
  articles = []
180
182
  if blog = Aerial.repo.tree/"#{Aerial.config.articles.dir}/"
@@ -183,7 +185,7 @@ module Aerial
183
185
  articles << self.find_article(entry, options) if article
184
186
  end
185
187
  end
186
- return articles.sort_by { |article| article.published_at}.reverse
188
+ return articles.sort_by { |article| article.publish_date}.reverse
187
189
  end
188
190
 
189
191
  # Look in the given tree, find the article
@@ -217,23 +219,19 @@ module Aerial
217
219
  def self.find_archives
218
220
  dates = []
219
221
  self.all.each do |article|
220
- date = article.published_at
222
+ date = article.publish_date
221
223
  dates << [date.strftime("%Y/%m"), date.strftime("%B %Y")]
222
224
  end
223
225
  return dates.inject(Hash.new(0)) { |h,x| h[x] += 1; h }
224
226
  end
225
227
 
226
228
  # Extract the Article attributes from the file
229
+ # +blob+
227
230
  def self.extract_article(blob, options={})
228
- file = blob.data
229
- article = Hash.new
231
+ article = self.extract_attributes(blob.data)
230
232
  article[:id] = blob.id
231
- article[:author] = self.extract_header("author", file)
232
- article[:title] = self.extract_header("title", file)
233
- article[:tags] = self.extract_header("tags", file).split(/, /)
234
- article[:published_at] = DateTime.parse(self.extract_header("published", file))
235
- article[:body] = self.scan_for_field(file, self.body_field)
236
- article[:body_html] = RDiscount::new( article[:body] ).to_html
233
+ article[:tags] = article[:tags].split(/, /)
234
+ article[:body_html] = RDiscount::new(article[:body]).to_html
237
235
  return article
238
236
  end
239
237
 
@@ -1,51 +1,43 @@
1
- CONFIG = YAML.load_file( File.join(AERIAL_ROOT, 'config', 'config.yml') ) unless defined?(CONFIG)
2
-
3
- require 'aerial/content'
4
- require 'aerial/article'
5
- require 'aerial/comment'
6
- require 'aerial/vendor/cache'
7
- require 'aerial/vendor/akismetor'
8
- require 'aerial/config'
9
-
10
1
  module Aerial
11
2
 
12
- VERSION = '0.1.0'
13
-
14
- class << self
15
- attr_accessor :debug, :logger, :repo, :config
16
-
17
- def log(str)
18
- logger.debug { str } if debug
19
- end
20
- end
21
-
22
- # Make sure git is added to the env path
23
- ENV['PATH'] = "#{ENV['PATH']}:/usr/local/bin"
24
- @logger ||= ::Logger.new(STDOUT)
25
- @config ||= Aerial::Config.new(CONFIG)
26
- @repo ||= Grit::Repo.new(File.join(AERIAL_ROOT, '.'))
27
- @debug ||= false
28
-
29
3
  module Helper
30
4
 
31
- # Defines the url for our application
32
- def hostname
5
+ # Returns the current url
6
+ def url() request.url end
7
+
8
+ # Returns the request host
9
+ # TODO: just use request.host (http://rack.lighthouseapp.com/projects/22435/tickets/77-requesthost-should-answer-the-forwarded-host)
10
+ def host
33
11
  if request.env['HTTP_X_FORWARDED_SERVER'] =~ /[a-z]*/
34
12
  request.env['HTTP_X_FORWARDED_SERVER']
35
13
  else
36
- request.env['HTTP_HOST']
14
+ request.host
37
15
  end
38
16
  end
39
17
 
40
- # Returns the
18
+ # Returns the path
41
19
  def path
42
20
  base = "#{request.env['REQUEST_URI']}".scan(/\w+/).first
43
21
  return base.blank? ? "index" : base
44
22
  end
45
23
 
46
- # Creates a complete link including the hostname
24
+ # Returns the absolute base url
25
+ def base_url
26
+ scheme = request.scheme
27
+ port = request.port
28
+ url = "#{scheme}://#{host}"
29
+ if scheme == "http" && port != 80 || scheme == "https" && port != 443
30
+ url << ":#{port}"
31
+ end
32
+ url << request.script_name
33
+ end
34
+
35
+ # Creates an absolute link
36
+ # +link+ link to append to the baseurl
37
+ # TODO: should we add more value to this? it seems like we might as well
38
+ # just take care of this by appending the link to base_url in the app
47
39
  def full_hostname(link = "")
48
- "http://#{hostname}#{link}"
40
+ "#{base_url}#{link}"
49
41
  end
50
42
 
51
43
  # Display the page titles in proper format
@@ -4,8 +4,8 @@ module Aerial
4
4
  class Comment < Content
5
5
 
6
6
  attr_reader :id, :permalink, :article, :spam, :file_path
7
- attr_accessor :archive_name, :spam, :published_at, :name, :referrer,
8
- :email, :homepage, :user_ip, :user_agent, :file_name
7
+ attr_accessor :archive_name, :spam, :publish_date, :name, :referrer,
8
+ :email, :homepage, :ip, :user_agent, :file_name
9
9
 
10
10
  def initialize(atts = {})
11
11
  super
@@ -81,12 +81,12 @@ module Aerial
81
81
  def to_s
82
82
  me = ""
83
83
  me << "Author: #{self.author} \n" if self.author
84
- me << "Published: #{self.published_at} \n" if self.published_at.to_s
84
+ me << "Publish Date: #{self.publish_date} \n" if self.publish_date.to_s
85
85
  me << "Email: #{self.email} \n" if self.email
86
86
  me << "Homepage: #{self.homepage} \n" if self.homepage
87
- me << "User IP: #{self.user_ip} \n" if self.user_ip
87
+ me << "User IP: #{self.ip} \n" if self.ip
88
88
  me << "User Agent: #{self.user_agent} \n" if self.user_agent
89
- me << "Spam?: #{self.spam} \n" if self.user_agent
89
+ me << "Spam?: #{self.spam} \n" if self.spam
90
90
  me << "\n#{self.body}" if self.body
91
91
  return me
92
92
  end
@@ -100,17 +100,8 @@ module Aerial
100
100
  # Create a new Comment instance with data from the given file
101
101
  def self.extract_comment_from(data, options = {})
102
102
  comment_file = data.to_s
103
- comment = options
104
- comment[:id] = self.extract_header("id", comment_file)
105
- comment[:user_ip] = self.extract_header("ip", comment_file)
106
- comment[:user_agent] = self.extract_header("user-agent", comment_file)
107
- comment[:referrer] = self.extract_header("referrer", comment_file)
108
- comment[:permalink] = self.extract_header("permalink", comment_file)
109
- comment[:author] = self.extract_header("author", comment_file)
110
- comment[:email] = self.extract_header("email", comment_file)
111
- comment[:homepage] = self.extract_header("homepage", comment_file)
112
- comment[:published_at]= DateTime.parse(self.extract_header("published", comment_file))
113
- comment[:body] = self.scan_for_field(comment_file, self.body_field)
103
+ comment = self.extract_attributes(comment_file)
104
+ comment.merge!(options)
114
105
  return comment
115
106
  end
116
107
 
@@ -118,7 +109,7 @@ module Aerial
118
109
  def self.save_new(comment)
119
110
  return false unless comment && comment.archive_name
120
111
  comment.generate_name!
121
- comment.published_at = DateTime.now
112
+ comment.publish_date = DateTime.now
122
113
  path = File.join(Aerial.config.articles.dir, comment.archive_name, comment.name)
123
114
  Dir.chdir(Aerial.repo.working_dir) do
124
115
  File.open(path, 'w') do |file|
@@ -136,7 +127,7 @@ module Aerial
136
127
  # Make sure the url is cleaned
137
128
  def sanitize_url
138
129
  return unless self.homepage
139
- homepage.gsub!(/^(.*)/, 'http://\1') unless homepage =~ %r{^http://} or homepage.empty?
130
+ homepage.gsub!(/^(.*)/, 'http://\1') unless homepage =~ %r{^(http://|https://)} or homepage.empty?
140
131
  end
141
132
 
142
133
  # Try to prevent spam with akismet
@@ -144,7 +135,7 @@ module Aerial
144
135
  {
145
136
  :key => Aerial.config.akismet.key,
146
137
  :blog => Aerial.config.akismet.url,
147
- :user_ip => self.user_ip,
138
+ :user_ip => self.ip,
148
139
  :user_agent => self.user_agent,
149
140
  :referrer => self.referrer,
150
141
  :permalink => self.permalink,
@@ -6,7 +6,7 @@ module Aerial
6
6
  # Base class for all the site's content
7
7
  class Content
8
8
 
9
- attr_reader :id, :author, :title, :body, :published_at, :archive_name, :file_name
9
+ attr_reader :id, :author, :title, :body, :publish_date, :archive_name, :file_name
10
10
 
11
11
  def initialize(atts = {})
12
12
  atts.each_pair { |key, value| instance_variable_set("@#{key}", value) if self.respond_to? key}
@@ -18,26 +18,17 @@ module Aerial
18
18
  # PROTECTED CLASS METHODS
19
19
  # =============================================================================================
20
20
 
21
- # With the comment string, attempt to find the given field
22
- # +field+ the label before the ":"
23
- # +comment+ is the contents of the comment
24
- def self.extract_header(field, content)
25
- return self.scan_for_field(content, self.header_field_for(field))
26
- end
27
-
28
- # Returns the string that matches the given pattern
29
- def self.scan_for_field(contents, pattern)
30
- content = contents.scan(pattern).first.to_s.strip
31
- end
32
-
33
- # Returns the regular expression pattern for the header fields
34
- def self.header_field_for(header)
35
- exp = Regexp.new('^'+header+'\s*:(.*)$', Regexp::IGNORECASE)
36
- end
37
-
38
- # Returns the regular expression pattern for the body field
39
- def self.body_field
40
- exp = Regexp.new('^\n(.*)$', Regexp::MULTILINE)
21
+ def self.extract_attributes(content, options={})
22
+ attributes = Hash.new
23
+ header, body = content.split(/\n\n/, 2)
24
+ attributes[:body] = body.strip if body
25
+ header.each do |line|
26
+ field, data = line.split(/:/, 2)
27
+ field = field.downcase.strip.gsub(' ', '_').gsub('-', '_')
28
+ attributes[field.to_sym] = data.to_s.strip
29
+ end
30
+ attributes[:publish_date] = DateTime.parse(attributes[:publish_date]) if attributes[:publish_date]
31
+ return attributes
41
32
  end
42
33
 
43
34
  # Look for <code> blocks and convert it for syntax highlighting