aerial 0.0.0

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 (48) hide show
  1. data/.gitignore +1 -0
  2. data/MIT-LICENSE +22 -0
  3. data/README.md +74 -0
  4. data/Rakefile +96 -0
  5. data/VERSION +1 -0
  6. data/config/config.yml +19 -0
  7. data/config/deploy.rb +50 -0
  8. data/lib/aerial.rb +100 -0
  9. data/lib/aerial/article.rb +241 -0
  10. data/lib/aerial/base.rb +172 -0
  11. data/lib/aerial/comment.rb +160 -0
  12. data/lib/aerial/config.rb +41 -0
  13. data/lib/aerial/content.rb +74 -0
  14. data/lib/aerial/vendor/akismetor.rb +52 -0
  15. data/lib/aerial/vendor/cache.rb +139 -0
  16. data/lib/features/article.feature +10 -0
  17. data/lib/features/home.feature +16 -0
  18. data/lib/features/step_definitions/article_steps.rb +4 -0
  19. data/lib/features/step_definitions/home_steps.rb +8 -0
  20. data/lib/features/support/env.rb +38 -0
  21. data/lib/features/support/pages/article.rb +9 -0
  22. data/lib/features/support/pages/homepage.rb +9 -0
  23. data/lib/spec/aerial_spec.rb +203 -0
  24. data/lib/spec/article_spec.rb +338 -0
  25. data/lib/spec/base_spec.rb +65 -0
  26. data/lib/spec/comment_spec.rb +216 -0
  27. data/lib/spec/config_spec.rb +25 -0
  28. data/lib/spec/fixtures/articles/sample-article/sample-article.article +6 -0
  29. data/lib/spec/fixtures/articles/test-article-one/test-article.article +7 -0
  30. data/lib/spec/fixtures/articles/test-article-three/test-article.article +7 -0
  31. data/lib/spec/fixtures/articles/test-article-two/comment-missing-fields.comment +8 -0
  32. data/lib/spec/fixtures/articles/test-article-two/test-article.article +7 -0
  33. data/lib/spec/fixtures/articles/test-article-two/test-comment.comment +10 -0
  34. data/lib/spec/fixtures/config.yml +35 -0
  35. data/lib/spec/fixtures/public/javascripts/application.js +109 -0
  36. data/lib/spec/fixtures/public/javascripts/jquery-1.3.1.min.js +19 -0
  37. data/lib/spec/fixtures/public/javascripts/jquery.template.js +255 -0
  38. data/lib/spec/fixtures/views/article.haml +19 -0
  39. data/lib/spec/fixtures/views/articles.haml +2 -0
  40. data/lib/spec/fixtures/views/comment.haml +8 -0
  41. data/lib/spec/fixtures/views/home.haml +2 -0
  42. data/lib/spec/fixtures/views/layout.haml +22 -0
  43. data/lib/spec/fixtures/views/post.haml +27 -0
  44. data/lib/spec/fixtures/views/rss.haml +15 -0
  45. data/lib/spec/fixtures/views/sidebar.haml +21 -0
  46. data/lib/spec/fixtures/views/style.sass +163 -0
  47. data/lib/spec/spec_helper.rb +117 -0
  48. metadata +101 -0
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ lib/spec/repo
data/MIT-LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2009, Matt Sears, Littlelines, LLC.
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,74 @@
1
+ Aerial
2
+ ====
3
+
4
+ Aerial is a simple, blogish, semi-static web application written in Sinatra.
5
+ Designed for developers, there is no admin interface and no SQL database.
6
+ Articles are written in your favorite text editor and versioned with Git.
7
+ Comments are also stored as plain-text files and pushed to the remote
8
+ repository when created. It uses Grit (http://github.com/mojombo/grit) to
9
+ interface with local and remote Git repositories.
10
+
11
+ Aerial was designed for small personal blogs and simple semi-static websites
12
+ such as marketing sites. The main goals are to provide a no-fuss alternative
13
+ with a basic set features.
14
+
15
+ Aerial is still in active development.
16
+
17
+ ## Features #################################################################
18
+
19
+ * Akismet spam filtering (see vendor/akismetor.rb)
20
+ * Page caching (see vendor/cache.rb)
21
+ * Support for Markdown
22
+ * Vlad deployment tasks
23
+ * YAML configuration
24
+ * 100% code coverage
25
+
26
+ ## Requirements #############################################################
27
+
28
+ * sinatra (for awesomeness)
29
+ * git (http://git-scm.com)
30
+ * grit (interface to git)
31
+ * yaml (for configuration)
32
+ * rdiscount (markdown-to-html)
33
+ * Haml (can easily be switch to erb, or whatever)
34
+ * jQuery (http://jquery.com)
35
+
36
+ ## Source ###################################################################
37
+
38
+ Grit's Git repo is available on GitHub, which can be browsed at:
39
+
40
+ http://github.com/mattsears/aerial
41
+
42
+ and cloned with:
43
+
44
+ git clone git://github.com/mattsears/aerial.git
45
+
46
+ ## Getting Started ###########################################################
47
+
48
+ Install the following Rubygems:
49
+
50
+ sudo gem install sinatra rack thin rdiscount grit haml
51
+
52
+ Add your custom settings to the configuration file:
53
+
54
+ config/config.yml
55
+
56
+ Run the bootstrap Rake task to get started with a sample article
57
+
58
+ rake bootstrap
59
+
60
+ Now open your browser to:
61
+
62
+ http://localhost:4567
63
+
64
+ ## Todo #####################################################################
65
+
66
+ * Enable/disable comments for an article.
67
+ * Limit the number of comments for an article.
68
+ * Improve bootstrap tasks
69
+ * Add more details to this README
70
+
71
+ ## License ###################################################################
72
+
73
+ Aerial is Copyright © 2009 Matt Sears, Littlelines. It is free software,
74
+ and may be redistributed under the terms specified in the MIT-LICENSE file.
data/Rakefile ADDED
@@ -0,0 +1,96 @@
1
+ # $:.unshift(File.dirname(__FILE__) + '/../../../lib')
2
+
3
+ require 'rubygems'
4
+ require 'spec/version'
5
+ require 'spec/rake/spectask'
6
+ require 'cucumber/rake/task'
7
+
8
+ AERIAL_ROOT = "."
9
+ require File.join(AERIAL_ROOT, 'lib', 'aerial')
10
+
11
+ # Rspec setup
12
+ desc "Run all specs"
13
+ Spec::Rake::SpecTask.new do |t|
14
+ t.spec_files = FileList['lib/spec/**/*_spec.rb']
15
+ end
16
+
17
+ namespace :spec do
18
+ desc "Run all specs with rcov"
19
+ Spec::Rake::SpecTask.new('rcov') do |t|
20
+ t.spec_files = FileList['lib/spec/**/*_spec.rb']
21
+ t.rcov = true
22
+ t.rcov_dir = 'coverage'
23
+ t.rcov_opts = ['--exclude',
24
+ "lib/spec.rb,spec\/spec,bin\/spec,examples,\.autotest,#{ENV['GEM_HOME']}"]
25
+ end
26
+ end
27
+
28
+ desc "Setup the directory structure"
29
+ task :bootstrap do
30
+ Rake::Task['setup:articles_directory'].invoke
31
+ Rake::Task['setup:views_directory'].invoke
32
+ Rake::Task['setup:public_directory'].invoke
33
+ Rake::Task['run'].invoke
34
+ end
35
+
36
+ desc 'Run Aerial in development mode'
37
+ task :run do
38
+ exec "ruby lib/aerial.rb"
39
+ end
40
+
41
+ namespace :setup do
42
+
43
+ desc "Copy over a sample article"
44
+ task :articles_directory do
45
+ puts "* Creating article directory in " + Aerial.config.views.dir
46
+ article_dir = File.join(AERIAL_ROOT, 'lib','spec','fixtures',
47
+ 'articles', 'sample-article')
48
+ FileUtils.mkdir_p( Aerial.config.articles.dir )
49
+ FileUtils.cp_r(article_dir, Aerial.config.articles.dir )
50
+ Aerial::Git.commit(Aerial.config.articles.dir, "Initial import of first article")
51
+ end
52
+
53
+ task :public_directory do
54
+ puts "* Creating public directory in " + Aerial.config.public.dir
55
+ FileUtils.cp_r(File.join(AERIAL_ROOT, 'lib', 'spec',
56
+ 'fixtures', 'public'),
57
+ Aerial.config.public.dir )
58
+ end
59
+
60
+ task :views_directory do
61
+ puts "* Creating views directory in " + Aerial.config.views.dir
62
+ FileUtils.cp_r(File.join(AERIAL_ROOT, 'lib', 'spec',
63
+ 'fixtures', 'views'),
64
+ Aerial.config.views.dir )
65
+ end
66
+ end
67
+
68
+ # Cucumber setup
69
+ Cucumber::Rake::Task.new(:features) do |t|
70
+ t.cucumber_opts = "--format pretty"
71
+ end
72
+
73
+ # Vlad setup
74
+ begin
75
+ require "vlad"
76
+ Vlad.load(:app => nil, :scm => "git")
77
+ rescue LoadError
78
+ # do nothing
79
+ end
80
+
81
+
82
+ begin
83
+ require 'jeweler'
84
+ Jeweler::Tasks.new do |gemspec|
85
+ gemspec.name = "aerial"
86
+ gemspec.summary = "A simple, blogish software build with Sinatra, jQuery, and uses Git for data storage "
87
+ gemspec.description = "A simple, blogish software build with Sinatra, jQuery, and uses Git for data storage "
88
+ gemspec.email = "matt@mattsears.com"
89
+ gemspec.homepage = "http://github.com/mattsears/aerial"
90
+ gemspec.description = "A simple, blogish software build with Sinatra, jQuery, and uses Git for data storage"
91
+ gemspec.authors = ["Matt Sears"]
92
+ end
93
+ Jeweler::GemcutterTasks.new
94
+ rescue LoadError
95
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
96
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.0
data/config/config.yml ADDED
@@ -0,0 +1,19 @@
1
+ title: ""
2
+ subtitle: ""
3
+ name: ""
4
+ author: ""
5
+ email: ""
6
+
7
+ articles:
8
+ dir: "app/articles"
9
+
10
+ public:
11
+ dir: "public"
12
+
13
+ views:
14
+ dir: "app/views"
15
+ default: "home"
16
+
17
+ akismet:
18
+ key: ""
19
+ url: ""
data/config/deploy.rb ADDED
@@ -0,0 +1,50 @@
1
+ # =============================================================================
2
+ # VLAD VARIABLES
3
+ # =============================================================================
4
+
5
+ set :application, ""
6
+ set :repository, ""
7
+ set :deploy_to, ""
8
+ set :user, ""
9
+ set :domain, ""
10
+ set :app_command, "/etc/init.d/apache2"
11
+
12
+ desc 'Deploy the app!'
13
+ task :deploy do
14
+ Rake::Task["vlad:update"].invoke
15
+ Rake::Task["vlad:setup_repo"].invoke
16
+ Rake::Task["vlad:update_config"].invoke
17
+ end
18
+
19
+ desc 'Sync local and production code'
20
+ remote_task :remote_pull do
21
+ run "cd #{current_release}; #{git_cmd} pull origin master"
22
+ end
23
+
24
+ namespace :vlad do
25
+
26
+ desc 'Restart Passenger'
27
+ remote_task :start_app do
28
+ run "touch #{current_release}/tmp/restart.txt"
29
+ end
30
+
31
+ desc 'Restarts the apache servers'
32
+ remote_task :start_web do
33
+ run "sudo #{app_command} restart"
34
+ end
35
+
36
+ desc 'Copy the git repo over'
37
+ remote_task :setup_repo do
38
+ run "cp -fR #{scm_path}/repo/.git #{current_release}/. "
39
+ run "cd #{current_release}; #{git_cmd} checkout master"
40
+ end
41
+
42
+ desc 'Upload the configuration script'
43
+ remote_task :update_config do
44
+ run "mkdir #{current_release}/config" rescue nil
45
+ rsync "config/config.yml", "#{current_release}/config/config.yml"
46
+ end
47
+
48
+ end
49
+
50
+
data/lib/aerial.rb ADDED
@@ -0,0 +1,100 @@
1
+ libdir = File.dirname(__FILE__)
2
+ $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
3
+ AERIAL_ROOT = File.join(File.dirname(__FILE__), '..') unless defined? AERIAL_ROOT
4
+
5
+ # System requirements
6
+ require 'rubygems'
7
+ require 'grit'
8
+ require 'yaml'
9
+ require 'sinatra'
10
+ require 'rdiscount'
11
+ require 'aerial/base'
12
+
13
+ before do
14
+ # kill trailing slashes for all requests except '/'
15
+ request.env['PATH_INFO'].gsub!(/\/$/, '') if request.env['PATH_INFO'] != '/'
16
+ end
17
+
18
+ # Configuration
19
+ configure do
20
+ set :views => Aerial.config.theme_directory
21
+ set :public => Aerial.config.public.dir
22
+ end
23
+
24
+ # Helpers
25
+ helpers do
26
+ include Rack::Utils
27
+ include Sinatra::Cache::Helpers
28
+ include Aerial::Helper
29
+ alias_method :h, :escape_html
30
+ end
31
+
32
+ # Homepage
33
+ get '/' do
34
+ @articles = Aerial::Article.recent(:limit => 10)
35
+ cache haml(Aerial.config.views.default.to_sym)
36
+ end
37
+
38
+ # Articles
39
+ get '/articles' do
40
+ @content_for_sidebar = partial(:sidebar)
41
+ @articles = Aerial::Article.recent(:limit => 10)
42
+ cache haml(:articles)
43
+ end
44
+
45
+ get '/feed*' do
46
+ content_type 'text/xml', :charset => 'utf-8'
47
+ @articles = Aerial::Article.all
48
+ cache haml(:rss, :layout => false)
49
+ end
50
+
51
+ # Sassy!
52
+ get '/style.css' do
53
+ content_type 'text/css', :charset => 'utf-8'
54
+ cache sass(:style)
55
+ end
56
+
57
+ # Single page
58
+ get '/:page' do
59
+ cache haml(params[:page])
60
+ end
61
+
62
+ # Single article page
63
+ get '/:year/:month/:day/:article' do
64
+ link = [params[:year], params[:month], params[:day], params[:article]].join("/")
65
+ @content_for_sidebar = partial(:sidebar)
66
+ @article = Aerial::Article.with_permalink("/#{link}")
67
+ throw :halt, [404, not_found ] unless @article
68
+ @page_title = @article.title
69
+ cache haml(:post)
70
+ end
71
+
72
+ # Article tags
73
+ get '/tags/:tag' do
74
+ @content_for_sidebar = partial(:sidebar)
75
+ @articles = Aerial::Article.with_tag(params[:tag])
76
+ cache haml(:articles)
77
+ end
78
+
79
+ # Article archives
80
+ get '/archives/:year/:month' do
81
+ @content_for_sidebar = partial(:sidebar)
82
+ @articles = Aerial::Article.with_date(params[:year], params[:month])
83
+ cache haml(:articles)
84
+ end
85
+
86
+ # Add comment
87
+ post '/article/:id/comments' do
88
+ @article = Aerial::Article.find(params[:id])
89
+ throw :halt, [404, not_found ] unless @article
90
+
91
+ comment = Aerial::Comment.new(params.merge!( {
92
+ :referrer => request.referrer,
93
+ :user_agent => request.user_agent,
94
+ :user_ip => request.ip
95
+ }))
96
+
97
+ @article.comments << comment.save(@article.archive_name)
98
+ cache_expire( @article.permalink )
99
+ status 204
100
+ end
@@ -0,0 +1,241 @@
1
+ module Aerial
2
+
3
+ class Article < Content
4
+
5
+ attr_reader :comments, :id, :tags, :archive_name, :body_html,
6
+ :meta, :updated_on, :published, :file_name
7
+
8
+ # =============================================================================================
9
+ # PUBLIC CLASS METHODS
10
+ # =============================================================================================
11
+
12
+ # Find all articles, including drafts
13
+ def self.all(options={})
14
+ self.find_all
15
+ end
16
+
17
+ # A quick way to load an article
18
+ # +id+ of the blob
19
+ def self.open(id, options = {})
20
+ self.find_by_blob_id(id, options)
21
+ end
22
+
23
+ # Find a single article
24
+ # +id+ of the blob
25
+ def self.find(id, options={})
26
+ self.find_by_id(id, options)
27
+ end
28
+
29
+ # Find a single article
30
+ # +name+ of the article file
31
+ def self.with_name(name, options={})
32
+ self.find_by_name(name, options)
33
+ end
34
+
35
+ # Find articles by tag
36
+ # +tag+ category
37
+ def self.with_tag(tag, options={})
38
+ self.find_by_tag(tag, options)
39
+ end
40
+
41
+ # Find articles by month and year
42
+ # +year+ of when article was published
43
+ # + month+ of when the article was published
44
+ def self.with_date(year, month, options={})
45
+ self.find_by_date(year, month, options)
46
+ end
47
+
48
+ # Return an article given its permalink value
49
+ # +link+ full path of the link
50
+ def self.with_permalink(link, options={})
51
+ self.find_by_permalink(link, options)
52
+ end
53
+
54
+ # Find the most recent articles
55
+ def self.recent(options={})
56
+ limit = options.delete(:limit) || 4
57
+ self.find_all(options).first(limit)
58
+ end
59
+
60
+ # Return true if the article file exists
61
+ def self.exists?(id)
62
+ self.find_by_name(id) ? true : false
63
+ end
64
+
65
+ # Return all the tags assigned to the articles
66
+ def self.tags
67
+ self.find_tags
68
+ end
69
+
70
+ # Calculate the ar
71
+ def self.archives
72
+ self.find_archives
73
+ end
74
+
75
+ # =============================================================================================
76
+ # PUBLIC INSTANCE METHODS
77
+ # =============================================================================================
78
+
79
+ # Add a comment to the list of this Article's comments
80
+ # +comment new comment
81
+ def add_comment(comment)
82
+ self.comments << comment.save(self.archive_name) # should we overload the << method?
83
+ end
84
+
85
+ # Permanent link for the article
86
+ def permalink
87
+ link = self.file_name.gsub('.article', '')
88
+ "/#{published_at.year}/#{published_at.month}/#{published_at.day}/#{escape(link)}"
89
+ end
90
+
91
+ # Returns the absolute path of the Article's file
92
+ def expand_path
93
+ return "#{self.archive_expand_path}/#{self.file_name}"
94
+ end
95
+
96
+ # Returns the full path of the article's archive
97
+ def archive_expand_path
98
+ return unless archive = self.archive_name
99
+ return "#{Aerial.repo.working_dir}/#{Aerial.config.articles.dir}/#{archive}"
100
+ end
101
+
102
+ private
103
+
104
+ # =============================================================================================
105
+ # PRIVATE CLASS METHODS
106
+ # =============================================================================================
107
+
108
+ # Find a single article given the article name
109
+ # +name+ file name
110
+ def self.find_by_name(name, options={})
111
+ if tree = Aerial.repo.tree/"#{Aerial.config.articles.dir}/#{name}"
112
+ return self.find_article(tree)
113
+ end
114
+ end
115
+
116
+ # Find the single article given the id
117
+ # +id+ the blob id
118
+ # +options+
119
+ def self.find_by_id(article_id, options = {})
120
+ if blog = Aerial.repo.tree/"#{Aerial.config.articles.dir}"
121
+ blog.contents.each do |entry|
122
+ article = self.find_article(entry, options)
123
+ return article if article.id == article_id
124
+ end
125
+ end
126
+ raise "Article not found"
127
+ end
128
+
129
+ # Find the article given the blob id.
130
+ # This is a more efficient way of find and Article
131
+ # However, we won't know anything else about the article such as the filename, tree, etc
132
+ # +id+ of the blob
133
+ def self.find_by_blob_id(id, options = {})
134
+ blob = Aerial.repo.blob(id)
135
+ if blob.size > 0
136
+ attributes = self.extract_article(blob, options)
137
+ return Article.new(attributes) if attributes
138
+ end
139
+ raise "Article doesn't exists"
140
+ end
141
+
142
+ # Returns the articles with the given tag
143
+ # +tag+ the article category
144
+ def self.find_by_tag(tag, options = {})
145
+ articles = []
146
+ self.find_all.each do |article|
147
+ if article.tags.include?(tag)
148
+ articles << article
149
+ end
150
+ end
151
+ return articles
152
+ end
153
+
154
+ # Find a single article given the article's permalink value
155
+ def self.find_by_permalink(link, options={})
156
+ if blog = Aerial.repo.tree/"#{Aerial.config.articles.dir}/"
157
+ blog.contents.each do |entry|
158
+ article = self.find_article(entry, options)
159
+ return article if article.permalink == link
160
+ end
161
+ end
162
+ return false
163
+ end
164
+
165
+ # Find all the articles with the given month and date
166
+ def self.find_by_date(year, month, options ={})
167
+ articles = []
168
+ self.find_all.each do |article|
169
+ if article.published_at.year == year.to_i &&
170
+ article.published_at.month == month.to_i
171
+ articles << article
172
+ end
173
+ end
174
+ return articles
175
+ end
176
+
177
+ # Find all the articles in the reposiotory
178
+ def self.find_all(options={})
179
+ articles = []
180
+ if blog = Aerial.repo.tree/"#{Aerial.config.articles.dir}/"
181
+ blog.contents.first( options[:limit] || 100 ).each do |entry|
182
+ article = self.find_article(entry, options)
183
+ articles << self.find_article(entry, options) if article
184
+ end
185
+ end
186
+ return articles.sort_by { |article| article.published_at}.reverse
187
+ end
188
+
189
+ # Look in the given tree, find the article
190
+ # +tree+ repository tree
191
+ # +options+ :blob_id
192
+ def self.find_article(tree, options = {})
193
+ comments = []
194
+ attributes = nil
195
+ tree.contents.each do |archive|
196
+ if archive.name =~ /article/
197
+ attributes = self.extract_article(archive, options)
198
+ attributes[:archive_name] = tree.name
199
+ attributes[:file_name] = archive.name
200
+ elsif archive.name =~ /comment/
201
+ comments << Comment.open(archive.data, :file_name => archive.name)
202
+ end
203
+ end
204
+ return Article.new(attributes.merge(:comments => comments)) if attributes
205
+ end
206
+
207
+ # Find all the tags assign to the articles
208
+ def self.find_tags
209
+ tags = []
210
+ self.all.each do |article|
211
+ tags.concat(article.tags)
212
+ end
213
+ return tags.uniq
214
+ end
215
+
216
+ # Create a histogram of article archives
217
+ def self.find_archives
218
+ dates = []
219
+ self.all.each do |article|
220
+ date = article.published_at
221
+ dates << [date.strftime("%Y/%m"), date.strftime("%B %Y")]
222
+ end
223
+ return dates.inject(Hash.new(0)) { |h,x| h[x] += 1; h }
224
+ end
225
+
226
+ # Extract the Article attributes from the file
227
+ def self.extract_article(blob, options={})
228
+ file = blob.data
229
+ article = Hash.new
230
+ 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
237
+ return article
238
+ end
239
+
240
+ end
241
+ end