serum 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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