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.
- data/Gemfile +2 -0
- data/History.txt +321 -0
- data/LICENSE +21 -0
- data/README.textile +41 -0
- data/Rakefile +161 -0
- data/bin/jekyll +289 -0
- data/cucumber.yml +1 -0
- data/features/create_sites.feature +112 -0
- data/features/embed_filters.feature +60 -0
- data/features/markdown.feature +30 -0
- data/features/pagination.feature +27 -0
- data/features/permalinks.feature +65 -0
- data/features/post_data.feature +153 -0
- data/features/site_configuration.feature +145 -0
- data/features/site_data.feature +82 -0
- data/features/step_definitions/jekyll_steps.rb +145 -0
- data/features/support/env.rb +19 -0
- data/jekyll.gemspec +146 -0
- data/lib/guard/jekyll.rb +57 -0
- data/lib/jekyll/converter.rb +50 -0
- data/lib/jekyll/converters/identity.rb +22 -0
- data/lib/jekyll/converters/markdown.rb +125 -0
- data/lib/jekyll/converters/textile.rb +50 -0
- data/lib/jekyll/convertible.rb +116 -0
- data/lib/jekyll/core_ext.rb +52 -0
- data/lib/jekyll/errors.rb +6 -0
- data/lib/jekyll/filters.rb +118 -0
- data/lib/jekyll/generator.rb +7 -0
- data/lib/jekyll/generators/pagination.rb +113 -0
- data/lib/jekyll/layout.rb +51 -0
- data/lib/jekyll/live_site.rb +216 -0
- data/lib/jekyll/migrators/csv.rb +26 -0
- data/lib/jekyll/migrators/drupal.rb +103 -0
- data/lib/jekyll/migrators/enki.rb +49 -0
- data/lib/jekyll/migrators/joomla.rb +53 -0
- data/lib/jekyll/migrators/marley.rb +52 -0
- data/lib/jekyll/migrators/mephisto.rb +84 -0
- data/lib/jekyll/migrators/mt.rb +86 -0
- data/lib/jekyll/migrators/posterous.rb +67 -0
- data/lib/jekyll/migrators/rss.rb +47 -0
- data/lib/jekyll/migrators/textpattern.rb +58 -0
- data/lib/jekyll/migrators/tumblr.rb +195 -0
- data/lib/jekyll/migrators/typo.rb +51 -0
- data/lib/jekyll/migrators/wordpress.rb +294 -0
- data/lib/jekyll/migrators/wordpressdotcom.rb +70 -0
- data/lib/jekyll/page.rb +160 -0
- data/lib/jekyll/plugin.rb +77 -0
- data/lib/jekyll/post.rb +262 -0
- data/lib/jekyll/site.rb +339 -0
- data/lib/jekyll/static_file.rb +77 -0
- data/lib/jekyll/tags/highlight.rb +118 -0
- data/lib/jekyll/tags/include.rb +37 -0
- data/lib/jekyll/tags/post_url.rb +38 -0
- data/lib/jekyll.rb +134 -0
- data/test/helper.rb +34 -0
- data/test/source/.htaccess +8 -0
- data/test/source/_includes/sig.markdown +3 -0
- data/test/source/_layouts/default.html +27 -0
- data/test/source/_layouts/simple.html +1 -0
- data/test/source/_posts/2008-02-02-not-published.textile +8 -0
- data/test/source/_posts/2008-02-02-published.textile +8 -0
- data/test/source/_posts/2008-10-18-foo-bar.textile +8 -0
- data/test/source/_posts/2008-11-21-complex.textile +8 -0
- data/test/source/_posts/2008-12-03-permalinked-post.textile +9 -0
- data/test/source/_posts/2008-12-13-include.markdown +8 -0
- data/test/source/_posts/2009-01-27-array-categories.textile +10 -0
- data/test/source/_posts/2009-01-27-categories.textile +7 -0
- data/test/source/_posts/2009-01-27-category.textile +7 -0
- data/test/source/_posts/2009-01-27-empty-categories.textile +7 -0
- data/test/source/_posts/2009-01-27-empty-category.textile +7 -0
- data/test/source/_posts/2009-03-12-hash-#1.markdown +6 -0
- data/test/source/_posts/2009-05-18-empty-tag.textile +6 -0
- data/test/source/_posts/2009-05-18-empty-tags.textile +6 -0
- data/test/source/_posts/2009-05-18-tag.textile +6 -0
- data/test/source/_posts/2009-05-18-tags.textile +9 -0
- data/test/source/_posts/2009-06-22-empty-yaml.textile +3 -0
- data/test/source/_posts/2009-06-22-no-yaml.textile +1 -0
- data/test/source/_posts/2010-01-08-triple-dash.markdown +5 -0
- data/test/source/_posts/2010-01-09-date-override.textile +7 -0
- data/test/source/_posts/2010-01-09-time-override.textile +7 -0
- data/test/source/_posts/2010-01-09-timezone-override.textile +7 -0
- data/test/source/_posts/2010-01-16-override-data.textile +4 -0
- data/test/source/_posts/2011-04-12-md-extension.md +7 -0
- data/test/source/_posts/2011-04-12-text-extension.text +0 -0
- data/test/source/about.html +6 -0
- data/test/source/category/_posts/2008-9-23-categories.textile +6 -0
- data/test/source/contacts.html +5 -0
- data/test/source/css/screen.css +76 -0
- data/test/source/deal.with.dots.html +7 -0
- data/test/source/foo/_posts/bar/2008-12-12-topical-post.textile +8 -0
- data/test/source/index.html +22 -0
- data/test/source/sitemap.xml +32 -0
- data/test/source/win/_posts/2009-05-24-yaml-linebreak.markdown +7 -0
- data/test/source/z_category/_posts/2008-9-23-categories.textile +6 -0
- data/test/suite.rb +11 -0
- data/test/test_configuration.rb +29 -0
- data/test/test_core_ext.rb +66 -0
- data/test/test_filters.rb +62 -0
- data/test/test_generated_site.rb +72 -0
- data/test/test_kramdown.rb +23 -0
- data/test/test_page.rb +117 -0
- data/test/test_pager.rb +113 -0
- data/test/test_post.rb +450 -0
- data/test/test_rdiscount.rb +18 -0
- data/test/test_redcarpet.rb +21 -0
- data/test/test_redcloth.rb +86 -0
- data/test/test_site.rb +220 -0
- data/test/test_tags.rb +201 -0
- metadata +332 -0
data/lib/jekyll/post.rb
ADDED
|
@@ -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
|
data/lib/jekyll/site.rb
ADDED
|
@@ -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
|