jekyll-reloaded 0.12

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 (109) hide show
  1. data/Gemfile +2 -0
  2. data/History.txt +321 -0
  3. data/LICENSE +21 -0
  4. data/README.textile +41 -0
  5. data/Rakefile +161 -0
  6. data/bin/jekyll +289 -0
  7. data/cucumber.yml +1 -0
  8. data/features/create_sites.feature +112 -0
  9. data/features/embed_filters.feature +60 -0
  10. data/features/markdown.feature +30 -0
  11. data/features/pagination.feature +27 -0
  12. data/features/permalinks.feature +65 -0
  13. data/features/post_data.feature +153 -0
  14. data/features/site_configuration.feature +145 -0
  15. data/features/site_data.feature +82 -0
  16. data/features/step_definitions/jekyll_steps.rb +145 -0
  17. data/features/support/env.rb +19 -0
  18. data/jekyll.gemspec +146 -0
  19. data/lib/guard/jekyll.rb +57 -0
  20. data/lib/jekyll/converter.rb +50 -0
  21. data/lib/jekyll/converters/identity.rb +22 -0
  22. data/lib/jekyll/converters/markdown.rb +125 -0
  23. data/lib/jekyll/converters/textile.rb +50 -0
  24. data/lib/jekyll/convertible.rb +116 -0
  25. data/lib/jekyll/core_ext.rb +52 -0
  26. data/lib/jekyll/errors.rb +6 -0
  27. data/lib/jekyll/filters.rb +118 -0
  28. data/lib/jekyll/generator.rb +7 -0
  29. data/lib/jekyll/generators/pagination.rb +113 -0
  30. data/lib/jekyll/layout.rb +51 -0
  31. data/lib/jekyll/live_site.rb +216 -0
  32. data/lib/jekyll/migrators/csv.rb +26 -0
  33. data/lib/jekyll/migrators/drupal.rb +103 -0
  34. data/lib/jekyll/migrators/enki.rb +49 -0
  35. data/lib/jekyll/migrators/joomla.rb +53 -0
  36. data/lib/jekyll/migrators/marley.rb +52 -0
  37. data/lib/jekyll/migrators/mephisto.rb +84 -0
  38. data/lib/jekyll/migrators/mt.rb +86 -0
  39. data/lib/jekyll/migrators/posterous.rb +67 -0
  40. data/lib/jekyll/migrators/rss.rb +47 -0
  41. data/lib/jekyll/migrators/textpattern.rb +58 -0
  42. data/lib/jekyll/migrators/tumblr.rb +195 -0
  43. data/lib/jekyll/migrators/typo.rb +51 -0
  44. data/lib/jekyll/migrators/wordpress.rb +294 -0
  45. data/lib/jekyll/migrators/wordpressdotcom.rb +70 -0
  46. data/lib/jekyll/page.rb +160 -0
  47. data/lib/jekyll/plugin.rb +77 -0
  48. data/lib/jekyll/post.rb +262 -0
  49. data/lib/jekyll/site.rb +339 -0
  50. data/lib/jekyll/static_file.rb +77 -0
  51. data/lib/jekyll/tags/highlight.rb +118 -0
  52. data/lib/jekyll/tags/include.rb +37 -0
  53. data/lib/jekyll/tags/post_url.rb +38 -0
  54. data/lib/jekyll.rb +134 -0
  55. data/test/helper.rb +34 -0
  56. data/test/source/.htaccess +8 -0
  57. data/test/source/_includes/sig.markdown +3 -0
  58. data/test/source/_layouts/default.html +27 -0
  59. data/test/source/_layouts/simple.html +1 -0
  60. data/test/source/_posts/2008-02-02-not-published.textile +8 -0
  61. data/test/source/_posts/2008-02-02-published.textile +8 -0
  62. data/test/source/_posts/2008-10-18-foo-bar.textile +8 -0
  63. data/test/source/_posts/2008-11-21-complex.textile +8 -0
  64. data/test/source/_posts/2008-12-03-permalinked-post.textile +9 -0
  65. data/test/source/_posts/2008-12-13-include.markdown +8 -0
  66. data/test/source/_posts/2009-01-27-array-categories.textile +10 -0
  67. data/test/source/_posts/2009-01-27-categories.textile +7 -0
  68. data/test/source/_posts/2009-01-27-category.textile +7 -0
  69. data/test/source/_posts/2009-01-27-empty-categories.textile +7 -0
  70. data/test/source/_posts/2009-01-27-empty-category.textile +7 -0
  71. data/test/source/_posts/2009-03-12-hash-#1.markdown +6 -0
  72. data/test/source/_posts/2009-05-18-empty-tag.textile +6 -0
  73. data/test/source/_posts/2009-05-18-empty-tags.textile +6 -0
  74. data/test/source/_posts/2009-05-18-tag.textile +6 -0
  75. data/test/source/_posts/2009-05-18-tags.textile +9 -0
  76. data/test/source/_posts/2009-06-22-empty-yaml.textile +3 -0
  77. data/test/source/_posts/2009-06-22-no-yaml.textile +1 -0
  78. data/test/source/_posts/2010-01-08-triple-dash.markdown +5 -0
  79. data/test/source/_posts/2010-01-09-date-override.textile +7 -0
  80. data/test/source/_posts/2010-01-09-time-override.textile +7 -0
  81. data/test/source/_posts/2010-01-09-timezone-override.textile +7 -0
  82. data/test/source/_posts/2010-01-16-override-data.textile +4 -0
  83. data/test/source/_posts/2011-04-12-md-extension.md +7 -0
  84. data/test/source/_posts/2011-04-12-text-extension.text +0 -0
  85. data/test/source/about.html +6 -0
  86. data/test/source/category/_posts/2008-9-23-categories.textile +6 -0
  87. data/test/source/contacts.html +5 -0
  88. data/test/source/css/screen.css +76 -0
  89. data/test/source/deal.with.dots.html +7 -0
  90. data/test/source/foo/_posts/bar/2008-12-12-topical-post.textile +8 -0
  91. data/test/source/index.html +22 -0
  92. data/test/source/sitemap.xml +32 -0
  93. data/test/source/win/_posts/2009-05-24-yaml-linebreak.markdown +7 -0
  94. data/test/source/z_category/_posts/2008-9-23-categories.textile +6 -0
  95. data/test/suite.rb +11 -0
  96. data/test/test_configuration.rb +29 -0
  97. data/test/test_core_ext.rb +66 -0
  98. data/test/test_filters.rb +62 -0
  99. data/test/test_generated_site.rb +72 -0
  100. data/test/test_kramdown.rb +23 -0
  101. data/test/test_page.rb +117 -0
  102. data/test/test_pager.rb +113 -0
  103. data/test/test_post.rb +450 -0
  104. data/test/test_rdiscount.rb +18 -0
  105. data/test/test_redcarpet.rb +21 -0
  106. data/test/test_redcloth.rb +86 -0
  107. data/test/test_site.rb +220 -0
  108. data/test/test_tags.rb +201 -0
  109. metadata +332 -0
@@ -0,0 +1,262 @@
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
22
+ attr_accessor :content, :output, :ext
23
+ attr_accessor :date, :slug, :published, :tags, :categories
24
+
25
+ attr_reader :name, :data
26
+
27
+ # Initialize a new Post.
28
+ #
29
+ # site - The Site object.
30
+ # source - The String path to the source.
31
+ # dir - The String path between the source and the file.
32
+ # name - The String filename of the file.
33
+ def initialize(site, source, dir, name)
34
+ @site = site
35
+ @base = File.join(source, dir, '_posts')
36
+ @name = name
37
+ @categories = dir.split('/').reject { |x| x.empty? }
38
+
39
+ self.process(name)
40
+ self.read_yaml(@base, name)
41
+ end
42
+
43
+ # The source filename for this post.
44
+ def filename
45
+ File.join(@base, name)
46
+ end
47
+
48
+ # Save post data and extract various post properties from it.
49
+ def data=(data)
50
+ @data = data
51
+ return if data.nil?
52
+
53
+ # data can override the date from the filename
54
+ if data.has_key?('date')
55
+ self.date = Time.parse(data['date'].to_s)
56
+ end
57
+
58
+ self.published = data['published'] != false
59
+
60
+ self.tags = data.pluralized_array('tag', 'tags')
61
+
62
+ if self.categories.empty?
63
+ # TODO: merge with categories from path?
64
+ self.categories = data.pluralized_array('category', 'categories')
65
+ end
66
+
67
+ @data
68
+ end
69
+
70
+ # Spaceship is based on Post#date, slug
71
+ #
72
+ # Returns -1, 0, 1
73
+ def <=>(other)
74
+ cmp = self.date <=> other.date
75
+ if 0 == cmp
76
+ cmp = self.slug <=> other.slug
77
+ end
78
+ return cmp
79
+ end
80
+
81
+ # Extract information from the post filename
82
+ # +name+ is the String filename of the post file
83
+ #
84
+ # Returns nothing
85
+ def process(name)
86
+ _, date, self.slug, self.ext = *name.match(MATCHER)
87
+ self.date = Time.parse(date)
88
+ rescue ArgumentError
89
+ raise FatalException.new("Post #{name} does not have a valid date.")
90
+ end
91
+
92
+ # The generated directory into which the post will be placed
93
+ # upon generation. This is derived from the permalink or, if
94
+ # permalink is absent, set to the default date
95
+ # e.g. "/2008/11/05/" if the permalink style is :date, otherwise nothing
96
+ #
97
+ # Returns <String>
98
+ def dir
99
+ File.dirname(url)
100
+ end
101
+
102
+ # The full path and filename of the post.
103
+ # Defined in the YAML of the post body
104
+ # (Optional)
105
+ #
106
+ # Returns <String>
107
+ def permalink
108
+ self.data && self.data['permalink']
109
+ end
110
+
111
+ def template
112
+ case self.site.permalink_style
113
+ when :pretty
114
+ "/:categories/:year/:month/:day/:title/"
115
+ when :none
116
+ "/:categories/:title.html"
117
+ when :date
118
+ "/:categories/:year/:month/:day/:title.html"
119
+ else
120
+ self.site.permalink_style.to_s
121
+ end
122
+ end
123
+
124
+ # The generated relative url of this post
125
+ # e.g. /2008/11/05/my-awesome-post.html
126
+ #
127
+ # Returns <String>
128
+ def url
129
+ return @url if @url
130
+
131
+ url = if permalink
132
+ permalink
133
+ else
134
+ {
135
+ "year" => date.strftime("%Y"),
136
+ "month" => date.strftime("%m"),
137
+ "day" => date.strftime("%d"),
138
+ "title" => CGI.escape(slug),
139
+ "i_day" => date.strftime("%d").to_i.to_s,
140
+ "i_month" => date.strftime("%m").to_i.to_s,
141
+ "categories" => categories.map { |c| URI.escape(c) }.join('/'),
142
+ "output_ext" => self.output_ext
143
+ }.inject(template) { |result, token|
144
+ result.gsub(/:#{Regexp.escape token.first}/, token.last)
145
+ }.gsub(/\/\//, "/")
146
+ end
147
+
148
+ # sanitize url
149
+ @url = url.split('/').reject{ |part| part =~ /^\.+$/ }.join('/')
150
+ @url += "/" if url =~ /\/$/
151
+ @url
152
+ end
153
+
154
+ # The UID for this post (useful in feeds)
155
+ # e.g. /2008/11/05/my-awesome-post
156
+ #
157
+ # Returns <String>
158
+ def id
159
+ File.join(self.dir, self.slug)
160
+ end
161
+
162
+ # Calculate related posts.
163
+ #
164
+ # Returns [<Post>]
165
+ def related_posts(posts)
166
+ return [] unless posts.size > 1
167
+
168
+ if self.site.lsi
169
+ self.class.lsi ||= begin
170
+ puts "Running the classifier... this could take a while."
171
+ lsi = Classifier::LSI.new
172
+ posts.each { |x| $stdout.print(".");$stdout.flush;lsi.add_item(x) }
173
+ puts ""
174
+ lsi
175
+ end
176
+
177
+ related = self.class.lsi.find_related(self.content, 11)
178
+ related - [self]
179
+ else
180
+ (posts - [self])[0..9]
181
+ end
182
+ end
183
+
184
+ # Add any necessary layouts to this post
185
+ # +layouts+ is a Hash of {"name" => "layout"}
186
+ # +site_payload+ is the site payload hash
187
+ #
188
+ # Returns nothing
189
+ def render(layouts, site_payload)
190
+ # construct payload
191
+ payload = {
192
+ "site" => { "related_posts" => related_posts(site_payload["site"]["posts"]) },
193
+ "page" => self.to_liquid
194
+ }.deep_merge(site_payload)
195
+
196
+ do_layout(payload, layouts)
197
+ end
198
+
199
+ # Obtain destination path.
200
+ # +dest+ is the String path to the destination dir
201
+ #
202
+ # Returns destination file path.
203
+ def destination(dest)
204
+ # The url needs to be unescaped in order to preserve the correct filename
205
+ path = File.join(dest, CGI.unescape(self.url))
206
+ path = File.join(path, "index.html") if template[/\.html$/].nil?
207
+ path
208
+ end
209
+
210
+ # Write the generated post file to the destination directory.
211
+ # +dest+ is the String path to the destination dir
212
+ #
213
+ # Returns nothing
214
+ def write(dest)
215
+ path = destination(dest)
216
+ FileUtils.mkdir_p(File.dirname(path))
217
+ File.open(path, 'w') do |f|
218
+ f.write(self.output)
219
+ end
220
+ end
221
+
222
+ # Convert this post into a Hash for use in Liquid templates.
223
+ #
224
+ # Returns <Hash>
225
+ def to_liquid
226
+ self.data.deep_merge({
227
+ "title" => self.data["title"] || self.slug.split('-').select {|w| w.capitalize! || w }.join(' '),
228
+ "url" => self.url,
229
+ "date" => self.date,
230
+ "id" => self.id,
231
+ "categories" => self.categories,
232
+ "next" => self.next,
233
+ "previous" => self.previous,
234
+ "tags" => self.tags,
235
+ "content" => self.content })
236
+ end
237
+
238
+ def inspect
239
+ "<Post: #{self.id}>"
240
+ end
241
+
242
+ def next
243
+ pos = self.site.posts.index(self)
244
+
245
+ if pos && pos < self.site.posts.length-1
246
+ self.site.posts[pos+1]
247
+ else
248
+ nil
249
+ end
250
+ end
251
+
252
+ def previous
253
+ pos = self.site.posts.index(self)
254
+ if pos && pos > 0
255
+ self.site.posts[pos-1]
256
+ else
257
+ nil
258
+ end
259
+ end
260
+ end
261
+
262
+ end
@@ -0,0 +1,339 @@
1
+ require 'set'
2
+
3
+ module Jekyll
4
+
5
+ class Site
6
+ attr_accessor :config, :layouts, :posts, :pages, :static_files,
7
+ :categories, :exclude, :include, :source, :dest, :lsi, :pygments,
8
+ :permalink_style, :tags, :time, :future, :safe, :plugins, :limit_posts
9
+
10
+ attr_accessor :converters, :generators
11
+
12
+ # Public: Initialize a new Site.
13
+ #
14
+ # config - A Hash containing site configuration details.
15
+ def initialize(config)
16
+ self.config = config.clone
17
+
18
+ self.safe = config['safe']
19
+ self.source = File.expand_path(config['source'])
20
+ self.dest = File.expand_path(config['destination'])
21
+ self.plugins = Array(config['plugins']).map { |d| File.expand_path(d) }
22
+ self.lsi = config['lsi']
23
+ self.pygments = config['pygments']
24
+ self.permalink_style = config['permalink'].to_sym
25
+ self.exclude = config['exclude'] || []
26
+ self.include = config['include'] || []
27
+ self.future = config['future']
28
+ self.limit_posts = config['limit_posts'] || nil
29
+
30
+ self.reset
31
+ self.setup
32
+ end
33
+
34
+ # Public: Read, process, and write this Site to output.
35
+ #
36
+ # Returns nothing.
37
+ def process
38
+ self.reset
39
+ self.read
40
+ self.generate
41
+ self.render
42
+ self.cleanup
43
+ self.write
44
+ end
45
+
46
+ # Reset Site details.
47
+ #
48
+ # Returns nothing
49
+ def reset
50
+ self.time = if self.config['time']
51
+ Time.parse(self.config['time'].to_s)
52
+ else
53
+ Time.now
54
+ end
55
+ self.layouts = {}
56
+ self.posts = []
57
+ self.pages = []
58
+ self.static_files = []
59
+ self.categories = Hash.new { |hash, key| hash[key] = [] }
60
+ self.tags = Hash.new { |hash, key| hash[key] = [] }
61
+
62
+ if !self.limit_posts.nil? && self.limit_posts < 1
63
+ raise ArgumentError, "Limit posts must be nil or >= 1"
64
+ end
65
+ end
66
+
67
+ # Load necessary libraries, plugins, converters, and generators.
68
+ #
69
+ # Returns nothing.
70
+ def setup
71
+ require 'classifier' if self.lsi
72
+
73
+ # If safe mode is off, load in any Ruby files under the plugins
74
+ # directory.
75
+ unless self.safe
76
+ self.plugins.each do |plugins|
77
+ Dir[File.join(plugins, "**/*.rb")].each do |f|
78
+ require f
79
+ end
80
+ end
81
+ end
82
+
83
+ self.converters = Jekyll::Converter.subclasses.select do |c|
84
+ !self.safe || c.safe
85
+ end.map do |c|
86
+ c.new(self.config)
87
+ end
88
+
89
+ self.generators = Jekyll::Generator.subclasses.select do |c|
90
+ !self.safe || c.safe
91
+ end.map do |c|
92
+ c.new(self.config)
93
+ end
94
+ end
95
+
96
+ # Read Site data from disk and load it into internal data structures.
97
+ #
98
+ # Returns nothing.
99
+ def read
100
+ self.read_layouts
101
+ self.read_directories
102
+ end
103
+
104
+ # Read all the files in <source>/<dir>/_layouts and create a new Layout
105
+ # object with each one.
106
+ #
107
+ # Returns nothing.
108
+ def read_layouts(dir = '')
109
+ base = File.join(self.source, dir, "_layouts")
110
+ return unless File.exists?(base)
111
+ entries = []
112
+ Dir.chdir(base) { entries = filter_entries(Dir['*.*']) }
113
+
114
+ entries.each do |f|
115
+ name = f.split(".")[0..-2].join(".")
116
+ self.layouts[name] = Layout.new(self, base, f)
117
+ end
118
+ end
119
+
120
+ # Recursively traverse directories to find posts, pages and static files
121
+ # that will become part of the site according to the rules in
122
+ # filter_entries.
123
+ #
124
+ # dir - The String relative path of the directory to read.
125
+ #
126
+ # Returns nothing.
127
+ def read_directories(dir = '')
128
+ base = File.join(self.source, dir)
129
+ entries = Dir.chdir(base) { filter_entries(Dir.entries('.')) }
130
+
131
+ self.read_posts(dir)
132
+
133
+ entries.each do |f|
134
+ f_abs = File.join(base, f)
135
+ f_rel = File.join(dir, f)
136
+ if File.directory?(f_abs)
137
+ next if self.dest.sub(/\/$/, '') == f_abs
138
+ read_directories(f_rel)
139
+ elsif !File.symlink?(f_abs)
140
+ first3 = File.open(f_abs) { |fd| fd.read(3) }
141
+ if first3 == "---"
142
+ # file appears to have a YAML header so process it as a page
143
+ pages << Page.new(self, self.source, dir, f)
144
+ else
145
+ # otherwise treat it as a static file
146
+ static_files << StaticFile.new(self, self.source, dir, f)
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ # Read all the files in <source>/<dir>/_posts and create a new Post
153
+ # object with each one.
154
+ #
155
+ # dir - The String relative path of the directory to read.
156
+ #
157
+ # Returns nothing.
158
+ def read_posts(dir)
159
+ base = File.join(self.source, dir, '_posts')
160
+ return unless File.exists?(base)
161
+ entries = Dir.chdir(base) { filter_entries(Dir['**/*']) }
162
+
163
+ # first pass processes, but does not yet render post content
164
+ entries.each do |f|
165
+ if Post.valid?(f)
166
+ post = Post.new(self, self.source, dir, f)
167
+
168
+ if post.published && (self.future || post.date <= self.time)
169
+ self.posts << post
170
+ post.categories.each { |c| self.categories[c] << post }
171
+ post.tags.each { |c| self.tags[c] << post }
172
+ end
173
+ end
174
+ end
175
+
176
+ self.posts.sort!
177
+
178
+ # limit the posts if :limit_posts option is set
179
+ if limit_posts
180
+ limit = self.posts.length < limit_posts ? self.posts.length : limit_posts
181
+ self.posts = self.posts[-limit, limit]
182
+ end
183
+ end
184
+
185
+ # Run each of the Generators.
186
+ #
187
+ # Returns nothing.
188
+ def generate
189
+ self.generators.each do |generator|
190
+ generator.generate(self)
191
+ end
192
+ end
193
+
194
+ # Render the site to the destination.
195
+ #
196
+ # Returns nothing.
197
+ def render
198
+ payload = site_payload
199
+
200
+ self.posts.each do |post|
201
+ post.render(self.layouts, payload)
202
+ end
203
+
204
+ self.pages.each do |page|
205
+ page.render(self.layouts, payload)
206
+ end
207
+
208
+ self.categories.values.map { |ps| ps.sort! { |a, b| b <=> a } }
209
+ self.tags.values.map { |ps| ps.sort! { |a, b| b <=> a } }
210
+ rescue Errno::ENOENT
211
+ # ignore missing layout dir
212
+ end
213
+
214
+ # Remove orphaned files and empty directories in destination.
215
+ #
216
+ # Returns nothing.
217
+ def cleanup
218
+ # all files and directories in destination, including hidden ones
219
+ dest_files = Set.new
220
+ Dir.glob(File.join(self.dest, "**", "*"), File::FNM_DOTMATCH) do |file|
221
+ dest_files << file unless file =~ /\/\.{1,2}$/
222
+ end
223
+
224
+ # files to be written
225
+ files = Set.new
226
+ self.posts.each do |post|
227
+ files << post.destination(self.dest)
228
+ end
229
+ self.pages.each do |page|
230
+ files << page.destination(self.dest)
231
+ end
232
+ self.static_files.each do |sf|
233
+ files << sf.destination(self.dest)
234
+ end
235
+
236
+ # adding files' parent directories
237
+ dirs = Set.new
238
+ files.each { |file| dirs << File.dirname(file) }
239
+ files.merge(dirs)
240
+
241
+ obsolete_files = dest_files - files
242
+
243
+ FileUtils.rm_rf(obsolete_files.to_a)
244
+ end
245
+
246
+ # Write static files, pages, and posts.
247
+ #
248
+ # Returns nothing.
249
+ def write
250
+ self.posts.each do |post|
251
+ post.write(self.dest)
252
+ end
253
+ self.pages.each do |page|
254
+ page.write(self.dest)
255
+ end
256
+ self.static_files.each do |sf|
257
+ sf.write(self.dest)
258
+ end
259
+ end
260
+
261
+ # Constructs a Hash of Posts indexed by the specified Post attribute.
262
+ #
263
+ # post_attr - The String name of the Post attribute.
264
+ #
265
+ # Examples
266
+ #
267
+ # post_attr_hash('categories')
268
+ # # => { 'tech' => [<Post A>, <Post B>],
269
+ # # 'ruby' => [<Post B>] }
270
+ #
271
+ # Returns the Hash: { attr => posts } where
272
+ # attr - One of the values for the requested attribute.
273
+ # posts - The Array of Posts with the given attr value.
274
+ def post_attr_hash(post_attr)
275
+ # Build a hash map based on the specified post attribute ( post attr =>
276
+ # array of posts ) then sort each array in reverse order.
277
+ hash = Hash.new { |hsh, key| hsh[key] = Array.new }
278
+ self.posts.each { |p| p.send(post_attr.to_sym).each { |t| hash[t] << p } }
279
+ hash.values.map { |sortme| sortme.sort! { |a, b| b <=> a } }
280
+ hash
281
+ end
282
+
283
+ # The Hash payload containing site-wide data.
284
+ #
285
+ # Returns the Hash: { "site" => data } where data is a Hash with keys:
286
+ # "time" - The Time as specified in the configuration or the
287
+ # current time if none was specified.
288
+ # "posts" - The Array of Posts, sorted chronologically by post date
289
+ # and then title.
290
+ # "pages" - The Array of all Pages.
291
+ # "html_pages" - The Array of HTML Pages.
292
+ # "categories" - The Hash of category values and Posts.
293
+ # See Site#post_attr_hash for type info.
294
+ # "tags" - The Hash of tag values and Posts.
295
+ # See Site#post_attr_hash for type info.
296
+ def site_payload
297
+ {"site" => self.config.merge({
298
+ "time" => self.time,
299
+ "posts" => self.posts.sort { |a, b| b <=> a },
300
+ "pages" => self.pages,
301
+ "html_pages" => self.pages.reject { |page| !page.html? },
302
+ "categories" => post_attr_hash('categories'),
303
+ "tags" => post_attr_hash('tags')})}
304
+ end
305
+
306
+ # Filter out any files/directories that are hidden or backup files (start
307
+ # with "." or "#" or end with "~"), or contain site content (start with "_"),
308
+ # or are excluded in the site configuration, unless they are web server
309
+ # files such as '.htaccess'.
310
+ #
311
+ # entries - The Array of file/directory entries to filter.
312
+ #
313
+ # Returns the Array of filtered entries.
314
+ def filter_entries(entries)
315
+ entries = entries.reject do |e|
316
+ unless self.include.include?(e)
317
+ ['.', '_', '#'].include?(e[0..0]) ||
318
+ e[-1..-1] == '~' ||
319
+ self.exclude.include?(e) ||
320
+ File.symlink?(e)
321
+ end
322
+ end
323
+ end
324
+
325
+ # Get the implementation class for the given Converter.
326
+ #
327
+ # klass - The Class of the Converter to fetch.
328
+ #
329
+ # Returns the Converter instance implementing the given Converter.
330
+ def getConverterImpl(klass)
331
+ matches = self.converters.select { |c| c.class == klass }
332
+ if impl = matches.first
333
+ impl
334
+ else
335
+ raise "Converter implementation not found for #{klass}"
336
+ end
337
+ end
338
+ end
339
+ end
@@ -0,0 +1,77 @@
1
+ module Jekyll
2
+
3
+ class StaticFile
4
+ # The cache of last modification times [path] -> mtime.
5
+ @@mtimes = Hash.new
6
+
7
+ # Initialize a new StaticFile.
8
+ #
9
+ # site - The Site.
10
+ # base - The String path to the <source>.
11
+ # dir - The String path between <source> and the file.
12
+ # name - The String filename of the file.
13
+ def initialize(site, base, dir, name)
14
+ @site = site
15
+ @base = base
16
+ @dir = dir
17
+ @name = name
18
+ end
19
+
20
+ # Returns source file path.
21
+ def path
22
+ File.join(@base, @dir, @name)
23
+ end
24
+ alias filename path
25
+
26
+ def inspect
27
+ "<StaticFile: #{self.path}>"
28
+ end
29
+
30
+ # Obtain destination path.
31
+ #
32
+ # dest - The String path to the destination dir.
33
+ #
34
+ # Returns destination file path.
35
+ def destination(dest)
36
+ File.join(dest, @dir, @name)
37
+ end
38
+
39
+ # Returns last modification time for this file.
40
+ def mtime
41
+ File.stat(path).mtime.to_i
42
+ end
43
+
44
+ # Is source path modified?
45
+ #
46
+ # Returns true if modified since last write.
47
+ def modified?
48
+ @@mtimes[path] != mtime
49
+ end
50
+
51
+ # Write the static file to the destination directory (if modified).
52
+ #
53
+ # dest - The String path to the destination dir.
54
+ #
55
+ # Returns false if the file was not modified since last time (no-op).
56
+ def write(dest)
57
+ dest_path = destination(dest)
58
+
59
+ return false if File.exist?(dest_path) and !modified?
60
+ @@mtimes[path] = mtime
61
+
62
+ FileUtils.mkdir_p(File.dirname(dest_path))
63
+ FileUtils.cp(path, dest_path)
64
+
65
+ true
66
+ end
67
+
68
+ # Reset the mtimes cache (for testing purposes).
69
+ #
70
+ # Returns nothing.
71
+ def self.reset_cache
72
+ @@mtimes = Hash.new
73
+ nil
74
+ end
75
+ end
76
+
77
+ end