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