aerial 0.0.0

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