baron 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +38 -0
- data/Rakefile +34 -0
- data/Readme.md +168 -0
- data/VERSION +1 -0
- data/lib/baron.rb +373 -0
- data/spec/baron_article_spec.rb +53 -0
- data/spec/baron_blog_engine_spec.rb +62 -0
- data/spec/baron_spec.rb +174 -0
- data/spec/sample_data/Gemfile +6 -0
- data/spec/sample_data/Rakefile +42 -0
- data/spec/sample_data/articles/2012-11-09-sample-post.txt +11 -0
- data/spec/sample_data/articles/poems/1909-01-02-If.txt +48 -0
- data/spec/sample_data/articles/poems/1916-01-01-the-road-not-taken.txt +26 -0
- data/spec/sample_data/config.ru +46 -0
- data/spec/sample_data/images/import-csv-file-1.png +0 -0
- data/spec/sample_data/images/import-csv-file-2.png +0 -0
- data/spec/sample_data/images/import-csv-file-3.png +0 -0
- data/spec/sample_data/images/instagram.png +0 -0
- data/spec/sample_data/pages/about.rhtml +14 -0
- data/spec/sample_data/resources/feed.rss +18 -0
- data/spec/sample_data/resources/redirects.txt +44 -0
- data/spec/sample_data/resources/robots.txt +1 -0
- data/spec/sample_data/themes/test/css/app.css +27 -0
- data/spec/sample_data/themes/test/css/bootstrap-responsive.css +1092 -0
- data/spec/sample_data/themes/test/css/bootstrap-responsive.min.css +9 -0
- data/spec/sample_data/themes/test/css/bootstrap.css +6039 -0
- data/spec/sample_data/themes/test/css/bootstrap.min.css +9 -0
- data/spec/sample_data/themes/test/img/glyphicons-halflings-white.png +0 -0
- data/spec/sample_data/themes/test/img/glyphicons-halflings.png +0 -0
- data/spec/sample_data/themes/test/img/instagram.png +0 -0
- data/spec/sample_data/themes/test/js/bootstrap.js +2159 -0
- data/spec/sample_data/themes/test/js/bootstrap.min.js +6 -0
- data/spec/sample_data/themes/test/js/image_alt.js +12 -0
- data/spec/sample_data/themes/test/js/read_later.js +14 -0
- data/spec/sample_data/themes/test/templates/archives.rhtml +14 -0
- data/spec/sample_data/themes/test/templates/article.rhtml +14 -0
- data/spec/sample_data/themes/test/templates/category.rhtml +15 -0
- data/spec/sample_data/themes/test/templates/error.rhtml +3 -0
- data/spec/sample_data/themes/test/templates/index.rhtml +26 -0
- data/spec/sample_data/themes/test/templates/layout.rhtml +86 -0
- data/spec/spec_helper.rb +19 -0
- 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 => "›", # used to divide the different elements of the page title
|
306
|
+
:truncation_marker => '…', # 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
|