monad 0.0.1
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/CONTRIBUTING.md +68 -0
- data/Gemfile +2 -0
- data/LICENSE +21 -0
- data/README.md +88 -0
- data/Rakefile +136 -0
- data/bin/monad +102 -0
- data/cucumber.yml +3 -0
- data/features/create_sites.feature +112 -0
- data/features/data_sources.feature +76 -0
- data/features/drafts.feature +25 -0
- data/features/embed_filters.feature +60 -0
- data/features/markdown.feature +30 -0
- data/features/pagination.feature +54 -0
- data/features/permalinks.feature +65 -0
- data/features/post_data.feature +214 -0
- data/features/site_configuration.feature +206 -0
- data/features/site_data.feature +101 -0
- data/features/step_definitions/monad_steps.rb +175 -0
- data/features/support/env.rb +25 -0
- data/lib/monad.rb +90 -0
- data/lib/monad/command.rb +27 -0
- data/lib/monad/commands/build.rb +64 -0
- data/lib/monad/commands/doctor.rb +29 -0
- data/lib/monad/commands/new.rb +50 -0
- data/lib/monad/commands/serve.rb +33 -0
- data/lib/monad/configuration.rb +183 -0
- data/lib/monad/converter.rb +48 -0
- data/lib/monad/converters/identity.rb +21 -0
- data/lib/monad/converters/markdown.rb +43 -0
- data/lib/monad/converters/markdown/kramdown_parser.rb +44 -0
- data/lib/monad/converters/markdown/maruku_parser.rb +47 -0
- data/lib/monad/converters/markdown/rdiscount_parser.rb +35 -0
- data/lib/monad/converters/markdown/redcarpet_parser.rb +70 -0
- data/lib/monad/converters/textile.rb +50 -0
- data/lib/monad/convertible.rb +152 -0
- data/lib/monad/core_ext.rb +68 -0
- data/lib/monad/deprecator.rb +32 -0
- data/lib/monad/draft.rb +35 -0
- data/lib/monad/drivers/json_driver.rb +39 -0
- data/lib/monad/drivers/yaml_driver.rb +23 -0
- data/lib/monad/errors.rb +4 -0
- data/lib/monad/filters.rb +154 -0
- data/lib/monad/generator.rb +4 -0
- data/lib/monad/generators/pagination.rb +143 -0
- data/lib/monad/layout.rb +42 -0
- data/lib/monad/logger.rb +54 -0
- data/lib/monad/mime.types +85 -0
- data/lib/monad/page.rb +163 -0
- data/lib/monad/plugin.rb +75 -0
- data/lib/monad/post.rb +377 -0
- data/lib/monad/site.rb +455 -0
- data/lib/monad/static_file.rb +70 -0
- data/lib/monad/tags/gist.rb +30 -0
- data/lib/monad/tags/highlight.rb +85 -0
- data/lib/monad/tags/include.rb +37 -0
- data/lib/monad/tags/post_url.rb +61 -0
- data/lib/site_template/.gitignore +1 -0
- data/lib/site_template/_config.yml +2 -0
- data/lib/site_template/_layouts/default.html +46 -0
- data/lib/site_template/_layouts/post.html +9 -0
- data/lib/site_template/_posts/0000-00-00-welcome-to-monad.markdown.erb +24 -0
- data/lib/site_template/css/main.css +165 -0
- data/lib/site_template/css/syntax.css +60 -0
- data/lib/site_template/index.html +13 -0
- data/monad.gemspec +197 -0
- data/script/bootstrap +2 -0
- data/test/fixtures/broken_front_matter1.erb +5 -0
- data/test/fixtures/broken_front_matter2.erb +4 -0
- data/test/fixtures/broken_front_matter3.erb +7 -0
- data/test/fixtures/exploit_front_matter.erb +4 -0
- data/test/fixtures/front_matter.erb +4 -0
- data/test/fixtures/members.yaml +7 -0
- data/test/helper.rb +62 -0
- data/test/source/.htaccess +8 -0
- data/test/source/_includes/sig.markdown +3 -0
- data/test/source/_layouts/default.html +27 -0
- data/test/source/_layouts/simple.html +1 -0
- data/test/source/_plugins/dummy.rb +8 -0
- data/test/source/_posts/2008-02-02-not-published.textile +8 -0
- data/test/source/_posts/2008-02-02-published.textile +8 -0
- data/test/source/_posts/2008-10-18-foo-bar.textile +8 -0
- data/test/source/_posts/2008-11-21-complex.textile +8 -0
- data/test/source/_posts/2008-12-03-permalinked-post.textile +9 -0
- data/test/source/_posts/2008-12-13-include.markdown +8 -0
- data/test/source/_posts/2009-01-27-array-categories.textile +10 -0
- data/test/source/_posts/2009-01-27-categories.textile +7 -0
- data/test/source/_posts/2009-01-27-category.textile +7 -0
- data/test/source/_posts/2009-01-27-empty-categories.textile +7 -0
- data/test/source/_posts/2009-01-27-empty-category.textile +7 -0
- data/test/source/_posts/2009-03-12-hash-#1.markdown +6 -0
- data/test/source/_posts/2009-05-18-empty-tag.textile +6 -0
- data/test/source/_posts/2009-05-18-empty-tags.textile +6 -0
- data/test/source/_posts/2009-05-18-tag.textile +6 -0
- data/test/source/_posts/2009-05-18-tags.textile +9 -0
- data/test/source/_posts/2009-06-22-empty-yaml.textile +3 -0
- data/test/source/_posts/2009-06-22-no-yaml.textile +1 -0
- data/test/source/_posts/2010-01-08-triple-dash.markdown +5 -0
- data/test/source/_posts/2010-01-09-date-override.textile +7 -0
- data/test/source/_posts/2010-01-09-time-override.textile +7 -0
- data/test/source/_posts/2010-01-09-timezone-override.textile +7 -0
- data/test/source/_posts/2010-01-16-override-data.textile +4 -0
- data/test/source/_posts/2011-04-12-md-extension.md +7 -0
- data/test/source/_posts/2011-04-12-text-extension.text +0 -0
- data/test/source/_posts/2013-01-02-post-excerpt.markdown +14 -0
- data/test/source/_posts/2013-01-12-nil-layout.textile +6 -0
- data/test/source/_posts/2013-01-12-no-layout.textile +5 -0
- data/test/source/_posts/2013-03-19-not-a-post.markdown/.gitkeep +0 -0
- data/test/source/_posts/2013-04-11-custom-excerpt.markdown +10 -0
- data/test/source/_posts/2013-05-10-number-category.textile +7 -0
- data/test/source/_posts/es/2008-11-21-nested.textile +8 -0
- data/test/source/about.html +6 -0
- data/test/source/category/_posts/2008-9-23-categories.textile +6 -0
- data/test/source/contacts.html +5 -0
- data/test/source/contacts/bar.html +5 -0
- data/test/source/contacts/index.html +5 -0
- data/test/source/css/screen.css +76 -0
- data/test/source/deal.with.dots.html +7 -0
- data/test/source/foo/_posts/bar/2008-12-12-topical-post.textile +8 -0
- data/test/source/index.html +22 -0
- data/test/source/sitemap.xml +32 -0
- data/test/source/symlink-test/symlinked-file +22 -0
- data/test/source/win/_posts/2009-05-24-yaml-linebreak.markdown +7 -0
- data/test/source/z_category/_posts/2008-9-23-categories.textile +6 -0
- data/test/suite.rb +11 -0
- data/test/test_command.rb +39 -0
- data/test/test_configuration.rb +137 -0
- data/test/test_convertible.rb +51 -0
- data/test/test_core_ext.rb +88 -0
- data/test/test_filters.rb +102 -0
- data/test/test_generated_site.rb +83 -0
- data/test/test_json_driver.rb +63 -0
- data/test/test_kramdown.rb +35 -0
- data/test/test_new_command.rb +104 -0
- data/test/test_page.rb +193 -0
- data/test/test_pager.rb +115 -0
- data/test/test_post.rb +573 -0
- data/test/test_rdiscount.rb +22 -0
- data/test/test_redcarpet.rb +61 -0
- data/test/test_redcloth.rb +86 -0
- data/test/test_site.rb +374 -0
- data/test/test_tags.rb +310 -0
- data/test/test_yaml_driver.rb +35 -0
- metadata +554 -0
|
@@ -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,32 @@
|
|
|
1
|
+
module Monad
|
|
2
|
+
class Deprecator
|
|
3
|
+
def self.process(args)
|
|
4
|
+
no_subcommand(args)
|
|
5
|
+
deprecation_message args, "--server", "The --server command has been replaced by the \
|
|
6
|
+
'serve' subcommand."
|
|
7
|
+
deprecation_message args, "--no-server", "To build Monad without launching a server, \
|
|
8
|
+
use the 'build' subcommand."
|
|
9
|
+
deprecation_message args, "--auto", "The switch '--auto' has been replaced with '--watch'."
|
|
10
|
+
deprecation_message args, "--no-auto", "To disable auto-replication, simply leave off \
|
|
11
|
+
the '--watch' switch."
|
|
12
|
+
deprecation_message args, "--pygments", "The 'pygments' setting can only be set in \
|
|
13
|
+
your config files."
|
|
14
|
+
deprecation_message args, "--paginate", "The 'paginate' setting can only be set in your \
|
|
15
|
+
config files."
|
|
16
|
+
deprecation_message args, "--url", "The 'url' setting can only be set in your config files."
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.no_subcommand(args)
|
|
20
|
+
if args.size > 0 && args.first =~ /^--/ && !%w[--help --version].include?(args.first)
|
|
21
|
+
Monad::Logger.error "Deprecation:", "Monad now uses subcommands instead of just \
|
|
22
|
+
switches. Run `monad help' to find out more."
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.deprecation_message(args, deprecated_argument, message)
|
|
27
|
+
if args.include?(deprecated_argument)
|
|
28
|
+
Monad::Logger.error "Deprecation:", message
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/monad/draft.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Monad
|
|
2
|
+
|
|
3
|
+
class Draft < Post
|
|
4
|
+
|
|
5
|
+
# Valid post name regex (no date)
|
|
6
|
+
MATCHER = /^(.*)(\.[^.]+)$/
|
|
7
|
+
|
|
8
|
+
# Draft name validator. Draft filenames must be like:
|
|
9
|
+
# my-awesome-post.textile
|
|
10
|
+
#
|
|
11
|
+
# Returns true if valid, false if not.
|
|
12
|
+
def self.valid?(name)
|
|
13
|
+
name =~ MATCHER
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Get the full path to the directory containing the draft files
|
|
17
|
+
def containing_dir(source, dir)
|
|
18
|
+
File.join(source, dir, '_drafts')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Extract information from the post filename.
|
|
22
|
+
#
|
|
23
|
+
# name - The String filename of the post file.
|
|
24
|
+
#
|
|
25
|
+
# Returns nothing.
|
|
26
|
+
def process(name)
|
|
27
|
+
m, slug, ext = *name.match(MATCHER)
|
|
28
|
+
self.date = File.mtime(File.join(@base, name))
|
|
29
|
+
self.slug = slug
|
|
30
|
+
self.ext = ext
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'uri'
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'net/https' # ruby 1.8.7 requires explicitly require net/https
|
|
5
|
+
|
|
6
|
+
module Monad
|
|
7
|
+
module Drivers
|
|
8
|
+
class JsonDriver
|
|
9
|
+
def initialize(options)
|
|
10
|
+
@url = options['url']
|
|
11
|
+
|
|
12
|
+
if !@url
|
|
13
|
+
raise FatalException.new "'url' must be specified for json data source: #{options['name']}."
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
if @url !~ URI::regexp || URI(@url).scheme !~ /^http|https$/
|
|
17
|
+
raise FatalException.new "incorrect json data source url: #{@url}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def load
|
|
22
|
+
uri = URI(@url)
|
|
23
|
+
|
|
24
|
+
if uri.scheme == 'https'
|
|
25
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
26
|
+
http.use_ssl = true
|
|
27
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
28
|
+
|
|
29
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
|
30
|
+
response = http.request(request).body
|
|
31
|
+
else
|
|
32
|
+
response = Net::HTTP.get(uri)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
JSON.parse(response)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require 'safe_yaml'
|
|
2
|
+
|
|
3
|
+
module Monad
|
|
4
|
+
module Drivers
|
|
5
|
+
class YamlDriver
|
|
6
|
+
def initialize(options)
|
|
7
|
+
@path = options['path']
|
|
8
|
+
|
|
9
|
+
if !@path
|
|
10
|
+
raise FatalException.new "'path' must be specified for yaml data source: #{options['name']}."
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
if !File.exists?(@path)
|
|
14
|
+
raise FatalException.new "the file '#{@path}' doesn't exist for data source '#{options['name']}'"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def load
|
|
19
|
+
YAML.safe_load_file(@path)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/monad/errors.rb
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
require 'uri'
|
|
2
|
+
|
|
3
|
+
module Monad
|
|
4
|
+
module Filters
|
|
5
|
+
# Convert a Textile string into HTML output.
|
|
6
|
+
#
|
|
7
|
+
# input - The Textile String to convert.
|
|
8
|
+
#
|
|
9
|
+
# Returns the HTML formatted String.
|
|
10
|
+
def textilize(input)
|
|
11
|
+
site = @context.registers[:site]
|
|
12
|
+
converter = site.getConverterImpl(Monad::Converters::Textile)
|
|
13
|
+
converter.convert(input)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Convert a Markdown string into HTML output.
|
|
17
|
+
#
|
|
18
|
+
# input - The Markdown String to convert.
|
|
19
|
+
#
|
|
20
|
+
# Returns the HTML formatted String.
|
|
21
|
+
def markdownify(input)
|
|
22
|
+
site = @context.registers[:site]
|
|
23
|
+
converter = site.getConverterImpl(Monad::Converters::Markdown)
|
|
24
|
+
converter.convert(input)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Format a date in short format e.g. "27 Jan 2011".
|
|
28
|
+
#
|
|
29
|
+
# date - the Time to format.
|
|
30
|
+
#
|
|
31
|
+
# Returns the formatting String.
|
|
32
|
+
def date_to_string(date)
|
|
33
|
+
time(date).strftime("%d %b %Y")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Format a date in long format e.g. "27 January 2011".
|
|
37
|
+
#
|
|
38
|
+
# date - The Time to format.
|
|
39
|
+
#
|
|
40
|
+
# Returns the formatted String.
|
|
41
|
+
def date_to_long_string(date)
|
|
42
|
+
time(date).strftime("%d %B %Y")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Format a date for use in XML.
|
|
46
|
+
#
|
|
47
|
+
# date - The Time to format.
|
|
48
|
+
#
|
|
49
|
+
# Examples
|
|
50
|
+
#
|
|
51
|
+
# date_to_xmlschema(Time.now)
|
|
52
|
+
# # => "2011-04-24T20:34:46+08:00"
|
|
53
|
+
#
|
|
54
|
+
# Returns the formatted String.
|
|
55
|
+
def date_to_xmlschema(date)
|
|
56
|
+
time(date).xmlschema
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Format a date according to RFC-822
|
|
60
|
+
#
|
|
61
|
+
# date - The Time to format.
|
|
62
|
+
#
|
|
63
|
+
# Examples
|
|
64
|
+
#
|
|
65
|
+
# date_to_rfc822(Time.now)
|
|
66
|
+
# # => "Sun, 24 Apr 2011 12:34:46 +0000"
|
|
67
|
+
#
|
|
68
|
+
# Returns the formatted String.
|
|
69
|
+
def date_to_rfc822(date)
|
|
70
|
+
time(date).rfc822
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# XML escape a string for use. Replaces any special characters with
|
|
74
|
+
# appropriate HTML entity replacements.
|
|
75
|
+
#
|
|
76
|
+
# input - The String to escape.
|
|
77
|
+
#
|
|
78
|
+
# Examples
|
|
79
|
+
#
|
|
80
|
+
# xml_escape('foo "bar" <baz>')
|
|
81
|
+
# # => "foo "bar" <baz>"
|
|
82
|
+
#
|
|
83
|
+
# Returns the escaped String.
|
|
84
|
+
def xml_escape(input)
|
|
85
|
+
CGI.escapeHTML(input)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# CGI escape a string for use in a URL. Replaces any special characters
|
|
89
|
+
# with appropriate %XX replacements.
|
|
90
|
+
#
|
|
91
|
+
# input - The String to escape.
|
|
92
|
+
#
|
|
93
|
+
# Examples
|
|
94
|
+
#
|
|
95
|
+
# cgi_escape('foo,bar;baz?')
|
|
96
|
+
# # => "foo%2Cbar%3Bbaz%3F"
|
|
97
|
+
#
|
|
98
|
+
# Returns the escaped String.
|
|
99
|
+
def cgi_escape(input)
|
|
100
|
+
CGI::escape(input)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def uri_escape(input)
|
|
104
|
+
URI.escape(input)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Count the number of words in the input string.
|
|
108
|
+
#
|
|
109
|
+
# input - The String on which to operate.
|
|
110
|
+
#
|
|
111
|
+
# Returns the Integer word count.
|
|
112
|
+
def number_of_words(input)
|
|
113
|
+
input.split.length
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Join an array of things into a string by separating with commes and the
|
|
117
|
+
# word "and" for the last one.
|
|
118
|
+
#
|
|
119
|
+
# array - The Array of Strings to join.
|
|
120
|
+
#
|
|
121
|
+
# Examples
|
|
122
|
+
#
|
|
123
|
+
# array_to_sentence_string(["apples", "oranges", "grapes"])
|
|
124
|
+
# # => "apples, oranges, and grapes"
|
|
125
|
+
#
|
|
126
|
+
# Returns the formatted String.
|
|
127
|
+
def array_to_sentence_string(array)
|
|
128
|
+
connector = "and"
|
|
129
|
+
case array.length
|
|
130
|
+
when 0
|
|
131
|
+
""
|
|
132
|
+
when 1
|
|
133
|
+
array[0].to_s
|
|
134
|
+
when 2
|
|
135
|
+
"#{array[0]} #{connector} #{array[1]}"
|
|
136
|
+
else
|
|
137
|
+
"#{array[0...-1].join(', ')}, #{connector} #{array[-1]}"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
def time(input)
|
|
143
|
+
case input
|
|
144
|
+
when Time
|
|
145
|
+
input
|
|
146
|
+
when String
|
|
147
|
+
Time.parse(input)
|
|
148
|
+
else
|
|
149
|
+
Monad::Logger.error "Invalid Date:", "'#{input}' is not a valid datetime."
|
|
150
|
+
exit(1)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
module Monad
|
|
2
|
+
module Generators
|
|
3
|
+
class Pagination < Generator
|
|
4
|
+
# This generator is safe from arbitrary code execution.
|
|
5
|
+
safe true
|
|
6
|
+
|
|
7
|
+
# Generate paginated pages if necessary.
|
|
8
|
+
#
|
|
9
|
+
# site - The Site.
|
|
10
|
+
#
|
|
11
|
+
# Returns nothing.
|
|
12
|
+
def generate(site)
|
|
13
|
+
site.pages.dup.each do |page|
|
|
14
|
+
paginate(site, page) if Pager.pagination_enabled?(site.config, page)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Paginates the blog's posts. Renders the index.html file into paginated
|
|
19
|
+
# directories, e.g.: page2/index.html, page3/index.html, etc and adds more
|
|
20
|
+
# site-wide data.
|
|
21
|
+
#
|
|
22
|
+
# site - The Site.
|
|
23
|
+
# page - The index.html Page that requires pagination.
|
|
24
|
+
#
|
|
25
|
+
# {"paginator" => { "page" => <Number>,
|
|
26
|
+
# "per_page" => <Number>,
|
|
27
|
+
# "posts" => [<Post>],
|
|
28
|
+
# "total_posts" => <Number>,
|
|
29
|
+
# "total_pages" => <Number>,
|
|
30
|
+
# "previous_page" => <Number>,
|
|
31
|
+
# "next_page" => <Number> }}
|
|
32
|
+
def paginate(site, page)
|
|
33
|
+
all_posts = site.site_payload['site']['posts']
|
|
34
|
+
pages = Pager.calculate_pages(all_posts, site.config['paginate'].to_i)
|
|
35
|
+
(1..pages).each do |num_page|
|
|
36
|
+
pager = Pager.new(site.config, num_page, all_posts, pages)
|
|
37
|
+
if num_page > 1
|
|
38
|
+
newpage = Page.new(site, site.source, page.dir, page.name)
|
|
39
|
+
newpage.pager = pager
|
|
40
|
+
newpage.dir = File.join(page.dir, Pager.paginate_path(site.config, num_page))
|
|
41
|
+
site.pages << newpage
|
|
42
|
+
else
|
|
43
|
+
page.pager = pager
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class Pager
|
|
52
|
+
attr_reader :page, :per_page, :posts, :total_posts, :total_pages,
|
|
53
|
+
:previous_page, :previous_page_path, :next_page, :next_page_path
|
|
54
|
+
|
|
55
|
+
# Calculate the number of pages.
|
|
56
|
+
#
|
|
57
|
+
# all_posts - The Array of all Posts.
|
|
58
|
+
# per_page - The Integer of entries per page.
|
|
59
|
+
#
|
|
60
|
+
# Returns the Integer number of pages.
|
|
61
|
+
def self.calculate_pages(all_posts, per_page)
|
|
62
|
+
(all_posts.size.to_f / per_page.to_i).ceil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Determine if pagination is enabled for a given file.
|
|
66
|
+
#
|
|
67
|
+
# config - The configuration Hash.
|
|
68
|
+
# page - The Monad::Page with which to paginate
|
|
69
|
+
#
|
|
70
|
+
# Returns true if pagination is enabled, false otherwise.
|
|
71
|
+
def self.pagination_enabled?(config, page)
|
|
72
|
+
!config['paginate'].nil? &&
|
|
73
|
+
page.name == 'index.html' &&
|
|
74
|
+
subdirectories_identical(config['paginate_path'], page.dir)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Determine if the subdirectories of the two paths are the same relative to source
|
|
78
|
+
#
|
|
79
|
+
# paginate_path - the paginate_path configuration setting
|
|
80
|
+
# page_dir - the directory of the Monad::Page
|
|
81
|
+
#
|
|
82
|
+
# Returns whether the subdirectories are the same relative to source
|
|
83
|
+
def self.subdirectories_identical(paginate_path, page_dir)
|
|
84
|
+
File.dirname(paginate_path).gsub(/\A\./, '') == page_dir.gsub(/\/\z/, '')
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Static: Return the pagination path of the page
|
|
88
|
+
#
|
|
89
|
+
# site_config - the site config
|
|
90
|
+
# num_page - the pagination page number
|
|
91
|
+
#
|
|
92
|
+
# Returns the pagination path as a string
|
|
93
|
+
def self.paginate_path(site_config, num_page)
|
|
94
|
+
return nil if num_page.nil? || num_page <= 1
|
|
95
|
+
format = File.basename(site_config['paginate_path'])
|
|
96
|
+
format.sub(':num', num_page.to_s)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Initialize a new Pager.
|
|
100
|
+
#
|
|
101
|
+
# config - The Hash configuration of the site.
|
|
102
|
+
# page - The Integer page number.
|
|
103
|
+
# all_posts - The Array of all the site's Posts.
|
|
104
|
+
# num_pages - The Integer number of pages or nil if you'd like the number
|
|
105
|
+
# of pages calculated.
|
|
106
|
+
def initialize(config, page, all_posts, num_pages = nil)
|
|
107
|
+
@page = page
|
|
108
|
+
@per_page = config['paginate'].to_i
|
|
109
|
+
@total_pages = num_pages || Pager.calculate_pages(all_posts, @per_page)
|
|
110
|
+
|
|
111
|
+
if @page > @total_pages
|
|
112
|
+
raise RuntimeError, "page number can't be greater than total pages: #{@page} > #{@total_pages}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
init = (@page - 1) * @per_page
|
|
116
|
+
offset = (init + @per_page - 1) >= all_posts.size ? all_posts.size : (init + @per_page - 1)
|
|
117
|
+
|
|
118
|
+
@total_posts = all_posts.size
|
|
119
|
+
@posts = all_posts[init..offset]
|
|
120
|
+
@previous_page = @page != 1 ? @page - 1 : nil
|
|
121
|
+
@previous_page_path = Pager.paginate_path(config, @previous_page)
|
|
122
|
+
@next_page = @page != @total_pages ? @page + 1 : nil
|
|
123
|
+
@next_page_path = Pager.paginate_path(config, @next_page)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Convert this Pager's data to a Hash suitable for use by Liquid.
|
|
127
|
+
#
|
|
128
|
+
# Returns the Hash representation of this Pager.
|
|
129
|
+
def to_liquid
|
|
130
|
+
{
|
|
131
|
+
'page' => page,
|
|
132
|
+
'per_page' => per_page,
|
|
133
|
+
'posts' => posts,
|
|
134
|
+
'total_posts' => total_posts,
|
|
135
|
+
'total_pages' => total_pages,
|
|
136
|
+
'previous_page' => previous_page,
|
|
137
|
+
'previous_page_path' => previous_page_path,
|
|
138
|
+
'next_page' => next_page,
|
|
139
|
+
'next_page_path' => next_page_path
|
|
140
|
+
}
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|