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.
Files changed (47) hide show
  1. data/Gemfile +2 -0
  2. data/LICENSE +23 -0
  3. data/README.md +55 -0
  4. data/Rakefile +10 -0
  5. data/lib/serum.rb +71 -0
  6. data/lib/serum/core_ext.rb +68 -0
  7. data/lib/serum/errors.rb +4 -0
  8. data/lib/serum/mime.types +84 -0
  9. data/lib/serum/post.rb +177 -0
  10. data/lib/serum/site.rb +116 -0
  11. data/lib/serum/static_file.rb +53 -0
  12. data/serum.gemspec +85 -0
  13. data/test/helper.rb +26 -0
  14. data/test/source/2008-02-02-not-published.textile +8 -0
  15. data/test/source/2008-02-02-published.textile +8 -0
  16. data/test/source/2008-10-18-foo-bar.textile +8 -0
  17. data/test/source/2008-11-21-complex.textile +8 -0
  18. data/test/source/2008-12-03-permalinked-post.textile +9 -0
  19. data/test/source/2008-12-13-include.markdown +8 -0
  20. data/test/source/2009-01-27-array-categories.textile +10 -0
  21. data/test/source/2009-01-27-categories.textile +7 -0
  22. data/test/source/2009-01-27-category.textile +7 -0
  23. data/test/source/2009-01-27-empty-categories.textile +7 -0
  24. data/test/source/2009-01-27-empty-category.textile +7 -0
  25. data/test/source/2009-03-12-hash-#1.markdown +6 -0
  26. data/test/source/2009-05-18-empty-tag.textile +6 -0
  27. data/test/source/2009-05-18-empty-tags.textile +6 -0
  28. data/test/source/2009-05-18-tag.textile +6 -0
  29. data/test/source/2009-05-18-tags.textile +9 -0
  30. data/test/source/2009-05-24-yaml-linebreak.markdown +7 -0
  31. data/test/source/2009-06-22-empty-yaml.textile +3 -0
  32. data/test/source/2009-06-22-no-yaml.textile +1 -0
  33. data/test/source/2010-01-08-triple-dash.markdown +6 -0
  34. data/test/source/2010-01-09-date-override.textile +7 -0
  35. data/test/source/2010-01-09-time-override.textile +7 -0
  36. data/test/source/2010-01-09-timezone-override.textile +7 -0
  37. data/test/source/2010-01-16-override-data.textile +4 -0
  38. data/test/source/2011-04-12-md-extension.md +7 -0
  39. data/test/source/2011-04-12-text-extension.text +0 -0
  40. data/test/source/2013-01-02-post-excerpt.markdown +14 -0
  41. data/test/source/2013-01-12-nil-layout.textile +6 -0
  42. data/test/source/2013-01-12-no-layout.textile +5 -0
  43. data/test/suite.rb +11 -0
  44. data/test/test_core_ext.rb +88 -0
  45. data/test/test_post.rb +138 -0
  46. data/test/test_site.rb +68 -0
  47. metadata +194 -0
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
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.
@@ -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.
@@ -0,0 +1,10 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+
4
+ task :default => :test
5
+
6
+ Rake::TestTask.new(:test) do |test|
7
+ test.libs << 'lib' << 'test'
8
+ test.pattern = 'test/**/test_*.rb'
9
+ test.verbose = true
10
+ end
@@ -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
@@ -0,0 +1,4 @@
1
+ module Serum
2
+ class FatalException < StandardError
3
+ end
4
+ end
@@ -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
@@ -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