serum 0.1.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/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
|