aerial 0.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/.gitignore +1 -0
- data/MIT-LICENSE +22 -0
- data/README.md +74 -0
- data/Rakefile +96 -0
- data/VERSION +1 -0
- data/config/config.yml +19 -0
- data/config/deploy.rb +50 -0
- data/lib/aerial.rb +100 -0
- data/lib/aerial/article.rb +241 -0
- data/lib/aerial/base.rb +172 -0
- data/lib/aerial/comment.rb +160 -0
- data/lib/aerial/config.rb +41 -0
- data/lib/aerial/content.rb +74 -0
- data/lib/aerial/vendor/akismetor.rb +52 -0
- data/lib/aerial/vendor/cache.rb +139 -0
- data/lib/features/article.feature +10 -0
- data/lib/features/home.feature +16 -0
- data/lib/features/step_definitions/article_steps.rb +4 -0
- data/lib/features/step_definitions/home_steps.rb +8 -0
- data/lib/features/support/env.rb +38 -0
- data/lib/features/support/pages/article.rb +9 -0
- data/lib/features/support/pages/homepage.rb +9 -0
- data/lib/spec/aerial_spec.rb +203 -0
- data/lib/spec/article_spec.rb +338 -0
- data/lib/spec/base_spec.rb +65 -0
- data/lib/spec/comment_spec.rb +216 -0
- data/lib/spec/config_spec.rb +25 -0
- data/lib/spec/fixtures/articles/sample-article/sample-article.article +6 -0
- data/lib/spec/fixtures/articles/test-article-one/test-article.article +7 -0
- data/lib/spec/fixtures/articles/test-article-three/test-article.article +7 -0
- data/lib/spec/fixtures/articles/test-article-two/comment-missing-fields.comment +8 -0
- data/lib/spec/fixtures/articles/test-article-two/test-article.article +7 -0
- data/lib/spec/fixtures/articles/test-article-two/test-comment.comment +10 -0
- data/lib/spec/fixtures/config.yml +35 -0
- data/lib/spec/fixtures/public/javascripts/application.js +109 -0
- data/lib/spec/fixtures/public/javascripts/jquery-1.3.1.min.js +19 -0
- data/lib/spec/fixtures/public/javascripts/jquery.template.js +255 -0
- data/lib/spec/fixtures/views/article.haml +19 -0
- data/lib/spec/fixtures/views/articles.haml +2 -0
- data/lib/spec/fixtures/views/comment.haml +8 -0
- data/lib/spec/fixtures/views/home.haml +2 -0
- data/lib/spec/fixtures/views/layout.haml +22 -0
- data/lib/spec/fixtures/views/post.haml +27 -0
- data/lib/spec/fixtures/views/rss.haml +15 -0
- data/lib/spec/fixtures/views/sidebar.haml +21 -0
- data/lib/spec/fixtures/views/style.sass +163 -0
- data/lib/spec/spec_helper.rb +117 -0
- metadata +101 -0
data/lib/aerial/base.rb
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
CONFIG = YAML.load_file( File.join(AERIAL_ROOT, 'config', 'config.yml') ) unless defined?(CONFIG)
|
2
|
+
|
3
|
+
require 'aerial/content'
|
4
|
+
require 'aerial/article'
|
5
|
+
require 'aerial/comment'
|
6
|
+
require 'aerial/vendor/cache'
|
7
|
+
require 'aerial/vendor/akismetor'
|
8
|
+
require 'aerial/config'
|
9
|
+
|
10
|
+
module Aerial
|
11
|
+
|
12
|
+
VERSION = '0.1.0'
|
13
|
+
|
14
|
+
class << self
|
15
|
+
attr_accessor :debug, :logger, :repo, :config
|
16
|
+
|
17
|
+
def log(str)
|
18
|
+
logger.debug { str } if debug
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Make sure git is added to the env path
|
23
|
+
ENV['PATH'] = "#{ENV['PATH']}:/usr/local/bin"
|
24
|
+
@logger ||= ::Logger.new(STDOUT)
|
25
|
+
@config ||= Aerial::Config.new(CONFIG)
|
26
|
+
@repo ||= Grit::Repo.new(File.join(AERIAL_ROOT, '.'))
|
27
|
+
@debug ||= false
|
28
|
+
|
29
|
+
module Helper
|
30
|
+
|
31
|
+
# Defines the url for our application
|
32
|
+
def hostname
|
33
|
+
if request.env['HTTP_X_FORWARDED_SERVER'] =~ /[a-z]*/
|
34
|
+
request.env['HTTP_X_FORWARDED_SERVER']
|
35
|
+
else
|
36
|
+
request.env['HTTP_HOST']
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns the
|
41
|
+
def path
|
42
|
+
base = "#{request.env['REQUEST_URI']}".scan(/\w+/).first
|
43
|
+
return base.blank? ? "index" : base
|
44
|
+
end
|
45
|
+
|
46
|
+
# Creates a complete link including the hostname
|
47
|
+
def full_hostname(link = "")
|
48
|
+
"http://#{hostname}#{link}"
|
49
|
+
end
|
50
|
+
|
51
|
+
# Display the page titles in proper format
|
52
|
+
def page_title
|
53
|
+
title = @page_title ? "| #{@page_title}" : ""
|
54
|
+
return "#{Aerial.config.title} #{title}"
|
55
|
+
end
|
56
|
+
|
57
|
+
# Format just the DATE in a nice easy to read format
|
58
|
+
def humanized_date(date)
|
59
|
+
if date && date.respond_to?(:strftime)
|
60
|
+
date.strftime('%A %B, %d %Y').strip
|
61
|
+
else
|
62
|
+
'Never'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Format just the DATE in a short way
|
67
|
+
def short_date(date)
|
68
|
+
if date && date.respond_to?(:strftime)
|
69
|
+
date.strftime('%b %d').strip
|
70
|
+
else
|
71
|
+
'Never'
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Format for the rss 2.0 feed
|
76
|
+
def rss_date(date)
|
77
|
+
date.strftime("%a, %d %b %Y %H:%M:%S %Z") #Tue, 03 Jun 2003 09:39:21 GMT
|
78
|
+
end
|
79
|
+
|
80
|
+
# Truncate a string
|
81
|
+
def blurb(text, options ={})
|
82
|
+
options.merge!(:length => 160, :omission => "...")
|
83
|
+
if text
|
84
|
+
l = options[:length] - options[:omission].length
|
85
|
+
chars = text
|
86
|
+
(chars.length > options[:length] ? chars[0...l] + options[:omission] : text).to_s
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Handy method to render partials including collections
|
91
|
+
def partial(template, options = {})
|
92
|
+
options.merge!(:layout => false)
|
93
|
+
return if options.has_key?(:collection) && options[:collection].nil?
|
94
|
+
|
95
|
+
if collection = options.delete(:collection) then
|
96
|
+
collection.inject([]) do |buffer, member|
|
97
|
+
buffer << haml(template, options.merge(:layout => false,
|
98
|
+
:locals => {template.to_sym => member}))
|
99
|
+
end.join("\n")
|
100
|
+
else
|
101
|
+
haml(template, options)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Author link
|
106
|
+
def link_to_author(comment)
|
107
|
+
unless comment.homepage.blank?
|
108
|
+
return "<a href='#{comment.homepage}' rel='external'>#{comment.author}</a>"
|
109
|
+
end
|
110
|
+
comment.author
|
111
|
+
end
|
112
|
+
|
113
|
+
# Create a list of hyperlinks with a set of tags
|
114
|
+
def link_to_tags(tags)
|
115
|
+
return unless tags
|
116
|
+
links = []
|
117
|
+
tags.each do |tag|
|
118
|
+
links << "<a href='/tags/#{tag}' rel='#{tag}'>#{tag}</a>"
|
119
|
+
end
|
120
|
+
links.join(", ")
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
# Provides a few methods for interacting with that Aerial repository
|
126
|
+
class Git
|
127
|
+
|
128
|
+
# Commit the new file and push it to the remote repository
|
129
|
+
def self.commit_and_push(path, message)
|
130
|
+
self.commit(path, message)
|
131
|
+
self.push
|
132
|
+
end
|
133
|
+
|
134
|
+
# Added the file in the path and commit the changs to the repo
|
135
|
+
# +path+ to the new file to commit
|
136
|
+
# +message+ description of the commit
|
137
|
+
def self.commit(path, message)
|
138
|
+
Dir.chdir(File.expand_path(Aerial.repo.working_dir)) do
|
139
|
+
Aerial.repo.add(path)
|
140
|
+
end
|
141
|
+
Aerial.repo.commit_index(message)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Adds all untracked files and commits them to the repo
|
145
|
+
def self.commit_all(path = ".", message = "Commited all changes at: #{DateTime.now}")
|
146
|
+
unless Aerial.repo.status.untracked.empty?
|
147
|
+
self.commit(path, message)
|
148
|
+
end
|
149
|
+
true
|
150
|
+
end
|
151
|
+
|
152
|
+
# Upload all new commits to the remote repo (if exists)
|
153
|
+
def self.push
|
154
|
+
return unless Aerial.config.git.name && Aerial.config.git.branch
|
155
|
+
|
156
|
+
begin
|
157
|
+
cmd = "push #{Aerial.config.git.name} #{Aerial.config.git.branch} "
|
158
|
+
Aerial.repo.git.run('', cmd, '', {}, "")
|
159
|
+
rescue Exception => e
|
160
|
+
Aerial.log(e.message)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|
167
|
+
|
168
|
+
class Object
|
169
|
+
def blank?
|
170
|
+
respond_to?(:empty?) ? empty? : !self
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
module Aerial
|
2
|
+
|
3
|
+
# Anonymous feedback
|
4
|
+
class Comment < Content
|
5
|
+
|
6
|
+
attr_reader :id, :permalink, :article, :spam, :file_path
|
7
|
+
attr_accessor :archive_name, :spam, :published_at, :name, :referrer,
|
8
|
+
:email, :homepage, :user_ip, :user_agent, :file_name
|
9
|
+
|
10
|
+
def initialize(atts = {})
|
11
|
+
super
|
12
|
+
sanitize_url
|
13
|
+
end
|
14
|
+
|
15
|
+
# =============================================================================================
|
16
|
+
# PUBLIC CLASS METHODS
|
17
|
+
# =============================================================================================
|
18
|
+
|
19
|
+
# Create a new instance and write comment to disk
|
20
|
+
# +path+ the file location of the comment
|
21
|
+
def self.create(archive_name, attributes ={})
|
22
|
+
comment = Comment.new(attributes.merge(:archive_name => archive_name))
|
23
|
+
if comment.valid?
|
24
|
+
return self.save_new(comment)
|
25
|
+
end
|
26
|
+
false
|
27
|
+
end
|
28
|
+
|
29
|
+
# Open and existing comment
|
30
|
+
# +data+ contains info about the comment
|
31
|
+
def self.open(data, options={})
|
32
|
+
self.new( self.extract_comment_from(data, options) )
|
33
|
+
end
|
34
|
+
|
35
|
+
# =============================================================================================
|
36
|
+
# PUBLIC INSTANCE METHODS
|
37
|
+
# =============================================================================================
|
38
|
+
|
39
|
+
# Save the instance to disk
|
40
|
+
# +archive_name+ the parent directory of the article, we're forcing the parameter to ensure
|
41
|
+
# the archive_name is established before attemping to write it to disk
|
42
|
+
def save(archive_name)
|
43
|
+
self.archive_name = archive_name
|
44
|
+
if File.directory? self.archive_path
|
45
|
+
Comment.save_new(self)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Absolute path of the comment file
|
50
|
+
def expand_file
|
51
|
+
return unless self.archive_name
|
52
|
+
File.join(self.archive_path, self.name)
|
53
|
+
end
|
54
|
+
|
55
|
+
# The absolute file path of the archive
|
56
|
+
def archive_path
|
57
|
+
File.join(Aerial.repo.working_dir, Aerial.config.articles.dir, self.archive_name)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Make sure comment has the required data
|
61
|
+
def valid?
|
62
|
+
return false if self.email.blank? || self.author.blank? || self.body.blank?
|
63
|
+
true
|
64
|
+
end
|
65
|
+
|
66
|
+
# Ask Akismetor if comment is spam
|
67
|
+
def suspicious?
|
68
|
+
return self.spam if self.spam
|
69
|
+
self.spam = Akismetor.spam?(akismet_attributes)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Create a unique file name for this comment
|
73
|
+
def generate_name!
|
74
|
+
return self.name unless self.name.nil?
|
75
|
+
|
76
|
+
extenstion = self.suspicious? ? "spam" : "comment"
|
77
|
+
self.name = "#{DateTime.now.strftime("%Y%m%d%H%d%S")}_#{self.email}.#{extenstion}"
|
78
|
+
end
|
79
|
+
|
80
|
+
# String representation
|
81
|
+
def to_s
|
82
|
+
me = ""
|
83
|
+
me << "Author: #{self.author} \n" if self.author
|
84
|
+
me << "Published: #{self.published_at} \n" if self.published_at.to_s
|
85
|
+
me << "Email: #{self.email} \n" if self.email
|
86
|
+
me << "Homepage: #{self.homepage} \n" if self.homepage
|
87
|
+
me << "User IP: #{self.user_ip} \n" if self.user_ip
|
88
|
+
me << "User Agent: #{self.user_agent} \n" if self.user_agent
|
89
|
+
me << "Spam?: #{self.spam} \n" if self.user_agent
|
90
|
+
me << "\n#{self.body}" if self.body
|
91
|
+
return me
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
# =============================================================================================
|
97
|
+
# PRIVATE CLASS METHODS
|
98
|
+
# =============================================================================================
|
99
|
+
|
100
|
+
# Create a new Comment instance with data from the given file
|
101
|
+
def self.extract_comment_from(data, options = {})
|
102
|
+
comment_file = data.to_s
|
103
|
+
comment = options
|
104
|
+
comment[:id] = self.extract_header("id", comment_file)
|
105
|
+
comment[:user_ip] = self.extract_header("ip", comment_file)
|
106
|
+
comment[:user_agent] = self.extract_header("user-agent", comment_file)
|
107
|
+
comment[:referrer] = self.extract_header("referrer", comment_file)
|
108
|
+
comment[:permalink] = self.extract_header("permalink", comment_file)
|
109
|
+
comment[:author] = self.extract_header("author", comment_file)
|
110
|
+
comment[:email] = self.extract_header("email", comment_file)
|
111
|
+
comment[:homepage] = self.extract_header("homepage", comment_file)
|
112
|
+
comment[:published_at]= DateTime.parse(self.extract_header("published", comment_file))
|
113
|
+
comment[:body] = self.scan_for_field(comment_file, self.body_field)
|
114
|
+
return comment
|
115
|
+
end
|
116
|
+
|
117
|
+
# Write the contents of the comment to the same directory as the article
|
118
|
+
def self.save_new(comment)
|
119
|
+
return false unless comment && comment.archive_name
|
120
|
+
comment.generate_name!
|
121
|
+
comment.published_at = DateTime.now
|
122
|
+
path = File.join(Aerial.config.articles.dir, comment.archive_name, comment.name)
|
123
|
+
Dir.chdir(Aerial.repo.working_dir) do
|
124
|
+
File.open(path, 'w') do |file|
|
125
|
+
file << comment.to_s
|
126
|
+
end
|
127
|
+
end
|
128
|
+
Aerial::Git.commit_and_push(path, "New comment: #{comment.name}")
|
129
|
+
return comment
|
130
|
+
end
|
131
|
+
|
132
|
+
# =============================================================================================
|
133
|
+
# PRIVATE INSTANCE METHODS
|
134
|
+
# =============================================================================================
|
135
|
+
|
136
|
+
# Make sure the url is cleaned
|
137
|
+
def sanitize_url
|
138
|
+
return unless self.homepage
|
139
|
+
homepage.gsub!(/^(.*)/, 'http://\1') unless homepage =~ %r{^http://} or homepage.empty?
|
140
|
+
end
|
141
|
+
|
142
|
+
# Try to prevent spam with akismet
|
143
|
+
def akismet_attributes
|
144
|
+
{
|
145
|
+
:key => Aerial.config.akismet.key,
|
146
|
+
:blog => Aerial.config.akismet.url,
|
147
|
+
:user_ip => self.user_ip,
|
148
|
+
:user_agent => self.user_agent,
|
149
|
+
:referrer => self.referrer,
|
150
|
+
:permalink => self.permalink,
|
151
|
+
:comment_type => 'comment',
|
152
|
+
:comment_author => self.author,
|
153
|
+
:comment_author_email => self.email,
|
154
|
+
:comment_author_url => self.homepage,
|
155
|
+
:comment_content => self.body
|
156
|
+
}
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Aerial
|
2
|
+
|
3
|
+
class Config
|
4
|
+
|
5
|
+
class << self
|
6
|
+
attr_accessor :config
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(yaml)
|
10
|
+
@config = nested_hash_to_openstruct(yaml)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Complete path to the directory theme
|
14
|
+
def theme_directory
|
15
|
+
directory = File.join(AERIAL_ROOT, self.views.dir)
|
16
|
+
File.join(directory)
|
17
|
+
end
|
18
|
+
|
19
|
+
def method_missing(method_name, *attributes)
|
20
|
+
if @config.respond_to?(method_name.to_sym)
|
21
|
+
return @config.send(method_name.to_sym)
|
22
|
+
else
|
23
|
+
false
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# Recursively convert nested Hashes into Openstructs
|
30
|
+
def nested_hash_to_openstruct(obj)
|
31
|
+
if obj.is_a? Hash
|
32
|
+
obj.each { |key, value| obj[key] = nested_hash_to_openstruct(value) }
|
33
|
+
OpenStruct.new(obj)
|
34
|
+
else
|
35
|
+
return obj
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'coderay'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
module Aerial
|
5
|
+
|
6
|
+
# Base class for all the site's content
|
7
|
+
class Content
|
8
|
+
|
9
|
+
attr_reader :id, :author, :title, :body, :published_at, :archive_name, :file_name
|
10
|
+
|
11
|
+
def initialize(atts = {})
|
12
|
+
atts.each_pair { |key, value| instance_variable_set("@#{key}", value) if self.respond_to? key}
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
# =============================================================================================
|
18
|
+
# PROTECTED CLASS METHODS
|
19
|
+
# =============================================================================================
|
20
|
+
|
21
|
+
# With the comment string, attempt to find the given field
|
22
|
+
# +field+ the label before the ":"
|
23
|
+
# +comment+ is the contents of the comment
|
24
|
+
def self.extract_header(field, content)
|
25
|
+
return self.scan_for_field(content, self.header_field_for(field))
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the string that matches the given pattern
|
29
|
+
def self.scan_for_field(contents, pattern)
|
30
|
+
content = contents.scan(pattern).first.to_s.strip
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns the regular expression pattern for the header fields
|
34
|
+
def self.header_field_for(header)
|
35
|
+
exp = Regexp.new('^'+header+'\s*:(.*)$', Regexp::IGNORECASE)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns the regular expression pattern for the body field
|
39
|
+
def self.body_field
|
40
|
+
exp = Regexp.new('^\n(.*)$', Regexp::MULTILINE)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Look for <code> blocks and convert it for syntax highlighting
|
44
|
+
def self.parse_coderay(text)
|
45
|
+
text.scan(/(\<code>(.+?)\<\/code>)/m).each do |match|
|
46
|
+
match[1] = match[1].gsub("<br>", "\n").
|
47
|
+
gsub("&nbsp;", " ").
|
48
|
+
gsub("&lt;", "<").
|
49
|
+
gsub("&gt;", ">").
|
50
|
+
gsub("&quot;", '"')
|
51
|
+
text.gsub!(match[0], CodeRay.scan(match[1].strip, :ruby).div(:line_numbers => :table, :css => :class))
|
52
|
+
end
|
53
|
+
return text
|
54
|
+
end
|
55
|
+
|
56
|
+
# =============================================================================================
|
57
|
+
# PROTECTED INSTANCE METHODS
|
58
|
+
# =============================================================================================
|
59
|
+
|
60
|
+
# Ensure string contains valid ASCII characters
|
61
|
+
def escape(string)
|
62
|
+
return unless string
|
63
|
+
result = String.new(string)
|
64
|
+
result.gsub!(/[^\x00-\x7F]+/, '') # Remove anything non-ASCII entirely (e.g. diacritics).
|
65
|
+
result.gsub!(/[^\w_ \-]+/i, '') # Remove unwanted chars.
|
66
|
+
result.gsub!(/[ \-]+/i, '-') # No more than one of the separator in a row.
|
67
|
+
result.gsub!(/^\-|\-$/i, '') # Remove leading/trailing separator.
|
68
|
+
result.downcase!
|
69
|
+
return result
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|