baron 1.0.0

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