serum 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +2 -0
- data/LICENSE +23 -0
- data/README.md +55 -0
- data/Rakefile +10 -0
- data/lib/serum.rb +71 -0
- data/lib/serum/core_ext.rb +68 -0
- data/lib/serum/errors.rb +4 -0
- data/lib/serum/mime.types +84 -0
- data/lib/serum/post.rb +177 -0
- data/lib/serum/site.rb +116 -0
- data/lib/serum/static_file.rb +53 -0
- data/serum.gemspec +85 -0
- data/test/helper.rb +26 -0
- data/test/source/2008-02-02-not-published.textile +8 -0
- data/test/source/2008-02-02-published.textile +8 -0
- data/test/source/2008-10-18-foo-bar.textile +8 -0
- data/test/source/2008-11-21-complex.textile +8 -0
- data/test/source/2008-12-03-permalinked-post.textile +9 -0
- data/test/source/2008-12-13-include.markdown +8 -0
- data/test/source/2009-01-27-array-categories.textile +10 -0
- data/test/source/2009-01-27-categories.textile +7 -0
- data/test/source/2009-01-27-category.textile +7 -0
- data/test/source/2009-01-27-empty-categories.textile +7 -0
- data/test/source/2009-01-27-empty-category.textile +7 -0
- data/test/source/2009-03-12-hash-#1.markdown +6 -0
- data/test/source/2009-05-18-empty-tag.textile +6 -0
- data/test/source/2009-05-18-empty-tags.textile +6 -0
- data/test/source/2009-05-18-tag.textile +6 -0
- data/test/source/2009-05-18-tags.textile +9 -0
- data/test/source/2009-05-24-yaml-linebreak.markdown +7 -0
- data/test/source/2009-06-22-empty-yaml.textile +3 -0
- data/test/source/2009-06-22-no-yaml.textile +1 -0
- data/test/source/2010-01-08-triple-dash.markdown +6 -0
- data/test/source/2010-01-09-date-override.textile +7 -0
- data/test/source/2010-01-09-time-override.textile +7 -0
- data/test/source/2010-01-09-timezone-override.textile +7 -0
- data/test/source/2010-01-16-override-data.textile +4 -0
- data/test/source/2011-04-12-md-extension.md +7 -0
- data/test/source/2011-04-12-text-extension.text +0 -0
- data/test/source/2013-01-02-post-excerpt.markdown +14 -0
- data/test/source/2013-01-12-nil-layout.textile +6 -0
- data/test/source/2013-01-12-no-layout.textile +5 -0
- data/test/suite.rb +11 -0
- data/test/test_core_ext.rb +88 -0
- data/test/test_post.rb +138 -0
- data/test/test_site.rb +68 -0
- metadata +194 -0
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
(The MIT License)
|
2
|
+
|
3
|
+
Copyright (c) 2013 Brad Fults
|
4
|
+
|
5
|
+
Jekyll Copyright (c) 2008 Tom Preston-Werner
|
6
|
+
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
8
|
+
of this software and associated documentation files (the 'Software'), to deal
|
9
|
+
in the Software without restriction, including without limitation the rights
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
12
|
+
furnished to do so, subject to the following conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
15
|
+
copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
23
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# Serum
|
2
|
+
|
3
|
+
Serum is a simple object model on static posts with YAML front matter.
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
Instantiate Serum passing in a directory of files that may or may not have
|
8
|
+
YAML front matter:
|
9
|
+
|
10
|
+
```irb
|
11
|
+
>> site = Serum.for_dir("posts/")
|
12
|
+
=> <Site: /Users/bob/posts>
|
13
|
+
```
|
14
|
+
|
15
|
+
Then ask questions about the posts:
|
16
|
+
|
17
|
+
```irb
|
18
|
+
>> site.posts.size
|
19
|
+
=> 28
|
20
|
+
|
21
|
+
>> site.posts.first
|
22
|
+
=> <Post: /published>
|
23
|
+
|
24
|
+
>> site.posts.first.next
|
25
|
+
=> <Post: /foo-bar>
|
26
|
+
```
|
27
|
+
|
28
|
+
You can also pass in a `'baseurl'` option to `for_dir` in order to get URL
|
29
|
+
generation on each of the posts:
|
30
|
+
|
31
|
+
```irb
|
32
|
+
>> site = Serum.for_dir('posts/', {'baseurl' => '/story'})
|
33
|
+
=> <Site: /Users/bob/posts>
|
34
|
+
|
35
|
+
>> site.posts.first.url
|
36
|
+
=> "/story/published"
|
37
|
+
```
|
38
|
+
|
39
|
+
It's really that simple. Sometimes you just want a Ruby object model on top of
|
40
|
+
a simple directory of posts.
|
41
|
+
|
42
|
+
## Author
|
43
|
+
|
44
|
+
Maybe more aptly named "deleter" considering this project's origin.
|
45
|
+
|
46
|
+
[Brad Fults](http://github.com/h3h) ([bfults@gmail.com][])
|
47
|
+
|
48
|
+
## Acknowledgements
|
49
|
+
|
50
|
+
Serum is based entirely on [Jekyll](http://jekyllrb.com/) from Tomm Preston-Werner.
|
51
|
+
Thank you, Tom.
|
52
|
+
|
53
|
+
## License
|
54
|
+
|
55
|
+
MIT License; see `LICENSE` file.
|
data/Rakefile
ADDED
data/lib/serum.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__) # For use/testing when no gem is installed
|
2
|
+
|
3
|
+
# Require all of the Ruby files in the given directory.
|
4
|
+
#
|
5
|
+
# path - The String relative path from here to the directory.
|
6
|
+
#
|
7
|
+
# Returns nothing.
|
8
|
+
def require_all(path)
|
9
|
+
glob = File.join(File.dirname(__FILE__), path, '*.rb')
|
10
|
+
Dir[glob].each do |f|
|
11
|
+
require f
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# rubygems
|
16
|
+
require 'rubygems'
|
17
|
+
|
18
|
+
# stdlib
|
19
|
+
require 'fileutils'
|
20
|
+
require 'time'
|
21
|
+
require 'safe_yaml'
|
22
|
+
require 'English'
|
23
|
+
|
24
|
+
# internal requires
|
25
|
+
require 'serum/core_ext'
|
26
|
+
require 'serum/site'
|
27
|
+
require 'serum/post'
|
28
|
+
require 'serum/static_file'
|
29
|
+
require 'serum/errors'
|
30
|
+
|
31
|
+
SafeYAML::OPTIONS[:suppress_warnings] = true
|
32
|
+
|
33
|
+
module Serum
|
34
|
+
VERSION = '0.1.0'
|
35
|
+
|
36
|
+
# Default options.
|
37
|
+
# Strings rather than symbols are used for compatability with YAML.
|
38
|
+
DEFAULTS = {
|
39
|
+
'source' => Dir.pwd,
|
40
|
+
'permalink' => ':title',
|
41
|
+
'baseurl' => '',
|
42
|
+
'include' => ['.htaccess'],
|
43
|
+
}
|
44
|
+
|
45
|
+
# Public: Generate a Serum configuration Hash by merging the default
|
46
|
+
# options with anything in _config.yml, and adding the given options on top.
|
47
|
+
#
|
48
|
+
# override - A Hash of config directives that override any options in both
|
49
|
+
# the defaults and the config file. See Serum::DEFAULTS for a
|
50
|
+
# list of option names and their defaults.
|
51
|
+
#
|
52
|
+
# Returns the final configuration Hash.
|
53
|
+
def self.configuration(override)
|
54
|
+
# Convert any symbol keys to strings and remove the old key/values
|
55
|
+
override = override.reduce({}) { |hsh,(k,v)| hsh.merge(k.to_s => v) }
|
56
|
+
|
57
|
+
# Merge DEFAULTS < override
|
58
|
+
Serum::DEFAULTS.deep_merge(override)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Public: Generate a new Serum::Site for the given directory.
|
62
|
+
#
|
63
|
+
# source - A String path to the directory.
|
64
|
+
# opts - A Hash of additional configuration options.
|
65
|
+
#
|
66
|
+
# Returns the Serum::Site.
|
67
|
+
def self.for_dir(source, opts={})
|
68
|
+
Serum::Site.new(configuration({source: source}.merge(opts)))
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
class Hash
|
2
|
+
# Merges self with another hash, recursively.
|
3
|
+
#
|
4
|
+
# This code was lovingly stolen from some random gem:
|
5
|
+
# http://gemjack.com/gems/tartan-0.1.1/classes/Hash.html
|
6
|
+
#
|
7
|
+
# Thanks to whoever made it.
|
8
|
+
def deep_merge(hash)
|
9
|
+
target = dup
|
10
|
+
|
11
|
+
hash.keys.each do |key|
|
12
|
+
if hash[key].is_a? Hash and self[key].is_a? Hash
|
13
|
+
target[key] = target[key].deep_merge(hash[key])
|
14
|
+
next
|
15
|
+
end
|
16
|
+
|
17
|
+
target[key] = hash[key]
|
18
|
+
end
|
19
|
+
|
20
|
+
target
|
21
|
+
end
|
22
|
+
|
23
|
+
# Read array from the supplied hash favouring the singular key
|
24
|
+
# and then the plural key, and handling any nil entries.
|
25
|
+
# +hash+ the hash to read from
|
26
|
+
# +singular_key+ the singular key
|
27
|
+
# +plural_key+ the plural key
|
28
|
+
#
|
29
|
+
# Returns an array
|
30
|
+
def pluralized_array(singular_key, plural_key)
|
31
|
+
hash = self
|
32
|
+
if hash.has_key?(singular_key)
|
33
|
+
array = [hash[singular_key]] if hash[singular_key]
|
34
|
+
elsif hash.has_key?(plural_key)
|
35
|
+
case hash[plural_key]
|
36
|
+
when String
|
37
|
+
array = hash[plural_key].split
|
38
|
+
when Array
|
39
|
+
array = hash[plural_key].compact
|
40
|
+
end
|
41
|
+
end
|
42
|
+
array || []
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Thanks, ActiveSupport!
|
47
|
+
class Date
|
48
|
+
# Converts datetime to an appropriate format for use in XML
|
49
|
+
def xmlschema
|
50
|
+
strftime("%Y-%m-%dT%H:%M:%S%Z")
|
51
|
+
end if RUBY_VERSION < '1.9'
|
52
|
+
end
|
53
|
+
|
54
|
+
module Enumerable
|
55
|
+
# Returns true if path matches against any glob pattern.
|
56
|
+
# Look for more detail about glob pattern in method File::fnmatch.
|
57
|
+
def glob_include?(e)
|
58
|
+
any? { |exp| File.fnmatch?(exp, e) }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
if RUBY_VERSION < "1.9"
|
63
|
+
class String
|
64
|
+
def force_encoding(enc)
|
65
|
+
self
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/serum/errors.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# These are the same MIME types that GitHub Pages uses as of 17 Mar 2013.
|
2
|
+
|
3
|
+
text/html html htm shtml
|
4
|
+
text/css css
|
5
|
+
text/xml xml rss xsl
|
6
|
+
image/gif gif
|
7
|
+
image/jpeg jpeg jpg
|
8
|
+
application/x-javascript js
|
9
|
+
application/atom+xml atom
|
10
|
+
|
11
|
+
text/mathml mml
|
12
|
+
text/plain txt
|
13
|
+
text/vnd.sun.j2me.app-descriptor jad
|
14
|
+
text/vnd.wap.wml wml
|
15
|
+
text/x-component htc
|
16
|
+
text/cache-manifest manifest appcache
|
17
|
+
text/coffeescript coffee
|
18
|
+
text/plain pde
|
19
|
+
text/plain md markdown
|
20
|
+
|
21
|
+
image/png png
|
22
|
+
image/svg+xml svg
|
23
|
+
image/tiff tif tiff
|
24
|
+
image/vnd.wap.wbmp wbmp
|
25
|
+
image/x-icon ico
|
26
|
+
image/x-jng jng
|
27
|
+
image/x-ms-bmp bmp
|
28
|
+
|
29
|
+
application/json json
|
30
|
+
application/java-archive jar ear
|
31
|
+
application/mac-binhex40 hqx
|
32
|
+
application/msword doc
|
33
|
+
application/pdf pdf
|
34
|
+
application/postscript ps eps ai
|
35
|
+
application/rdf+xml rdf
|
36
|
+
application/rtf rtf
|
37
|
+
text/vcard vcf vcard
|
38
|
+
application/vnd.ms-excel xls
|
39
|
+
application/vnd.ms-powerpoint ppt
|
40
|
+
application/vnd.wap.wmlc wmlc
|
41
|
+
application/xhtml+xml xhtml
|
42
|
+
application/x-chrome-extension crx
|
43
|
+
application/x-cocoa cco
|
44
|
+
application/x-font-ttf ttf
|
45
|
+
application/x-java-archive-diff jardiff
|
46
|
+
application/x-java-jnlp-file jnlp
|
47
|
+
application/x-makeself run
|
48
|
+
application/x-ns-proxy-autoconfig pac
|
49
|
+
application/x-perl pl pm
|
50
|
+
application/x-pilot prc pdb
|
51
|
+
application/x-rar-compressed rar
|
52
|
+
application/x-redhat-package-manager rpm
|
53
|
+
application/x-sea sea
|
54
|
+
application/x-shockwave-flash swf
|
55
|
+
application/x-stuffit sit
|
56
|
+
application/x-tcl tcl tk
|
57
|
+
application/x-web-app-manifest+json webapp
|
58
|
+
application/x-x509-ca-cert der pem crt
|
59
|
+
application/x-xpinstall xpi
|
60
|
+
application/x-zip war
|
61
|
+
application/zip zip
|
62
|
+
|
63
|
+
application/octet-stream bin exe dll
|
64
|
+
application/octet-stream deb
|
65
|
+
application/octet-stream dmg
|
66
|
+
application/octet-stream eot
|
67
|
+
application/octet-stream iso img
|
68
|
+
application/octet-stream msi msp msm
|
69
|
+
|
70
|
+
audio/midi mid midi kar
|
71
|
+
audio/mpeg mp3
|
72
|
+
audio/x-realaudio ra
|
73
|
+
audio/ogg ogg
|
74
|
+
|
75
|
+
video/3gpp 3gpp 3gp
|
76
|
+
video/mpeg mpeg mpg
|
77
|
+
video/quicktime mov
|
78
|
+
video/x-flv flv
|
79
|
+
video/x-mng mng
|
80
|
+
video/x-ms-asf asx asf
|
81
|
+
video/x-ms-wmv wmv
|
82
|
+
video/x-msvideo avi
|
83
|
+
video/ogg ogv
|
84
|
+
video/webm webm
|
data/lib/serum/post.rb
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module Serum
|
4
|
+
class Post
|
5
|
+
include Comparable
|
6
|
+
|
7
|
+
# Valid post name regex.
|
8
|
+
MATCHER = %r{
|
9
|
+
^(.+\/)* # zero or more path segments including their trailing slash
|
10
|
+
(\d+-\d+-\d+) # three numbers (YYYY-mm-dd) separated by hyphens for the date
|
11
|
+
-(.*) # a hyphen followed by the slug
|
12
|
+
(\.[^.]+)$ # the file extension after a .
|
13
|
+
}x
|
14
|
+
|
15
|
+
# Post name validator. Post filenames must be like:
|
16
|
+
# 2008-11-05-my-awesome-post.textile
|
17
|
+
#
|
18
|
+
# Returns true if valid, false if not.
|
19
|
+
def self.valid?(name)
|
20
|
+
name =~ MATCHER
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_accessor :site
|
24
|
+
attr_accessor :data, :content, :output, :ext
|
25
|
+
attr_accessor :date, :slug, :published, :dir
|
26
|
+
|
27
|
+
attr_reader :name
|
28
|
+
|
29
|
+
# Initialize this Post instance.
|
30
|
+
#
|
31
|
+
# site - The Site.
|
32
|
+
# base - The String path to the dir containing the post file.
|
33
|
+
# name - The String filename of the post file.
|
34
|
+
#
|
35
|
+
# Returns the new Post.
|
36
|
+
def initialize(site, source, dir, name)
|
37
|
+
@site = site
|
38
|
+
@base = source
|
39
|
+
@dir = dir
|
40
|
+
@name = name
|
41
|
+
|
42
|
+
self.process(name)
|
43
|
+
begin
|
44
|
+
self.read_yaml(@base, name)
|
45
|
+
rescue Exception => msg
|
46
|
+
raise FatalException.new("#{msg} in #{@base}/#{name}")
|
47
|
+
end
|
48
|
+
|
49
|
+
# If we've added a date and time to the YAML, use that instead of the
|
50
|
+
# filename date. Means we'll sort correctly.
|
51
|
+
if self.data.has_key?('date')
|
52
|
+
# ensure Time via to_s and reparse
|
53
|
+
self.date = Time.parse(self.data["date"].to_s)
|
54
|
+
end
|
55
|
+
|
56
|
+
if self.data.has_key?('published') && self.data['published'] == false
|
57
|
+
self.published = false
|
58
|
+
else
|
59
|
+
self.published = true
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Read the YAML frontmatter.
|
64
|
+
#
|
65
|
+
# base - The String path to the dir containing the file.
|
66
|
+
# name - The String filename of the file.
|
67
|
+
#
|
68
|
+
# Returns nothing.
|
69
|
+
def read_yaml(base, name)
|
70
|
+
begin
|
71
|
+
content = File.read(File.join(base, name))
|
72
|
+
|
73
|
+
if content =~ /\A(---\s*\n.*?\n?)^(---\s*$\n?)/m
|
74
|
+
self.data = YAML.safe_load($1)
|
75
|
+
end
|
76
|
+
rescue => e
|
77
|
+
puts "Error reading file #{File.join(base, name)}: #{e.message}"
|
78
|
+
rescue SyntaxError => e
|
79
|
+
puts "YAML Exception reading #{File.join(base, name)}: #{e.message}"
|
80
|
+
end
|
81
|
+
|
82
|
+
self.data ||= {}
|
83
|
+
end
|
84
|
+
|
85
|
+
# Compares Post objects. First compares the Post date. If the dates are
|
86
|
+
# equal, it compares the Post slugs.
|
87
|
+
#
|
88
|
+
# other - The other Post we are comparing to.
|
89
|
+
#
|
90
|
+
# Returns -1, 0, 1
|
91
|
+
def <=>(other)
|
92
|
+
cmp = self.date <=> other.date
|
93
|
+
if 0 == cmp
|
94
|
+
cmp = self.slug <=> other.slug
|
95
|
+
end
|
96
|
+
return cmp
|
97
|
+
end
|
98
|
+
|
99
|
+
# Extract information from the post filename.
|
100
|
+
#
|
101
|
+
# name - The String filename of the post file.
|
102
|
+
#
|
103
|
+
# Returns nothing.
|
104
|
+
def process(name)
|
105
|
+
m, cats, date, slug, ext = *name.match(MATCHER)
|
106
|
+
self.date = Time.parse(date)
|
107
|
+
self.slug = slug
|
108
|
+
self.ext = ext
|
109
|
+
rescue ArgumentError
|
110
|
+
raise FatalException.new("Post #{name} does not have a valid date.")
|
111
|
+
end
|
112
|
+
|
113
|
+
# The full path and filename of the post. Defined in the YAML of the post
|
114
|
+
# body (optional).
|
115
|
+
#
|
116
|
+
# Returns the String permalink.
|
117
|
+
def permalink
|
118
|
+
self.data && self.data['permalink']
|
119
|
+
end
|
120
|
+
|
121
|
+
# The generated relative url of this post.
|
122
|
+
# e.g. /2008/11/05/my-awesome-post.html
|
123
|
+
#
|
124
|
+
# Returns the String URL.
|
125
|
+
def url
|
126
|
+
return @url if @url
|
127
|
+
|
128
|
+
url = "#{self.site.baseurl}#{self.id}"
|
129
|
+
|
130
|
+
# sanitize url
|
131
|
+
@url = url.split('/').reject{ |part| part =~ /^\.+$/ }.join('/')
|
132
|
+
@url += "/" if url =~ /\/$/
|
133
|
+
@url
|
134
|
+
end
|
135
|
+
|
136
|
+
# The UID for this post (useful in feeds).
|
137
|
+
# e.g. /2008/11/05/my-awesome-post
|
138
|
+
#
|
139
|
+
# Returns the String UID.
|
140
|
+
def id
|
141
|
+
File.join(self.dir, self.slug)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Returns the shorthand String identifier of this Post.
|
145
|
+
def inspect
|
146
|
+
"<Post: #{self.id}>"
|
147
|
+
end
|
148
|
+
|
149
|
+
def next
|
150
|
+
pos = self.site.posts.index(self)
|
151
|
+
|
152
|
+
if pos && pos < self.site.posts.length-1
|
153
|
+
self.site.posts[pos+1]
|
154
|
+
else
|
155
|
+
nil
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def previous
|
160
|
+
pos = self.site.posts.index(self)
|
161
|
+
if pos && pos > 0
|
162
|
+
self.site.posts[pos-1]
|
163
|
+
else
|
164
|
+
nil
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def method_missing(meth, *args)
|
169
|
+
if self.data.has_key?(meth.to_s) && args.empty?
|
170
|
+
self.data[meth.to_s]
|
171
|
+
else
|
172
|
+
super
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
end
|