jekyll 0.5.7 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of jekyll might be problematic. Click here for more details.

Files changed (48) hide show
  1. data/History.txt +88 -31
  2. data/LICENSE +21 -0
  3. data/README.textile +1 -9
  4. data/Rakefile +119 -51
  5. data/bin/jekyll +26 -2
  6. data/cucumber.yml +1 -0
  7. data/features/create_sites.feature +28 -10
  8. data/features/post_data.feature +7 -7
  9. data/features/site_configuration.feature +41 -1
  10. data/features/step_definitions/jekyll_steps.rb +13 -4
  11. data/jekyll.gemspec +125 -143
  12. data/lib/jekyll.rb +40 -20
  13. data/lib/jekyll/albino.rb +5 -7
  14. data/lib/jekyll/converter.rb +50 -0
  15. data/lib/jekyll/converters/identity.rb +22 -0
  16. data/lib/jekyll/converters/markdown.rb +77 -0
  17. data/lib/jekyll/converters/textile.rb +33 -0
  18. data/lib/jekyll/convertible.rb +18 -24
  19. data/lib/jekyll/errors.rb +6 -0
  20. data/lib/jekyll/generator.rb +7 -0
  21. data/lib/jekyll/generators/pagination.rb +87 -0
  22. data/lib/jekyll/{converters → migrators}/csv.rb +0 -0
  23. data/lib/jekyll/{converters → migrators}/mephisto.rb +0 -0
  24. data/lib/jekyll/{converters → migrators}/mt.rb +0 -0
  25. data/lib/jekyll/{converters → migrators}/textpattern.rb +0 -0
  26. data/lib/jekyll/{converters → migrators}/typo.rb +0 -0
  27. data/lib/jekyll/{converters → migrators}/wordpress.rb +1 -0
  28. data/lib/jekyll/page.rb +28 -11
  29. data/lib/jekyll/plugin.rb +76 -0
  30. data/lib/jekyll/post.rb +10 -8
  31. data/lib/jekyll/site.rb +40 -88
  32. data/lib/jekyll/static_file.rb +52 -4
  33. data/lib/jekyll/tags/highlight.rb +20 -9
  34. data/test/helper.rb +6 -0
  35. data/test/source/_posts/2010-01-09-date-override.textile +2 -0
  36. data/test/source/_posts/2010-01-09-time-override.textile +2 -0
  37. data/test/source/_posts/2010-01-09-timezone-override.textile +7 -0
  38. data/test/source/_posts/2010-01-16-override-data.textile +4 -0
  39. data/test/source/sitemap.xml +27 -18
  40. data/test/test_configuration.rb +1 -1
  41. data/test/test_generated_site.rb +1 -1
  42. data/test/test_post.rb +63 -12
  43. data/test/test_site.rb +69 -7
  44. data/test/test_tags.rb +6 -14
  45. metadata +156 -52
  46. data/.gitignore +0 -6
  47. data/VERSION.yml +0 -5
  48. data/lib/jekyll/pager.rb +0 -45
@@ -1,6 +1,7 @@
1
1
  require 'rubygems'
2
2
  require 'sequel'
3
3
  require 'fileutils'
4
+ require 'yaml'
4
5
 
5
6
  # NOTE: This converter requires Sequel and the MySQL gems.
6
7
  # The MySQL gem can be difficult to install on OS X. Once you have MySQL
@@ -3,7 +3,7 @@ module Jekyll
3
3
  class Page
4
4
  include Convertible
5
5
 
6
- attr_accessor :site
6
+ attr_accessor :site, :pager
7
7
  attr_accessor :name, :ext, :basename, :dir
8
8
  attr_accessor :data, :content, :output
9
9
 
@@ -43,10 +43,10 @@ module Jekyll
43
43
  end
44
44
 
45
45
  def template
46
- if self.site.permalink_style == :pretty && !index?
47
- "/:name/"
46
+ if self.site.permalink_style == :pretty && !index? && html?
47
+ "/:basename/"
48
48
  else
49
- "/:name.html"
49
+ "/:basename:output_ext"
50
50
  end
51
51
  end
52
52
 
@@ -57,7 +57,12 @@ module Jekyll
57
57
  def url
58
58
  return permalink if permalink
59
59
 
60
- @url ||= (ext == '.html') ? template.gsub(':name', basename) : "/#{name}"
60
+ @url ||= {
61
+ "basename" => self.basename,
62
+ "output_ext" => self.output_ext,
63
+ }.inject(template) { |result, token|
64
+ result.gsub(/:#{token.first}/, token.last)
65
+ }.gsub(/\/\//, "/")
61
66
  end
62
67
 
63
68
  # Extract information from the page filename
@@ -75,10 +80,20 @@ module Jekyll
75
80
  #
76
81
  # Returns nothing
77
82
  def render(layouts, site_payload)
78
- payload = {"page" => self.data}.deep_merge(site_payload)
83
+ payload = {
84
+ "page" => self.to_liquid,
85
+ 'paginator' => pager.to_liquid
86
+ }.deep_merge(site_payload)
87
+
79
88
  do_layout(payload, layouts)
80
89
  end
81
90
 
91
+ def to_liquid
92
+ self.data.deep_merge({
93
+ "url" => self.url,
94
+ "content" => self.content })
95
+ end
96
+
82
97
  # Write the generated page file to the destination directory.
83
98
  # +dest_prefix+ is the String path to the destination dir
84
99
  # +dest_suffix+ is a suffix path to the destination dir
@@ -91,7 +106,7 @@ module Jekyll
91
106
 
92
107
  # The url needs to be unescaped in order to preserve the correct filename
93
108
  path = File.join(dest, CGI.unescape(self.url))
94
- if self.ext == '.html' && self.url[/\.html$/].nil?
109
+ if self.url =~ /\/$/
95
110
  FileUtils.mkdir_p(path)
96
111
  path = File.join(path, "index.html")
97
112
  end
@@ -105,11 +120,13 @@ module Jekyll
105
120
  "#<Jekyll:Page @name=#{self.name.inspect}>"
106
121
  end
107
122
 
108
- private
123
+ def html?
124
+ output_ext == '.html'
125
+ end
109
126
 
110
- def index?
111
- basename == 'index'
112
- end
127
+ def index?
128
+ basename == 'index'
129
+ end
113
130
 
114
131
  end
115
132
 
@@ -0,0 +1,76 @@
1
+ module Jekyll
2
+
3
+ class Plugin
4
+ PRIORITIES = { :lowest => -100,
5
+ :low => -10,
6
+ :normal => 0,
7
+ :high => 10,
8
+ :highest => 100 }
9
+
10
+ # Install a hook so that subclasses are recorded. This method is only
11
+ # ever called by Ruby itself.
12
+ #
13
+ # base - The Class subclass.
14
+ #
15
+ # Returns nothing.
16
+ def self.inherited(base)
17
+ subclasses << base
18
+ subclasses.sort!
19
+ end
20
+
21
+ # The list of Classes that have been subclassed.
22
+ #
23
+ # Returns an Array of Class objects.
24
+ def self.subclasses
25
+ @subclasses ||= []
26
+ end
27
+
28
+ # Get or set the priority of this plugin. When called without an
29
+ # argument it returns the priority. When an argument is given, it will
30
+ # set the priority.
31
+ #
32
+ # priority - The Symbol priority (default: nil). Valid options are:
33
+ # :lowest, :low, :normal, :high, :highest
34
+ #
35
+ # Returns the Symbol priority.
36
+ def self.priority(priority = nil)
37
+ if priority && PRIORITIES.has_key?(priority)
38
+ @priority = priority
39
+ end
40
+ @priority || :normal
41
+ end
42
+
43
+ # Get or set the safety of this plugin. When called without an argument
44
+ # it returns the safety. When an argument is given, it will set the
45
+ # safety.
46
+ #
47
+ # safe - The Boolean safety (default: nil).
48
+ #
49
+ # Returns the safety Boolean.
50
+ def self.safe(safe = nil)
51
+ if safe
52
+ @safe = safe
53
+ end
54
+ @safe || false
55
+ end
56
+
57
+ # Spaceship is priority [higher -> lower]
58
+ #
59
+ # other - The class to be compared.
60
+ #
61
+ # Returns -1, 0, 1.
62
+ def self.<=>(other)
63
+ PRIORITIES[other.priority] <=> PRIORITIES[self.priority]
64
+ end
65
+
66
+ # Initialize a new plugin. This should be overridden by the subclass.
67
+ #
68
+ # config - The Hash of configuration options.
69
+ #
70
+ # Returns a new instance.
71
+ def initialize(config = {})
72
+ # no-op for default
73
+ end
74
+ end
75
+
76
+ end
@@ -124,9 +124,12 @@ module Jekyll
124
124
  "month" => date.strftime("%m"),
125
125
  "day" => date.strftime("%d"),
126
126
  "title" => CGI.escape(slug),
127
- "categories" => categories.sort.join('/')
127
+ "i_day" => date.strftime("%d").to_i.to_s,
128
+ "i_month" => date.strftime("%m").to_i.to_s,
129
+ "categories" => categories.join('/'),
130
+ "output_ext" => self.output_ext
128
131
  }.inject(template) { |result, token|
129
- result.gsub(/:#{token.first}/, token.last)
132
+ result.gsub(/:#{Regexp.escape token.first}/, token.last)
130
133
  }.gsub(/\/\//, "/")
131
134
  end
132
135
 
@@ -167,12 +170,10 @@ module Jekyll
167
170
  # Returns nothing
168
171
  def render(layouts, site_payload)
169
172
  # construct payload
170
- payload =
171
- {
173
+ payload = {
172
174
  "site" => { "related_posts" => related_posts(site_payload["site"]["posts"]) },
173
175
  "page" => self.to_liquid
174
- }
175
- payload = payload.deep_merge(site_payload)
176
+ }.deep_merge(site_payload)
176
177
 
177
178
  do_layout(payload, layouts)
178
179
  end
@@ -201,7 +202,8 @@ module Jekyll
201
202
  #
202
203
  # Returns <Hash>
203
204
  def to_liquid
204
- { "title" => self.data["title"] || self.slug.split('-').select {|w| w.capitalize! || w }.join(' '),
205
+ self.data.deep_merge({
206
+ "title" => self.data["title"] || self.slug.split('-').select {|w| w.capitalize! || w }.join(' '),
205
207
  "url" => self.url,
206
208
  "date" => self.date,
207
209
  "id" => self.id,
@@ -209,7 +211,7 @@ module Jekyll
209
211
  "next" => self.next,
210
212
  "previous" => self.previous,
211
213
  "tags" => self.tags,
212
- "content" => self.content }.deep_merge(self.data)
214
+ "content" => self.content })
213
215
  end
214
216
 
215
217
  def inspect
@@ -1,8 +1,10 @@
1
1
  module Jekyll
2
2
 
3
3
  class Site
4
- attr_accessor :config, :layouts, :posts, :pages, :static_files, :categories, :exclude,
5
- :source, :dest, :lsi, :pygments, :permalink_style, :tags
4
+ attr_accessor :config, :layouts, :posts, :pages, :static_files,
5
+ :categories, :exclude, :source, :dest, :lsi, :pygments,
6
+ :permalink_style, :tags, :time, :future, :safe, :plugins
7
+ attr_accessor :converters, :generators
6
8
 
7
9
  # Initialize the site
8
10
  # +config+ is a Hash containing site configurations details
@@ -11,18 +13,22 @@ module Jekyll
11
13
  def initialize(config)
12
14
  self.config = config.clone
13
15
 
16
+ self.safe = config['safe']
14
17
  self.source = config['source']
15
18
  self.dest = config['destination']
19
+ self.plugins = config['plugins']
16
20
  self.lsi = config['lsi']
17
21
  self.pygments = config['pygments']
18
22
  self.permalink_style = config['permalink'].to_sym
19
23
  self.exclude = config['exclude'] || []
24
+ self.future = config['future']
20
25
 
21
26
  self.reset
22
27
  self.setup
23
28
  end
24
29
 
25
30
  def reset
31
+ self.time = Time.parse(self.config['time'].to_s) || Time.now
26
32
  self.layouts = {}
27
33
  self.posts = []
28
34
  self.pages = []
@@ -32,70 +38,38 @@ module Jekyll
32
38
  end
33
39
 
34
40
  def setup
35
- # Check to see if LSI is enabled.
36
41
  require 'classifier' if self.lsi
37
42
 
38
- # Set the Markdown interpreter (and Maruku self.config, if necessary)
39
- case self.config['markdown']
40
- when 'rdiscount'
41
- begin
42
- require 'rdiscount'
43
-
44
- def markdown(content)
45
- RDiscount.new(content).to_html
46
- end
47
-
48
- rescue LoadError
49
- puts 'You must have the rdiscount gem installed first'
50
- end
51
- when 'maruku'
52
- begin
53
- require 'maruku'
54
-
55
- def markdown(content)
56
- Maruku.new(content).to_html
57
- end
58
-
59
- if self.config['maruku']['use_divs']
60
- require 'maruku/ext/div'
61
- puts 'Maruku: Using extended syntax for div elements.'
62
- end
63
-
64
- if self.config['maruku']['use_tex']
65
- require 'maruku/ext/math'
66
- puts "Maruku: Using LaTeX extension. Images in `#{self.config['maruku']['png_dir']}`."
67
-
68
- # Switch off MathML output
69
- MaRuKu::Globals[:html_math_output_mathml] = false
70
- MaRuKu::Globals[:html_math_engine] = 'none'
43
+ # If safe mode is off, load in any ruby files under the plugins
44
+ # directory.
45
+ unless self.safe
46
+ Dir[File.join(self.plugins, "**/*.rb")].each do |f|
47
+ require f
48
+ end
49
+ end
71
50
 
72
- # Turn on math to PNG support with blahtex
73
- # Resulting PNGs stored in `images/latex`
74
- MaRuKu::Globals[:html_math_output_png] = true
75
- MaRuKu::Globals[:html_png_engine] = self.config['maruku']['png_engine']
76
- MaRuKu::Globals[:html_png_dir] = self.config['maruku']['png_dir']
77
- MaRuKu::Globals[:html_png_url] = self.config['maruku']['png_url']
78
- end
79
- rescue LoadError
80
- puts "The maruku gem is required for markdown support!"
81
- end
82
- else
83
- raise "Invalid Markdown processor: '#{self.config['markdown']}' -- did you mean 'maruku' or 'rdiscount'?"
51
+ self.converters = Jekyll::Converter.subclasses.select do |c|
52
+ !self.safe || c.safe
53
+ end.map do |c|
54
+ c.new(self.config)
84
55
  end
85
- end
86
56
 
87
- def textile(content)
88
- RedCloth.new(content).to_html
57
+ self.generators = Jekyll::Generator.subclasses.select do |c|
58
+ !self.safe || c.safe
59
+ end.map do |c|
60
+ c.new(self.config)
61
+ end
89
62
  end
90
63
 
91
64
  # Do the actual work of processing the site and generating the
92
- # real deal. Now has 4 phases; reset, read, render, write. This allows
65
+ # real deal. 5 phases; reset, read, generate, render, write. This allows
93
66
  # rendering to have full site payload available.
94
67
  #
95
68
  # Returns nothing
96
69
  def process
97
70
  self.reset
98
71
  self.read
72
+ self.generate
99
73
  self.render
100
74
  self.write
101
75
  end
@@ -135,7 +109,7 @@ module Jekyll
135
109
  if Post.valid?(f)
136
110
  post = Post.new(self, self.source, dir, f)
137
111
 
138
- if post.published
112
+ if post.published && (self.future || post.date <= self.time)
139
113
  self.posts << post
140
114
  post.categories.each { |c| self.categories[c] << post }
141
115
  post.tags.each { |c| self.tags[c] << post }
@@ -146,17 +120,19 @@ module Jekyll
146
120
  self.posts.sort!
147
121
  end
148
122
 
123
+ def generate
124
+ self.generators.each do |generator|
125
+ generator.generate(self)
126
+ end
127
+ end
128
+
149
129
  def render
150
130
  self.posts.each do |post|
151
131
  post.render(self.layouts, site_payload)
152
132
  end
153
133
 
154
- self.pages.dup.each do |page|
155
- if Pager.pagination_enabled?(self.config, page.name)
156
- paginate(page)
157
- else
158
- page.render(self.layouts, site_payload)
159
- end
134
+ self.pages.each do |page|
135
+ page.render(self.layouts, site_payload)
160
136
  end
161
137
 
162
138
  self.categories.values.map { |ps| ps.sort! { |a, b| b <=> a} }
@@ -180,7 +156,7 @@ module Jekyll
180
156
  end
181
157
  end
182
158
 
183
- # Reads the directories and finds posts, pages and static files that will
159
+ # Reads the directories and finds posts, pages and static files that will
184
160
  # become part of the valid site according to the rules in +filter_entries+.
185
161
  # The +dir+ String is a relative path used to call this method
186
162
  # recursively as it descends through directories
@@ -227,11 +203,14 @@ module Jekyll
227
203
  #
228
204
  # Returns {"site" => {"time" => <Time>,
229
205
  # "posts" => [<Post>],
206
+ # "pages" => [<Page>],
230
207
  # "categories" => [<Post>]}
231
208
  def site_payload
232
209
  {"site" => self.config.merge({
233
- "time" => Time.now,
210
+ "time" => self.time,
234
211
  "posts" => self.posts.sort { |a,b| b <=> a },
212
+ "pages" => self.pages,
213
+ "html_pages" => self.pages.reject { |page| !page.html? },
235
214
  "categories" => post_attr_hash('categories'),
236
215
  "tags" => post_attr_hash('tags')})}
237
216
  end
@@ -248,32 +227,5 @@ module Jekyll
248
227
  end
249
228
  end
250
229
 
251
- # Paginates the blog's posts. Renders the index.html file into paginated
252
- # directories, ie: page2/index.html, page3/index.html, etc and adds more
253
- # site-wide data.
254
- # +page+ is the index.html Page that requires pagination
255
- #
256
- # {"paginator" => { "page" => <Number>,
257
- # "per_page" => <Number>,
258
- # "posts" => [<Post>],
259
- # "total_posts" => <Number>,
260
- # "total_pages" => <Number>,
261
- # "previous_page" => <Number>,
262
- # "next_page" => <Number> }}
263
- def paginate(page)
264
- all_posts = site_payload['site']['posts']
265
- pages = Pager.calculate_pages(all_posts, self.config['paginate'].to_i)
266
- (1..pages).each do |num_page|
267
- pager = Pager.new(self.config, num_page, all_posts, pages)
268
- if num_page > 1
269
- newpage = Page.new(self, self.source, page.dir, page.name)
270
- newpage.render(self.layouts, site_payload.merge({'paginator' => pager.to_hash}))
271
- newpage.dir = File.join(page.dir, "page#{num_page}")
272
- self.pages << newpage
273
- else
274
- page.render(self.layouts, site_payload.merge({'paginator' => pager.to_hash}))
275
- end
276
- end
277
- end
278
230
  end
279
231
  end
@@ -1,6 +1,8 @@
1
1
  module Jekyll
2
2
 
3
3
  class StaticFile
4
+ @@mtimes = Hash.new # the cache of last modification times [path] -> mtime
5
+
4
6
  # Initialize a new StaticFile.
5
7
  # +site+ is the Site
6
8
  # +base+ is the String path to the <source>
@@ -15,13 +17,59 @@ module Jekyll
15
17
  @name = name
16
18
  end
17
19
 
18
- # Write the static file to the destination directory.
20
+ # Obtains source file path.
21
+ #
22
+ # Returns source file path.
23
+ def path
24
+ File.join(@base, @dir, @name)
25
+ end
26
+
27
+ # Obtain destination path.
19
28
  # +dest+ is the String path to the destination dir
20
29
  #
21
- # Returns nothing
30
+ # Returns destination file path.
31
+ def destination(dest)
32
+ File.join(dest, @dir, @name)
33
+ end
34
+
35
+ # Obtain mtime of the source path.
36
+ #
37
+ # Returns last modifiaction time for this file.
38
+ def mtime
39
+ File.stat(path).mtime.to_i
40
+ end
41
+
42
+ # Is source path modified?
43
+ #
44
+ # Returns true if modified since last write.
45
+ def modified?
46
+ @@mtimes[path] != mtime
47
+ end
48
+
49
+ # Write the static file to the destination directory (if modified).
50
+ # +dest+ is the String path to the destination dir
51
+ #
52
+ # Returns false if the file was not modified since last time (no-op).
22
53
  def write(dest)
23
- FileUtils.mkdir_p(File.join(dest, @dir))
24
- FileUtils.cp(File.join(@base, @dir, @name), File.join(dest, @dir, @name))
54
+ dest_path = destination(dest)
55
+ dest_dir = File.join(dest, @dir)
56
+
57
+ return false if File.exist? dest_path and !modified?
58
+ @@mtimes[path] = mtime
59
+
60
+ FileUtils.mkdir_p(dest_dir)
61
+ FileUtils.cp(path, dest_path)
62
+
63
+ true
64
+ end
65
+
66
+ # Reset the mtimes cache (for testing purposes).
67
+ #
68
+ # Returns nothing.
69
+ def self.reset_cache
70
+ @@mtimes = Hash.new
71
+
72
+ nil
25
73
  end
26
74
  end
27
75