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.
- 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
|