PerfectlyNormal-jekyll 0.5.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.
Files changed (48) hide show
  1. data/History.txt +127 -0
  2. data/README.textile +41 -0
  3. data/Rakefile +91 -0
  4. data/VERSION.yml +4 -0
  5. data/bin/jekyll +150 -0
  6. data/lib/jekyll.rb +84 -0
  7. data/lib/jekyll/albino.rb +122 -0
  8. data/lib/jekyll/converters/csv.rb +26 -0
  9. data/lib/jekyll/converters/mephisto.rb +79 -0
  10. data/lib/jekyll/converters/mt.rb +59 -0
  11. data/lib/jekyll/converters/textpattern.rb +50 -0
  12. data/lib/jekyll/converters/typo.rb +49 -0
  13. data/lib/jekyll/converters/wordpress.rb +54 -0
  14. data/lib/jekyll/convertible.rb +82 -0
  15. data/lib/jekyll/core_ext.rb +22 -0
  16. data/lib/jekyll/filters.rb +47 -0
  17. data/lib/jekyll/layout.rb +36 -0
  18. data/lib/jekyll/page.rb +70 -0
  19. data/lib/jekyll/post.rb +243 -0
  20. data/lib/jekyll/site.rb +265 -0
  21. data/lib/jekyll/tags/highlight.rb +56 -0
  22. data/lib/jekyll/tags/include.rb +31 -0
  23. data/test/helper.rb +27 -0
  24. data/test/source/_includes/sig.markdown +3 -0
  25. data/test/source/_layouts/default.html +27 -0
  26. data/test/source/_layouts/simple.html +1 -0
  27. data/test/source/_posts/2008-02-02-not-published.textile +8 -0
  28. data/test/source/_posts/2008-02-02-published.textile +8 -0
  29. data/test/source/_posts/2008-10-18-foo-bar.textile +8 -0
  30. data/test/source/_posts/2008-11-21-complex.textile +8 -0
  31. data/test/source/_posts/2008-12-03-permalinked-post.textile +9 -0
  32. data/test/source/_posts/2008-12-13-include.markdown +8 -0
  33. data/test/source/_posts/2009-01-27-array-categories.textile +10 -0
  34. data/test/source/_posts/2009-01-27-categories.textile +7 -0
  35. data/test/source/_posts/2009-01-27-category.textile +7 -0
  36. data/test/source/_posts/2009-03-12-hash-#1.markdown +6 -0
  37. data/test/source/category/_posts/2008-9-23-categories.textile +6 -0
  38. data/test/source/css/screen.css +76 -0
  39. data/test/source/foo/_posts/bar/2008-12-12-topical-post.textile +8 -0
  40. data/test/source/index.html +22 -0
  41. data/test/source/z_category/_posts/2008-9-23-categories.textile +6 -0
  42. data/test/suite.rb +9 -0
  43. data/test/test_filters.rb +49 -0
  44. data/test/test_generated_site.rb +38 -0
  45. data/test/test_post.rb +269 -0
  46. data/test/test_site.rb +65 -0
  47. data/test/test_tags.rb +116 -0
  48. metadata +164 -0
@@ -0,0 +1,22 @@
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
+ end
@@ -0,0 +1,47 @@
1
+ module Jekyll
2
+
3
+ module Filters
4
+ def textilize(input)
5
+ RedCloth.new(input).to_html
6
+ end
7
+
8
+ def date_to_string(date)
9
+ date.strftime("%d %b %Y")
10
+ end
11
+
12
+ def date_to_long_string(date)
13
+ date.strftime("%d %B %Y")
14
+ end
15
+
16
+ def date_to_xmlschema(date)
17
+ date.xmlschema
18
+ end
19
+
20
+ def xml_escape(input)
21
+ CGI.escapeHTML(input)
22
+ end
23
+
24
+ def cgi_escape(input)
25
+ CGI::escape(input)
26
+ end
27
+
28
+ def number_of_words(input)
29
+ input.split.length
30
+ end
31
+
32
+ def array_to_sentence_string(array)
33
+ connector = "and"
34
+ case array.length
35
+ when 0
36
+ ""
37
+ when 1
38
+ array[0].to_s
39
+ when 2
40
+ "#{array[0]} #{connector} #{array[1]}"
41
+ else
42
+ "#{array[0...-1].join(', ')}, #{connector} #{array[-1]}"
43
+ end
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,36 @@
1
+ module Jekyll
2
+
3
+ class Layout
4
+ include Convertible
5
+
6
+ attr_accessor :site
7
+ attr_accessor :ext
8
+ attr_accessor :data, :content
9
+
10
+ # Initialize a new Layout.
11
+ # +site+ is the Site
12
+ # +base+ is the String path to the <source>
13
+ # +name+ is the String filename of the post file
14
+ #
15
+ # Returns <Page>
16
+ def initialize(site, base, name)
17
+ @site = site
18
+ @base = base
19
+ @name = name
20
+
21
+ self.data = {}
22
+
23
+ self.process(name)
24
+ self.read_yaml(base, name)
25
+ end
26
+
27
+ # Extract information from the layout filename
28
+ # +name+ is the String filename of the layout file
29
+ #
30
+ # Returns nothing
31
+ def process(name)
32
+ self.ext = File.extname(name)
33
+ end
34
+ end
35
+
36
+ end
@@ -0,0 +1,70 @@
1
+ module Jekyll
2
+
3
+ class Page
4
+ include Convertible
5
+
6
+ attr_accessor :site
7
+ attr_accessor :ext
8
+ attr_accessor :data, :content, :output
9
+
10
+ # Initialize a new Page.
11
+ # +site+ is the Site
12
+ # +base+ is the String path to the <source>
13
+ # +dir+ is the String path between <source> and the file
14
+ # +name+ is the String filename of the file
15
+ #
16
+ # Returns <Page>
17
+ def initialize(site, base, dir, name)
18
+ @site = site
19
+ @base = base
20
+ @dir = dir
21
+ @name = name
22
+
23
+ self.data = {}
24
+
25
+ self.process(name)
26
+ self.read_yaml(File.join(base, dir), name)
27
+ #self.transform
28
+ end
29
+
30
+ # Extract information from the page filename
31
+ # +name+ is the String filename of the page file
32
+ #
33
+ # Returns nothing
34
+ def process(name)
35
+ self.ext = File.extname(name)
36
+ end
37
+
38
+ # Add any necessary layouts to this post
39
+ # +layouts+ is a Hash of {"name" => "layout"}
40
+ # +site_payload+ is the site payload hash
41
+ #
42
+ # Returns nothing
43
+ def render(layouts, site_payload)
44
+ payload = {"page" => self.data}.deep_merge(site_payload)
45
+ do_layout(payload, layouts)
46
+ end
47
+
48
+ # Write the generated page file to the destination directory.
49
+ # +dest_prefix+ is the String path to the destination dir
50
+ # +dest_suffix+ is a suffix path to the destination dir
51
+ #
52
+ # Returns nothing
53
+ def write(dest_prefix, dest_suffix = nil)
54
+ dest = File.join(dest_prefix, @dir)
55
+ dest = File.join(dest, dest_suffix) if dest_suffix
56
+ FileUtils.mkdir_p(dest)
57
+
58
+ name = @name
59
+ if self.ext != ""
60
+ name = @name.split(".")[0..-2].join('.') + self.ext
61
+ end
62
+
63
+ path = File.join(dest, name)
64
+ File.open(path, 'w') do |f|
65
+ f.write(self.output)
66
+ end
67
+ end
68
+ end
69
+
70
+ end
@@ -0,0 +1,243 @@
1
+ module Jekyll
2
+
3
+ class Post
4
+ include Comparable
5
+ include Convertible
6
+
7
+ class << self
8
+ attr_accessor :lsi
9
+ end
10
+
11
+ MATCHER = /^(.+\/)*(\d+-\d+-\d+)-(.*)(\.[^.]+)$/
12
+
13
+ # Post name validator. Post filenames must be like:
14
+ # 2008-11-05-my-awesome-post.textile
15
+ #
16
+ # Returns <Bool>
17
+ def self.valid?(name)
18
+ name =~ MATCHER
19
+ end
20
+
21
+ attr_accessor :site, :date, :slug, :ext, :topics, :published, :data, :content, :output
22
+ attr_writer :categories
23
+
24
+ def categories
25
+ @categories ||= []
26
+ end
27
+
28
+ # Initialize this Post instance.
29
+ # +site+ is the Site
30
+ # +base+ is the String path to the dir containing the post file
31
+ # +name+ is the String filename of the post file
32
+ # +categories+ is an Array of Strings for the categories for this post
33
+ #
34
+ # Returns <Post>
35
+ def initialize(site, source, dir, name)
36
+ @site = site
37
+ @base = File.join(source, dir, '_posts')
38
+ @name = name
39
+
40
+ self.categories = dir.split('/').reject { |x| x.empty? }
41
+
42
+ parts = name.split('/')
43
+ self.topics = parts.size > 1 ? parts[0..-2] : []
44
+
45
+ self.process(name)
46
+ self.read_yaml(@base, name)
47
+
48
+ if self.data.has_key?('published') && self.data['published'] == false
49
+ self.published = false
50
+ else
51
+ self.published = true
52
+ end
53
+
54
+ if self.categories.empty?
55
+ if self.data.has_key?('category')
56
+ self.categories << self.data['category']
57
+ elsif self.data.has_key?('categories')
58
+ # Look for categories in the YAML-header, either specified as
59
+ # an array or a string.
60
+ if self.data['categories'].kind_of? String
61
+ self.categories = self.data['categories'].split
62
+ else
63
+ self.categories = self.data['categories']
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ # Spaceship is based on Post#date
70
+ #
71
+ # Returns -1, 0, 1
72
+ def <=>(other)
73
+ self.date <=> other.date
74
+ end
75
+
76
+ # Extract information from the post filename
77
+ # +name+ is the String filename of the post file
78
+ #
79
+ # Returns nothing
80
+ def process(name)
81
+ m, cats, date, slug, ext = *name.match(MATCHER)
82
+ self.date = Time.parse(date)
83
+ self.slug = slug
84
+ self.ext = ext
85
+ end
86
+
87
+ # The generated directory into which the post will be placed
88
+ # upon generation. This is derived from the permalink or, if
89
+ # permalink is absent, set to the default date
90
+ # e.g. "/2008/11/05/" if the permalink style is :date, otherwise nothing
91
+ #
92
+ # Returns <String>
93
+ def dir
94
+ File.dirname(url)
95
+ end
96
+
97
+ # The full path and filename of the post.
98
+ # Defined in the YAML of the post body
99
+ # (Optional)
100
+ #
101
+ # Returns <String>
102
+ def permalink
103
+ self.data && self.data['permalink']
104
+ end
105
+
106
+ def template
107
+ case self.site.permalink_style
108
+ when :pretty
109
+ "/:categories/:year/:month/:day/:title"
110
+ when :none
111
+ "/:categories/:title.html"
112
+ when :date
113
+ "/:categories/:year/:month/:day/:title.html"
114
+ else
115
+ self.site.permalink_style.to_s
116
+ end
117
+ end
118
+
119
+ # The generated relative url of this post
120
+ # e.g. /2008/11/05/my-awesome-post.html
121
+ #
122
+ # Returns <String>
123
+ def url
124
+ return permalink if permalink
125
+
126
+ @url ||= {
127
+ "year" => date.strftime("%Y"),
128
+ "month" => date.strftime("%m"),
129
+ "day" => date.strftime("%d"),
130
+ "title" => CGI.escape(slug),
131
+ "categories" => categories.sort.join('/')
132
+ }.inject(template) { |result, token|
133
+ result.gsub(/:#{token.first}/, token.last)
134
+ }.gsub(/\/\//, "/")
135
+ end
136
+
137
+ # The UID for this post (useful in feeds)
138
+ # e.g. /2008/11/05/my-awesome-post
139
+ #
140
+ # Returns <String>
141
+ def id
142
+ File.join(self.dir, self.slug)
143
+ end
144
+
145
+ # Calculate related posts.
146
+ #
147
+ # Returns [<Post>]
148
+ def related_posts(posts)
149
+ return [] unless posts.size > 1
150
+
151
+ if self.site.lsi
152
+ self.class.lsi ||= begin
153
+ puts "Running the classifier... this could take a while."
154
+ lsi = Classifier::LSI.new
155
+ posts.each { |x| $stdout.print(".");$stdout.flush;lsi.add_item(x) }
156
+ puts ""
157
+ lsi
158
+ end
159
+
160
+ related = self.class.lsi.find_related(self.content, 11)
161
+ related - [self]
162
+ else
163
+ (posts - [self])[0..9]
164
+ end
165
+ end
166
+
167
+ # Add any necessary layouts to this post
168
+ # +layouts+ is a Hash of {"name" => "layout"}
169
+ # +site_payload+ is the site payload hash
170
+ #
171
+ # Returns nothing
172
+ def render(layouts, site_payload)
173
+ # construct payload
174
+ payload =
175
+ {
176
+ "site" => { "related_posts" => related_posts(site_payload["site"]["posts"]) },
177
+ "page" => self.to_liquid
178
+ }
179
+ payload = payload.deep_merge(site_payload)
180
+
181
+ do_layout(payload, layouts)
182
+ end
183
+
184
+ # Write the generated post file to the destination directory.
185
+ # +dest+ is the String path to the destination dir
186
+ #
187
+ # Returns nothing
188
+ def write(dest)
189
+ FileUtils.mkdir_p(File.join(dest, dir))
190
+
191
+ # The url needs to be unescaped in order to preserve the correct filename
192
+ path = File.join(dest, CGI.unescape(self.url))
193
+
194
+ if template[/\.html$/].nil?
195
+ FileUtils.mkdir_p(path)
196
+ path = File.join(path, "index.html")
197
+ end
198
+
199
+ File.open(path, 'w') do |f|
200
+ f.write(self.output)
201
+ end
202
+ end
203
+
204
+ # Convert this post into a Hash for use in Liquid templates.
205
+ #
206
+ # Returns <Hash>
207
+ def to_liquid
208
+ { "title" => self.data["title"] || self.slug.split('-').select {|w| w.capitalize! || w }.join(' '),
209
+ "url" => self.url,
210
+ "date" => self.date,
211
+ "id" => self.id,
212
+ "topics" => self.topics,
213
+ "categories" => self.categories,
214
+ "next" => self.next,
215
+ "previous" => self.previous,
216
+ "content" => self.content }.deep_merge(self.data)
217
+ end
218
+
219
+ def inspect
220
+ "<Post: #{self.id}>"
221
+ end
222
+
223
+ def next
224
+ pos = self.site.posts.index(self)
225
+
226
+ if pos && pos < self.site.posts.length-1
227
+ self.site.posts[pos+1]
228
+ else
229
+ nil
230
+ end
231
+ end
232
+
233
+ def previous
234
+ pos = self.site.posts.index(self)
235
+ if pos && pos > 0
236
+ self.site.posts[pos-1]
237
+ else
238
+ nil
239
+ end
240
+ end
241
+ end
242
+
243
+ end
@@ -0,0 +1,265 @@
1
+ module Jekyll
2
+
3
+ class Site
4
+ attr_accessor :config, :layouts, :posts, :categories, :exclude
5
+ attr_accessor :source, :dest, :lsi, :pygments, :permalink_style
6
+
7
+ # Initialize the site
8
+ # +config+ is a Hash containing site configurations details
9
+ #
10
+ # Returns <Site>
11
+ def initialize(config)
12
+ self.config = config.clone
13
+
14
+ self.source = config['source']
15
+ self.dest = config['destination']
16
+ self.lsi = config['lsi']
17
+ self.pygments = config['pygments']
18
+ self.permalink_style = config['permalink'].to_sym
19
+ self.exclude = config['exclude'] || []
20
+
21
+ self.reset
22
+ self.setup
23
+ end
24
+
25
+ def reset
26
+ self.layouts = {}
27
+ self.posts = []
28
+ self.categories = Hash.new { |hash, key| hash[key] = Array.new }
29
+ end
30
+
31
+ def setup
32
+ # Check to see if LSI is enabled.
33
+ require 'classifier' if self.lsi
34
+
35
+ # Set the Markdown interpreter (and Maruku self.config, if necessary)
36
+ case self.config['markdown']
37
+ when 'rdiscount'
38
+ begin
39
+ require 'rdiscount'
40
+
41
+ def markdown(content)
42
+ RDiscount.new(content).to_html
43
+ end
44
+
45
+ rescue LoadError
46
+ puts 'You must have the rdiscount gem installed first'
47
+ end
48
+ when 'maruku'
49
+ begin
50
+ require 'maruku'
51
+
52
+ def markdown(content)
53
+ Maruku.new(content).to_html
54
+ end
55
+
56
+ if self.config['maruku']['use_divs']
57
+ require 'maruku/ext/div'
58
+ puts 'Maruku: Using extended syntax for div elements.'
59
+ end
60
+
61
+ if self.config['maruku']['use_tex']
62
+ require 'maruku/ext/math'
63
+ puts "Maruku: Using LaTeX extension. Images in `#{self.config['maruku']['png_dir']}`."
64
+
65
+ # Switch off MathML output
66
+ MaRuKu::Globals[:html_math_output_mathml] = false
67
+ MaRuKu::Globals[:html_math_engine] = 'none'
68
+
69
+ # Turn on math to PNG support with blahtex
70
+ # Resulting PNGs stored in `images/latex`
71
+ MaRuKu::Globals[:html_math_output_png] = true
72
+ MaRuKu::Globals[:html_png_engine] = self.config['maruku']['png_engine']
73
+ MaRuKu::Globals[:html_png_dir] = self.config['maruku']['png_dir']
74
+ MaRuKu::Globals[:html_png_url] = self.config['maruku']['png_url']
75
+ end
76
+ rescue LoadError
77
+ puts "The maruku gem is required for markdown support!"
78
+ end
79
+ end
80
+ end
81
+
82
+ def textile(content)
83
+ RedCloth.new(content).to_html
84
+ end
85
+
86
+ # Do the actual work of processing the site and generating the
87
+ # real deal.
88
+ #
89
+ # Returns nothing
90
+ def process
91
+ self.reset
92
+ self.read_layouts
93
+ self.transform_pages
94
+ self.write_posts
95
+ end
96
+
97
+ # Read all the files in <source>/_layouts into memory for later use.
98
+ #
99
+ # Returns nothing
100
+ def read_layouts
101
+ base = File.join(self.source, "_layouts")
102
+ entries = []
103
+ Dir.chdir(base) { entries = filter_entries(Dir['*.*']) }
104
+
105
+ entries.each do |f|
106
+ name = f.split(".")[0..-2].join(".")
107
+ self.layouts[name] = Layout.new(self, base, f)
108
+ end
109
+ rescue Errno::ENOENT => e
110
+ # ignore missing layout dir
111
+ end
112
+
113
+ # Read all the files in <base>/_posts and create a new Post object with each one.
114
+ #
115
+ # Returns nothing
116
+ def read_posts(dir)
117
+ base = File.join(self.source, dir, '_posts')
118
+ entries = []
119
+ Dir.chdir(base) { entries = filter_entries(Dir['**/*']) }
120
+
121
+ # first pass processes, but does not yet render post content
122
+ entries.each do |f|
123
+ if Post.valid?(f)
124
+ post = Post.new(self, self.source, dir, f)
125
+
126
+ if post.published
127
+ self.posts << post
128
+ post.categories.each { |c| self.categories[c] << post }
129
+ end
130
+ end
131
+ end
132
+
133
+ self.posts.sort!
134
+
135
+ # second pass renders each post now that full site payload is available
136
+ self.posts.each do |post|
137
+ post.render(self.layouts, site_payload)
138
+ end
139
+
140
+ self.categories.values.map { |cats| cats.sort! { |a, b| b <=> a} }
141
+ rescue Errno::ENOENT => e
142
+ # ignore missing layout dir
143
+ end
144
+
145
+ # Write each post to <dest>/<year>/<month>/<day>/<slug>
146
+ #
147
+ # Returns nothing
148
+ def write_posts
149
+ self.posts.each do |post|
150
+ post.write(self.dest)
151
+ end
152
+ end
153
+
154
+ # Copy all regular files from <source> to <dest>/ ignoring
155
+ # any files/directories that are hidden or backup files (start
156
+ # with "." or "#" or end with "~") or contain site content (start with "_")
157
+ # unless they are "_posts" directories or web server files such as
158
+ # '.htaccess'
159
+ # The +dir+ String is a relative path used to call this method
160
+ # recursively as it descends through directories
161
+ #
162
+ # Returns nothing
163
+ def transform_pages(dir = '')
164
+ base = File.join(self.source, dir)
165
+ entries = filter_entries(Dir.entries(base))
166
+ directories = entries.select { |e| File.directory?(File.join(base, e)) }
167
+ files = entries.reject { |e| File.directory?(File.join(base, e)) }
168
+
169
+ # we need to make sure to process _posts *first* otherwise they
170
+ # might not be available yet to other templates as {{ site.posts }}
171
+ if directories.include?('_posts')
172
+ directories.delete('_posts')
173
+ read_posts(dir)
174
+ end
175
+
176
+ [directories, files].each do |entries|
177
+ entries.each do |f|
178
+ if File.directory?(File.join(base, f))
179
+ next if self.dest.sub(/\/$/, '') == File.join(base, f)
180
+ transform_pages(File.join(dir, f))
181
+ elsif Pager.pagination_enabled?(self.config, f)
182
+ paginate_posts(f, dir)
183
+ else
184
+ first3 = File.open(File.join(self.source, dir, f)) { |fd| fd.read(3) }
185
+
186
+ if first3 == "---"
187
+ # file appears to have a YAML header so process it as a page
188
+ page = Page.new(self, self.source, dir, f)
189
+ page.render(self.layouts, site_payload)
190
+ page.write(self.dest)
191
+ else
192
+ # otherwise copy the file without transforming it
193
+ FileUtils.mkdir_p(File.join(self.dest, dir))
194
+ FileUtils.cp(File.join(self.source, dir, f), File.join(self.dest, dir, f))
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
200
+
201
+ # Constructs a hash map of Posts indexed by the specified Post attribute
202
+ #
203
+ # Returns {post_attr => [<Post>]}
204
+ def post_attr_hash(post_attr)
205
+ # Build a hash map based on the specified post attribute ( post attr => array of posts )
206
+ # then sort each array in reverse order
207
+ hash = Hash.new { |hash, key| hash[key] = Array.new }
208
+ self.posts.each { |p| p.send(post_attr.to_sym).each { |t| hash[t] << p } }
209
+ hash.values.map { |sortme| sortme.sort! { |a, b| b <=> a} }
210
+ return hash
211
+ end
212
+
213
+ # The Hash payload containing site-wide data
214
+ #
215
+ # Returns {"site" => {"time" => <Time>,
216
+ # "posts" => [<Post>],
217
+ # "categories" => [<Post>],
218
+ # "topics" => [<Post>] }}
219
+ def site_payload
220
+ {"site" => {
221
+ "time" => Time.now,
222
+ "posts" => self.posts.sort { |a,b| b <=> a },
223
+ "categories" => post_attr_hash('categories'),
224
+ "topics" => post_attr_hash('topics')
225
+ }}
226
+ end
227
+
228
+ # Filter out any files/directories that are hidden or backup files (start
229
+ # with "." or "#" or end with "~") or contain site content (start with "_")
230
+ # unless they are "_posts" directories or web server files such as
231
+ # '.htaccess'
232
+ def filter_entries(entries)
233
+ entries = entries.reject do |e|
234
+ unless ['_posts', '.htaccess'].include?(e)
235
+ ['.', '_', '#'].include?(e[0..0]) || e[-1..-1] == '~' || self.exclude.include?(e)
236
+ end
237
+ end
238
+ end
239
+
240
+ # Paginates the blog's posts. Renders the index.html file into paginated directories, ie: page2, page3...
241
+ # and adds more wite-wide data
242
+ #
243
+ # {"paginator" => { "page" => <Number>,
244
+ # "per_page" => <Number>,
245
+ # "posts" => [<Post>],
246
+ # "total_posts" => <Number>,
247
+ # "total_pages" => <Number>,
248
+ # "previous_page" => <Number>,
249
+ # "next_page" => <Number> }}
250
+ def paginate_posts(file, dir)
251
+ all_posts = self.posts.sort { |a,b| b <=> a }
252
+ page = Page.new(self, self.source, dir, file)
253
+
254
+ pages = Pager.calculate_pages(all_posts, self.config['paginate'].to_i)
255
+
256
+ (1..pages).each do |num_page|
257
+ pager = Pager.new(self.config, num_page, all_posts, pages)
258
+
259
+ page.render(self.layouts, site_payload.merge({'paginator' => pager.to_hash}))
260
+ suffix = "page#{num_page}" if num_page > 1
261
+ page.write(self.dest, suffix)
262
+ end
263
+ end
264
+ end
265
+ end