tekin-jekyll 0.5.2

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