baron 1.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 (42) hide show
  1. data/LICENSE +38 -0
  2. data/Rakefile +34 -0
  3. data/Readme.md +168 -0
  4. data/VERSION +1 -0
  5. data/lib/baron.rb +373 -0
  6. data/spec/baron_article_spec.rb +53 -0
  7. data/spec/baron_blog_engine_spec.rb +62 -0
  8. data/spec/baron_spec.rb +174 -0
  9. data/spec/sample_data/Gemfile +6 -0
  10. data/spec/sample_data/Rakefile +42 -0
  11. data/spec/sample_data/articles/2012-11-09-sample-post.txt +11 -0
  12. data/spec/sample_data/articles/poems/1909-01-02-If.txt +48 -0
  13. data/spec/sample_data/articles/poems/1916-01-01-the-road-not-taken.txt +26 -0
  14. data/spec/sample_data/config.ru +46 -0
  15. data/spec/sample_data/images/import-csv-file-1.png +0 -0
  16. data/spec/sample_data/images/import-csv-file-2.png +0 -0
  17. data/spec/sample_data/images/import-csv-file-3.png +0 -0
  18. data/spec/sample_data/images/instagram.png +0 -0
  19. data/spec/sample_data/pages/about.rhtml +14 -0
  20. data/spec/sample_data/resources/feed.rss +18 -0
  21. data/spec/sample_data/resources/redirects.txt +44 -0
  22. data/spec/sample_data/resources/robots.txt +1 -0
  23. data/spec/sample_data/themes/test/css/app.css +27 -0
  24. data/spec/sample_data/themes/test/css/bootstrap-responsive.css +1092 -0
  25. data/spec/sample_data/themes/test/css/bootstrap-responsive.min.css +9 -0
  26. data/spec/sample_data/themes/test/css/bootstrap.css +6039 -0
  27. data/spec/sample_data/themes/test/css/bootstrap.min.css +9 -0
  28. data/spec/sample_data/themes/test/img/glyphicons-halflings-white.png +0 -0
  29. data/spec/sample_data/themes/test/img/glyphicons-halflings.png +0 -0
  30. data/spec/sample_data/themes/test/img/instagram.png +0 -0
  31. data/spec/sample_data/themes/test/js/bootstrap.js +2159 -0
  32. data/spec/sample_data/themes/test/js/bootstrap.min.js +6 -0
  33. data/spec/sample_data/themes/test/js/image_alt.js +12 -0
  34. data/spec/sample_data/themes/test/js/read_later.js +14 -0
  35. data/spec/sample_data/themes/test/templates/archives.rhtml +14 -0
  36. data/spec/sample_data/themes/test/templates/article.rhtml +14 -0
  37. data/spec/sample_data/themes/test/templates/category.rhtml +15 -0
  38. data/spec/sample_data/themes/test/templates/error.rhtml +3 -0
  39. data/spec/sample_data/themes/test/templates/index.rhtml +26 -0
  40. data/spec/sample_data/themes/test/templates/layout.rhtml +86 -0
  41. data/spec/spec_helper.rb +19 -0
  42. metadata +150 -0
data/LICENSE ADDED
@@ -0,0 +1,38 @@
1
+ This software is licensed under the MIT Software License
2
+
3
+ Copyright (c) 2013 Nathan Buggia
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9
+ of the Software, and to permit persons to whom the Software is furnished to do
10
+ so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ - - -
24
+
25
+ While writing this blog engine, I barrowed some code and design approches
26
+ from the Toto project by Cloudhead and the Scanty project by Adam Wiggins.
27
+ I'm not sure how much code or design awesomeness one needs to use before
28
+ they are obligated to include their license, so I'm included a link to
29
+ each of them just in case (and thank you both for your awesomeness!)
30
+
31
+ Toto
32
+ - URL: https://github.com/cloudhead/toto
33
+ - Author: http://cloudhead.io/ (Alexis Sellier)
34
+ - License: https://github.com/cloudhead/toto/blob/master/LICENSE
35
+
36
+ Scanty
37
+ - URL: https://github.com/adamwiggins/scanty
38
+ - Author: http://about.adamwiggins.com/ (Adam Wiggins)
data/Rakefile ADDED
@@ -0,0 +1,34 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ ##
5
+ # Create gem
6
+
7
+ begin
8
+ require 'jeweler'
9
+ Jeweler::Tasks.new do |gem|
10
+ gem.name = "baron"
11
+ gem.summary = %Q{Hacked version of the toto blog engine}
12
+ gem.description = %Q{Hacked version of the toto blog engine.}
13
+ gem.email = "nbuggia@gmail.com"
14
+ gem.homepage = "https://github.com/nbuggia/baron"
15
+ gem.authors = ["Nathan Buggia"]
16
+ gem.add_development_dependency "rspec"
17
+ gem.add_dependency "builder"
18
+ gem.add_dependency "rack"
19
+ gem.add_dependency "rdiscount"
20
+ end
21
+ Jeweler::GemcutterTasks.new
22
+ rescue LoadError
23
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
24
+ end
25
+
26
+ ##
27
+ # Run RSpec tests
28
+
29
+ require 'rspec/core/rake_task'
30
+ RSpec::Core::RakeTask.new(:spec)
31
+
32
+
33
+ task :default => :test
34
+ task :test => [:check_dependencies, :spec]
data/Readme.md ADDED
@@ -0,0 +1,168 @@
1
+ #Baron Blog Engine Gem
2
+
3
+ A full-featured, yet minimalist, blog engine for developers
4
+
5
+ I know what you're thinking, the world doesn't need another Ruby blog
6
+ engine. And, okay, you're right, however Baron is a little bit different from
7
+ all the others in that it is a lot more full-featured, and still only a scant
8
+ 400 lines of easy-to-ready code.
9
+
10
+ **Features**
11
+ * Publish to heroku (or similar PaaS) using Git
12
+ * Author articles or custom pages in markdown, text or HTML
13
+ * Article categories supported by simply putting articles in a folder
14
+ * Many permalink formats are supported, including a custom prefix and several
15
+ date formats
16
+ * 301 or 302 redirects are support for easy porting from your current blog
17
+ * SEO optimized with built-in support for Robots.txt, Google Analytics, Google
18
+ web master tools
19
+ * Easy to customize the look & feel via a common site layout template
20
+ * Frameworks used: Rack, RSpec, Bootstrap, JQuery, Disqus, Thin
21
+
22
+ ##Quick Start
23
+
24
+ To use the baron blog, go to the client project and follow the instructions
25
+ there. This project holds the source for the engine gem.
26
+
27
+ TODO - insert link to client project when ready...
28
+
29
+ ##Next Steps
30
+
31
+ I wrote this as an excuse to learn a handful of new technologies and approaches,
32
+ like Ruby and TDD. There are an ambitious set of features I'd like to add that
33
+ each align to something else I would like to learn:
34
+
35
+ * Themes - I'm designing 3-4 fancy, shmancy themes to try out this new 'flat'
36
+ and minimalist thing everyone's excited about. Also a good excuse to dig into
37
+ HTML5, CSS3, JQuery, Instagram's API and a few other things.
38
+
39
+ * Pre-rendering - the platform nerd in me doesn't understand why the whole
40
+ blog isn't pre-rendered at deploy time so heroku just serves static HTML and
41
+ assets (a la <a href="https://github.com/mojombo/jekyll">Jekyll</a>)
42
+
43
+ * JavaScript Comments - the blog engine currently uses Disqus for comments,
44
+ which is free and cool, but I hate letting other people own my data. I want
45
+ to build something similar to Disqus on top of
46
+ <a href="https://www.parse.com/">Parse</a> /
47
+ <a href="https://github.com/documentcloud/backbone">Backbone</a> and make it
48
+ really easy to use
49
+
50
+ * Simple Plugin Model - I've always wanted to write a plug-in model. I tried
51
+ to write one in C++ in college and was only able to do static linking (lame). I
52
+ think an interpreted language will make it much easier, right?
53
+
54
+ ##How Does it Work?
55
+
56
+ Here's a quick overview of how the whole thing comes together.
57
+
58
+ There are two parts to this blog, the **Baron Blog Engine Gem** and the
59
+ **Baron Blog** project. The blog engine gem contains all the code for the data
60
+ model and the view controllers, which is conveniently packaged up into a gem
61
+ for easy distribution (I might change that in the future to make it easier to
62
+ hack). The Baron Blog project contains all of the views, and the assets (CSS,
63
+ images, articles, etc). It references the blog engine gem.
64
+
65
+ **Baron Blog Engine Gem**
66
+
67
+ All of the source code for this is in a single code file (./lib/baron.rb).
68
+
69
+ Project structure:
70
+
71
+ ├── LICENSE
72
+ ├── Rakefile rake test, rake install
73
+ ├── Readme.md you are reading this document now...
74
+ ├── VERSION
75
+ ├── lib/
76
+ │   └── baron.rb all the source code for the gem
77
+ ├── pkg/
78
+ │   └── baron-1.0.0.gem byte-code compiled gem (I think)
79
+ └── spec/ unit tests using RSpec
80
+ ├── baron_article_spec.rb article data model tests
81
+ ├── baron_blog_engine_spec.rb BlogEngine class tests
82
+ ├── baron_spec.rb end-to-end tests
83
+ ├── sample_data/ sample data for testing
84
+ └── spec_helper.rb
85
+
86
+ * Baron::BlogEngine - handles the main application loop. It handles building the
87
+ right page for every given route. It also contains all the logic for where all
88
+ the files are stored.
89
+
90
+ * Baron::PageController - uses Ruby's ERB library to render the template pages
91
+ with the variable's from the Baron::BlogEngine. Most pages get rendered twice,
92
+ the first time we render the partial page (e.g. an article data model into the
93
+ article rhtml template) and the second time we render the article.rhtml results
94
+ into the site layout template (./themes/theme/templates/layout.rhtml)
95
+
96
+ * Baron::Article - the data model for a single article.
97
+
98
+ **Baron Blog**
99
+
100
+ Project structure:
101
+
102
+ ├── Gemfile
103
+ ├── Rakefile
104
+ ├── articles/ place your published articles here
105
+ │   ├── 2012-11-09-sample-1.txt the date and URL slug are the filename
106
+ │   └── category/ creating folders puts these articles in a category
107
+ │   ├── another category/ spaces in folder names will be replaces with '-'s
108
+ ├── config.ru configure features of the blog here
109
+ ├── downloads/ files in here are publicly accessible
110
+ ├── drafts/ place for your unfinished articles
111
+ ├── images/ images in here are publicly accessible
112
+ ├── pages/ you can create custom pages in here
113
+ │   └── about.rhtml
114
+ ├── resources/
115
+ │   ├── feed.rss your rss feed's rendering template
116
+ │   ├── redirects.txt list of redirects the blog will process
117
+ │   └── robots.txt your robots.txt file
118
+ └── themes/
119
+ └── my-theme/ each theme has the same folder structure
120
+ ├── css/
121
+ ├── img/
122
+ ├── js/
123
+ └── templates/ rhtml rendering templates for each page type
124
+
125
+ TODO - I'll update this with more detail once I've posted the Baron Blog project to github
126
+
127
+ ##Thanks
128
+
129
+ While writing this blog engine, I barrowed a lot of code and design approaches
130
+ from the Toto project by Cloudhead and the Scanty project by Adam Wiggins. The
131
+ primary purpose of this project was a learning one for me, and both of these
132
+ folks provided a lot of good code an examples. I'm not sure how much code or
133
+ design awesomeness one needs to use before they are obligated to include their
134
+ license, so I'm included a link to each of them just in case (and thank you
135
+ both for your awesomeness!)
136
+
137
+ Toto
138
+ - URL: https://github.com/cloudhead/toto
139
+ - Author: http://cloudhead.io/ (Alexis Sellier)
140
+ - License: https://github.com/cloudhead/toto/blob/master/LICENSE
141
+
142
+ Scanty
143
+ - URL: https://github.com/adamwiggins/scanty
144
+ - Author: http://about.adamwiggins.com/ (Adam Wiggins)
145
+
146
+ ##License
147
+
148
+ This software is licensed under the MIT Software License
149
+
150
+ Copyright (c) 2013 Nathan Buggia
151
+
152
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
153
+ this software and associated documentation files (the "Software"), to deal in
154
+ the Software without restriction, including without limitation the rights to
155
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
156
+ of the Software, and to permit persons to whom the Software is furnished to do
157
+ so, subject to the following conditions:
158
+
159
+ The above copyright notice and this permission notice shall be included in all
160
+ copies or substantial portions of the Software.
161
+
162
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
163
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
164
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
165
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
166
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
167
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
168
+ SOFTWARE.
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
data/lib/baron.rb ADDED
@@ -0,0 +1,373 @@
1
+ require 'yaml'
2
+ require 'date'
3
+ require 'erb'
4
+ require 'rack'
5
+ require 'digest'
6
+ require 'rdiscount'
7
+
8
+ $:.unshift File.dirname(__FILE__)
9
+
10
+ # Converts a number into an ordinal, 1=>1st, 2=>2nd, 3=>3rd, etc
11
+ class Fixnum
12
+ def ordinal
13
+ case self % 100
14
+ when 11..13; "#{self}th"
15
+ else
16
+ case self % 10
17
+ when 1; "#{self}st"
18
+ when 2; "#{self}nd"
19
+ when 3; "#{self}rd"
20
+ else "#{self}th"
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ # Avoid a collision with ActiveSupport
27
+ class Date
28
+ unless respond_to? :iso8601
29
+ # Return the date as a String formatted according to ISO 8601.
30
+ def iso8601
31
+ ::Time.utc(year, month, day, 0, 0, 0, 0).iso8601
32
+ end
33
+ end
34
+ end
35
+
36
+ class String
37
+ # Support String::bytesize in old versions of Ruby
38
+ if RUBY_VERSION < "1.9"
39
+ def bytesize
40
+ size
41
+ end
42
+ end
43
+
44
+ # Capitalize the first letter of each word in a string
45
+ def titleize
46
+ self.split(/(\W)/).map(&:capitalize).join
47
+ end
48
+ end
49
+
50
+ module Baron
51
+ def self.env
52
+ ENV['RACK_ENV'] || 'production'
53
+ end
54
+
55
+ def self.env= env
56
+ ENV['RACK_ENV'] = env
57
+ end
58
+
59
+ class PageController
60
+ def initialize articles_parts, categories, max_articles, params, config
61
+ @categories, @params, @config = categories, params, config
62
+ stop_at = (:all == max_articles) ? articles_parts.count : max_articles
63
+ @articles = articles_parts.take(stop_at).map { |file_parts| Article.new(file_parts, @config) }
64
+ @article = @articles.first
65
+ end
66
+
67
+ def render_html partial_template, layout_template
68
+ @content = ERB.new(File.read(partial_template)).result(binding)
69
+ @theme_root = "/themes/#{@config[:theme]}"
70
+ ERB.new(File.read(layout_template)).result(binding)
71
+ end
72
+
73
+ def render_rss template
74
+ ERB.new(File.read(template)).result(binding)
75
+ end
76
+ end
77
+
78
+ class BlogEngine
79
+ def initialize config
80
+ @config = config
81
+ end
82
+
83
+ def process_redirects request_path
84
+ File.open(get_system_resource('redirects.txt'), 'r') do |file|
85
+ file.each_line do |line|
86
+ if line[0] != '#'
87
+ command, status, source_path, destination_path = line.split(' ')
88
+ return destination_path, status if request_path == source_path
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ def process_request path, env = {}, mime_type = :html
95
+ route = (path || '/').split('/').reject { |i| i.empty? }
96
+ route << @config[:root] if route.empty?
97
+ mime_type = (mime_type =~ /txt|rss|json/) ? mime_type.to_sym : :html
98
+ categories = get_all_categories()
99
+ params = {:page_name => route.first}
100
+ params[:page_title] = (route.first == @config[:root] ? '' : "#{route.first.capitalize} #{@config[:title_delimiter]} ") + "#{@config[:title]}"
101
+
102
+ begin
103
+
104
+ # RSS feed... /feed.rss
105
+ body = if mime_type == :rss
106
+ PageController.new(get_all_articles, categories, @config[:article_max], params, @config) .
107
+ render_rss(get_system_resource('feed.rss'))
108
+
109
+ # Robots... /robots.txt
110
+ elsif route.first == 'robots'
111
+ File.read(get_system_resource('robots.txt')) rescue raise(Errno::ENOENT, 'Page not found')
112
+
113
+ # Home page... /
114
+ elsif route.first == 'index'
115
+ all_articles = get_all_articles()
116
+ params[:page_forward] = '/page/2/' if @config[:article_max] < all_articles.count
117
+ PageController.new(all_articles, categories, @config[:article_max], params, @config) .
118
+ render_html(get_theme_template(route.first), get_theme_template('layout'))
119
+
120
+ # Pagination... /page/2
121
+ elsif route.first == 'page' && route.count == 2
122
+ page_num = route.last.to_i() rescue page_num = -1
123
+ all_articles = get_all_articles()
124
+ max_pages = (all_articles.count.to_f / @config[:article_max].to_f).ceil
125
+ raise(Errno::ENOENT, 'Page not found') if page_num < 1 or page_num > max_pages
126
+
127
+ starting_article = ((page_num - 1) * @config[:article_max])
128
+ articles_on_this_page = all_articles.slice(starting_article, @config[:article_max])
129
+
130
+ show_next = (page_num * @config[:article_max]) < all_articles.count
131
+ params[:page_back] = "/page/#{(page_num-1).to_s}/" if page_num > 1
132
+ params[:page_forward] = "/page/#{(page_num+1).to_s}/" if show_next
133
+ params[:page_title] = "Page #{page_num.to_s} #{@config[:title_delimiter]} #{@config[:title]}"
134
+
135
+ PageController.new(articles_on_this_page, categories, @config[:article_max], params, @config) .
136
+ render_html(get_theme_template('index'), get_theme_template('layout'))
137
+
138
+ # System routes... /robots.txt, /archives
139
+ elsif route.first == 'archives' or route.first == 'robots'
140
+ max_articles = ('archives' == route.first) ? :all : @config[:article_max]
141
+ PageController.new(get_all_articles(), categories, max_articles, params, @config) .
142
+ render_html(get_theme_template(route.first), get_theme_template('layout'))
143
+
144
+ # Custom pages... /about, /contact-us
145
+ elsif is_route_custom_page? route.first
146
+ PageController.new(get_all_articles(), categories, @config[:article_max], params, @config) .
147
+ render_html(get_page_template(route.first), get_theme_template('layout'))
148
+
149
+ # Category index pages... /projects/, /photography/, /poems/, etc
150
+ elsif is_route_category_index? route.last
151
+ filtered_articles = get_all_articles().select { |h| h[:category] == route.last }
152
+ params[:page_name] = route.last.gsub('-', ' ').titleize()
153
+ PageController.new(filtered_articles, categories, :all, params, @config) .
154
+ render_html(get_theme_template('category'), get_theme_template('layout'))
155
+
156
+ # Articles... /posts/2013/01/18/my-article-title, /posts/category/2013/my-article-title, etc
157
+ else
158
+ article = [ find_single_article(route.last) ]
159
+ params[:page_title] = "#{article.first[:filename].gsub('-',' ').titleize} #{@config[:title_delimiter]} #{@config[:title]}"
160
+ PageController.new(article, categories, 1, params, @config) .
161
+ render_html(get_theme_template('article'), get_theme_template('layout'))
162
+ end
163
+
164
+ return :body => body, :type => mime_type, :status => 200
165
+
166
+ rescue Errno::ENOENT => e
167
+
168
+ # 404 Page Not Found
169
+ params[:error_message] = 'Page not found'
170
+ params[:error_code] = '404'
171
+ body = PageController.new([], categories, 0, params, @config) .
172
+ render_html(get_theme_template('error'), get_theme_template('layout'))
173
+
174
+ return :body => body, :type => :html, :status => 404
175
+ end
176
+ end
177
+
178
+ def get_all_category_folder_paths
179
+ category_paths = Dir["#{get_articles_path}/*/"].map { |a| "#{get_articles_path}/#{File.basename(a)}" }
180
+ # includes the default articles directory as an unnamed (e.g. empty) path
181
+ category_paths << "#{get_articles_path}"
182
+ end
183
+
184
+ def get_all_articles
185
+ get_all_category_folder_paths().map do |folder_name|
186
+ Dir["#{folder_name}/*"].map do |e|
187
+ if e.end_with? @config[:ext]
188
+ parts = e.split('/')
189
+ {
190
+ :filename_and_path => e,
191
+ :date => parts.last[0..9],
192
+ :filename => parts.last[11..(-1 * (@config[:ext].length + 2))].downcase, # trims date and extention
193
+ :category => parts[parts.count-2] == 'articles' ? '' : parts[parts.count-2]
194
+ }
195
+ end
196
+ end
197
+ end .
198
+ flatten .
199
+ delete_if { |a| a == nil } .
200
+ sort_by { |hash| hash[:date] } .
201
+ reverse # sorts by decending date
202
+ end
203
+
204
+ def get_all_categories
205
+ Dir["#{get_articles_path}/*/"].map do |a|
206
+ folder_name = File.basename(a)
207
+ {
208
+ :name => folder_name.titleize,
209
+ :node_name => folder_name.gsub(' ', '-'),
210
+ :path => "/#{@config[:permalink_prefix]}/#{folder_name.gsub(' ', '-')}/".squeeze('/'),
211
+ :count => Dir["#{get_articles_path}/#{folder_name}/*"].count
212
+ }
213
+ end .
214
+ sort_by { |hash| hash[:name] }
215
+ end
216
+
217
+ def find_single_article article_slug
218
+ get_all_articles().each { |fileparts| return fileparts if fileparts[:filename] == article_slug }
219
+ raise Errno::ENOENT, 'Article not found'
220
+ end
221
+
222
+ def is_route_custom_page? path_node
223
+ (Dir["#{get_pages_path}/*"]).include?("#{get_pages_path}/#{path_node}.rhtml")
224
+ end
225
+
226
+ def is_route_category_index? path_node
227
+ get_all_categories().each { |h| return true if h[:node_name] == path_node }
228
+ return false
229
+ end
230
+
231
+ def get_pages_path() "#{@config[:sample_data_path]}pages/" end
232
+ def get_articles_path() "#{@config[:sample_data_path]}articles" end
233
+ def get_page_template(name) "#{@config[:sample_data_path]}pages/#{name}.rhtml" end
234
+ def get_theme_template(name) "#{@config[:sample_data_path]}themes/#{@config[:theme]}/templates/#{name}.rhtml" end
235
+ def get_system_resource(name) "#{@config[:sample_data_path]}resources/#{name}" end
236
+ end
237
+
238
+ class Article < Hash
239
+ def initialize file_parts, config = {}
240
+ @config = config
241
+ self[:filename_and_path] = file_parts[:filename_and_path]
242
+ self[:slug] = file_parts[:filename]
243
+ self[:category] = file_parts[:category].empty? ? :default : file_parts[:category]
244
+ self[:date] = Date.parse(file_parts[:date].gsub('/', '-')) rescue Date.today
245
+ load_article(file_parts[:filename_and_path])
246
+ end
247
+
248
+ def summary length = nil
249
+ config = @config[:summary]
250
+ sum = if self[:body] =~ config[:delim]
251
+ self[:body].split(config[:delim]).first
252
+ else
253
+ self[:body].match(/(.{1,#{length || config[:length] || config[:max]}}.*?)(\n|\Z)/m).to_s
254
+ end
255
+ markdown(sum.length == self[:body].length ? sum : sum.strip.sub(/\.\Z/, @config[:truncation_marker]))
256
+ end
257
+
258
+ def body
259
+ markdown self[:body].sub(@config[:summary][:delim], '') rescue markdown self[:body]
260
+ end
261
+
262
+ def path prefix = '', date_format = ''
263
+ permalink_prefix = prefix.empty? ? @config[:permalink_prefix] : prefix
264
+ permalink_date_format = date_format.empty? ? @config[:permalink_date_format] : date_format
265
+ date_path = case permalink_date_format
266
+ when :year_date; self[:date].strftime("/%Y")
267
+ when :year_month_date; self[:date].strftime("/%Y/%m")
268
+ when :year_month_day_date; self[:date].strftime("/%Y/%m/%d")
269
+ else ''
270
+ end
271
+
272
+ "/#{permalink_prefix}/#{self[:category]}#{date_path}/#{slug}/".squeeze('/')
273
+ end
274
+
275
+ def title() self[:title].titleize || 'Untitled' end
276
+ def date() @config[:date].call(self[:date]) end
277
+ def author() self[:author] || @config[:author] end
278
+ def category() self[:category] end
279
+ def permalink() "http://#{(@config[:url].sub("http://", '') + self.path).squeeze('/')}" end
280
+ def slug() self[:slug] end
281
+
282
+ protected
283
+
284
+ def load_article filename_and_path
285
+ metadata, self[:body] = File.read(filename_and_path).split(/\n\n/, 2)
286
+ YAML.load(metadata).each_pair { |k,v| self[k.downcase.to_sym] = v }
287
+ end
288
+
289
+ def markdown text
290
+ if (options = @config[:markdown])
291
+ Markdown.new(text.to_s.strip, *(options.eql?(true) ? [] : options)).to_html
292
+ else
293
+ text.strip
294
+ end
295
+ end
296
+ end
297
+
298
+ class Config < Hash
299
+ Defaults = {
300
+ :cache => 28800, # cache duration (seconds)
301
+ :root => 'index', # site index
302
+ :sample_data_path => '', # used by the RSpec tests to show where the sample data is stored
303
+ :author => ENV['USER'], # blog author
304
+ :title => Dir.pwd.split('/').last, # site title
305
+ :title_delimiter => "&rsaquo;", # used to divide the different elements of the page title
306
+ :truncation_marker => '&hellip;', # symbol used to represent trucated text (article summary)
307
+ :url => 'http://localhost/', # root URL of the site
308
+ :date => lambda {|now| now.strftime("%d/%m/%Y") }, # date function
309
+ :markdown => :smart, # use markdown
310
+ :disqus => false, # disqus name
311
+ :summary => {:max => 150, :delim => /~\n/}, # length of summary and delimiter
312
+ :ext => 'txt', # extension for articles
313
+ :permalink_prefix => '', # common path prefix for article permalinks
314
+ :permalink_date_format => :year_month_day_date, # :year_date, :year_month_date, :year_month_day_date, :no_date
315
+ :article_max => 5, # number of most recent articles to return to custom pages
316
+ :theme => 'default', # name of the theme to use
317
+ :google_analytics => '', # account id for google analytics account
318
+ :google_webmaster => '' # HTML Meta Tag verification code for google webmaster account
319
+ }
320
+
321
+ def initialize obj
322
+ self.update Defaults
323
+ self.update obj
324
+ end
325
+
326
+ def set key, val = nil, &block
327
+ if val.is_a? Hash
328
+ self[key].update val
329
+ else
330
+ self[key] = block_given?? block : val
331
+ end
332
+ end
333
+ end
334
+
335
+ class Server
336
+ attr_reader :config, :site
337
+
338
+ def initialize config = {}, &block
339
+ @config = config.is_a?(Config) ? config : Config.new(config)
340
+ @config.instance_eval(&block) if block_given?
341
+ @blog_engine = Baron::BlogEngine.new(@config)
342
+ end
343
+
344
+ def call env
345
+ @request = Rack::Request.new env
346
+ return [400, {}, []] unless @request.get?
347
+
348
+ @response = Rack::Response.new
349
+ path, mime = @request.path_info.split('.')
350
+ redirected_url, status = @blog_engine.process_redirects(path)
351
+
352
+ if status
353
+ @response.status = status
354
+ @response['Location'] = redirected_url
355
+ else
356
+ baron_response = @blog_engine.process_request(path, env, *(mime ? mime : []))
357
+ @response.body = [baron_response[:body]]
358
+ @response.status = baron_response[:status]
359
+ @response['Content-Length'] = baron_response[:body].bytesize.to_s unless baron_response[:body].empty?
360
+ @response['Content-Type'] = Rack::Mime.mime_type(".#{baron_response[:type]}")
361
+ @response['Cache-Control'] = if Baron.env == 'production'
362
+ "public, max-age=#{@config[:cache]}"
363
+ else
364
+ "no-cache, must-revalidate"
365
+ end
366
+
367
+ @response['ETag'] = %("#{Digest::SHA1.hexdigest(baron_response[:body])}")
368
+ end
369
+
370
+ @response.finish
371
+ end
372
+ end
373
+ end