crnixon-jekyll 0.5.1

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 (59) hide show
  1. data/.gitignore +6 -0
  2. data/History.txt +115 -0
  3. data/README.textile +649 -0
  4. data/Rakefile +91 -0
  5. data/TODO +3 -0
  6. data/VERSION.yml +4 -0
  7. data/bin/jekyll +156 -0
  8. data/crnixon-jekyll.gemspec +125 -0
  9. data/features/create_sites.feature +46 -0
  10. data/features/embed_filters.feature +60 -0
  11. data/features/permalinks.feature +54 -0
  12. data/features/post_data.feature +155 -0
  13. data/features/site_configuration.feature +40 -0
  14. data/features/site_data.feature +51 -0
  15. data/features/step_definitions/jekyll_steps.rb +118 -0
  16. data/features/support/env.rb +18 -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 +54 -0
  24. data/lib/jekyll/convertible.rb +117 -0
  25. data/lib/jekyll/core_ext.rb +29 -0
  26. data/lib/jekyll/filters.rb +47 -0
  27. data/lib/jekyll/haml_helpers.rb +17 -0
  28. data/lib/jekyll/layout.rb +37 -0
  29. data/lib/jekyll/page.rb +67 -0
  30. data/lib/jekyll/post.rb +305 -0
  31. data/lib/jekyll/site.rb +310 -0
  32. data/lib/jekyll/tags/highlight.rb +68 -0
  33. data/lib/jekyll/tags/include.rb +31 -0
  34. data/lib/jekyll.rb +84 -0
  35. data/test/helper.rb +24 -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/category/_posts/2008-9-23-categories.textile +6 -0
  49. data/test/source/css/screen.css +76 -0
  50. data/test/source/foo/_posts/bar/2008-12-12-topical-post.textile +8 -0
  51. data/test/source/index.html +22 -0
  52. data/test/source/z_category/_posts/2008-9-23-categories.textile +6 -0
  53. data/test/suite.rb +9 -0
  54. data/test/test_filters.rb +41 -0
  55. data/test/test_generated_site.rb +38 -0
  56. data/test/test_post.rb +262 -0
  57. data/test/test_site.rb +61 -0
  58. data/test/test_tags.rb +51 -0
  59. metadata +180 -0
@@ -0,0 +1,310 @@
1
+ module Jekyll
2
+
3
+ class Site
4
+ attr_accessor :config, :layouts, :posts, :collated_posts, :categories, :tags
5
+ attr_accessor :source, :dest, :lsi, :pygments, :pygments_cache, :permalink_style,
6
+ :sass, :post_defaults
7
+
8
+ # Initialize the site
9
+ # +config+ is a Hash containing site configurations details
10
+ #
11
+ # Returns <Site>
12
+ def initialize(config)
13
+ self.config = config.clone
14
+
15
+ self.source = config['source']
16
+ self.dest = config['destination']
17
+ self.lsi = config['lsi']
18
+ self.pygments = config['pygments']
19
+ self.pygments_cache = config['pygments_cache']
20
+ self.permalink_style = config['permalink'].to_sym
21
+ self.post_defaults = config['post_defaults'] || {}
22
+
23
+ self.reset
24
+ self.setup
25
+ end
26
+
27
+ # TODO add a feature or test for this
28
+
29
+ def method_missing(method, *args)
30
+ if self.config.keys.include?(method.to_s) && args.length == 0
31
+ self.config[method.to_s]
32
+ else
33
+ super
34
+ end
35
+ end
36
+
37
+ def reset
38
+ self.layouts = {}
39
+ self.posts = []
40
+ self.collated_posts = Hash.new {|h,k| h[k] = Hash.new {|h,k| h[k] = Hash.new {|h,k| h[k] = [] } } }
41
+ self.categories = Hash.new { |hash, key| hash[key] = [] }
42
+ self.tags = Hash.new { |hash, key| hash[key] = [] }
43
+ end
44
+
45
+ def setup
46
+ # Check to see if LSI is enabled.
47
+ require 'classifier' if self.lsi
48
+
49
+ if self.config['sass']
50
+ begin
51
+ require 'sass'
52
+ self.sass = true
53
+ puts 'Using Sass for CSS generation'
54
+ rescue LoadError
55
+ puts 'You must have the haml gem installed first'
56
+ end
57
+ end
58
+
59
+ if self.config['haml']
60
+ begin
61
+ require 'haml'
62
+ require 'jekyll/haml_helpers'
63
+ helpers = File.join(source, '_helpers.rb')
64
+ require helpers if File.exist?(helpers)
65
+ puts 'Enabled Haml'
66
+ rescue LoadError
67
+ puts 'You must have the haml gem installed first'
68
+ end
69
+ end
70
+
71
+ if self.pygments_cache
72
+ require 'fileutils'
73
+ FileUtils.mkdir_p(pygments_cache)
74
+ require 'digest/md5'
75
+ end
76
+
77
+ # Set the Markdown interpreter (and Maruku self.config, if necessary)
78
+ case self.config['markdown']
79
+ when 'rdiscount'
80
+ begin
81
+ require 'rdiscount'
82
+
83
+ def markdown(content)
84
+ RDiscount.new(content).to_html
85
+ end
86
+
87
+ puts 'Using rdiscount for Markdown'
88
+ rescue LoadError
89
+ puts 'You must have the rdiscount gem installed first'
90
+ end
91
+ when 'maruku'
92
+ begin
93
+ require 'maruku'
94
+
95
+ def markdown(content)
96
+ Maruku.new(content).to_html
97
+ end
98
+
99
+ if self.config['maruku']['use_divs']
100
+ require 'maruku/ext/div'
101
+ puts 'Maruku: Using extended syntax for div elements.'
102
+ end
103
+
104
+ if self.config['maruku']['use_tex']
105
+ require 'maruku/ext/math'
106
+ puts "Maruku: Using LaTeX extension. Images in `#{self.config['maruku']['png_dir']}`."
107
+
108
+ # Switch off MathML output
109
+ MaRuKu::Globals[:html_math_output_mathml] = false
110
+ MaRuKu::Globals[:html_math_engine] = 'none'
111
+
112
+ # Turn on math to PNG support with blahtex
113
+ # Resulting PNGs stored in `images/latex`
114
+ MaRuKu::Globals[:html_math_output_png] = true
115
+ MaRuKu::Globals[:html_png_engine] = self.config['maruku']['png_engine']
116
+ MaRuKu::Globals[:html_png_dir] = self.config['maruku']['png_dir']
117
+ MaRuKu::Globals[:html_png_url] = self.config['maruku']['png_url']
118
+ end
119
+ rescue LoadError
120
+ puts "The maruku gem is required for markdown support!"
121
+ end
122
+ end
123
+ end
124
+
125
+ def textile(content)
126
+ RedCloth.new(content).to_html
127
+ end
128
+
129
+ # Do the actual work of processing the site and generating the
130
+ # real deal.
131
+ #
132
+ # Returns nothing
133
+ def process
134
+ self.reset
135
+ self.read_layouts
136
+ self.transform_pages
137
+ self.transform_sass if self.sass
138
+ self.write_posts
139
+ end
140
+
141
+ # Read all the files in <source>/_layouts into memory for later use.
142
+ #
143
+ # Returns nothing
144
+ def read_layouts
145
+ base = File.join(self.source, "_layouts")
146
+ entries = []
147
+ Dir.chdir(base) { entries = filter_entries(Dir['*.*']) }
148
+
149
+ entries.each do |f|
150
+ name = f.split(".")[0..-2].join(".")
151
+ self.layouts[name] = Layout.new(self, base, f)
152
+ end
153
+ rescue Errno::ENOENT => e
154
+ # ignore missing layout dir
155
+ end
156
+
157
+ # Read all the files in <base>/_posts and create a new Post object with each one.
158
+ #
159
+ # Returns nothing
160
+ def read_posts(dir)
161
+ base = File.join(self.source, dir, '_posts')
162
+ entries = []
163
+ Dir.chdir(base) { entries = filter_entries(Dir['**/*']) }
164
+
165
+ # first pass processes, but does not yet render post content
166
+ entries.each do |f|
167
+ if Post.valid?(f)
168
+ post = Post.new(self, self.source, dir, f)
169
+
170
+ if post.published
171
+ self.posts << post
172
+ post.categories.each { |c| self.categories[c] << post }
173
+ post.tags.each { |c| self.tags[c] << post }
174
+ end
175
+ end
176
+ end
177
+
178
+ self.posts.sort!
179
+
180
+ # second pass renders each post now that full site payload is available
181
+ self.posts.each do |post|
182
+ post.render(self.layouts, site_payload)
183
+ self.collated_posts[post.date.year][post.date.month][post.date.day].unshift(post)
184
+ end
185
+
186
+ self.categories.values.map { |ps| ps.sort! { |a, b| b <=> a} }
187
+ self.tags.values.map { |ps| ps.sort! { |a, b| b <=> a} }
188
+ rescue Errno::ENOENT => e
189
+ # ignore missing layout dir
190
+ end
191
+
192
+ # Write each post to <dest>/<year>/<month>/<day>/<slug>
193
+ #
194
+ # Returns nothing
195
+ def write_posts
196
+ self.posts.each do |post|
197
+ post.write(self.dest)
198
+ end
199
+ end
200
+
201
+ # Copy all regular files from <source> to <dest>/ ignoring
202
+ # any files/directories that are hidden or backup files (start
203
+ # with "." or "#" or end with "~") or contain site content (start with "_")
204
+ # unless they are "_posts" directories or web server files such as
205
+ # '.htaccess'
206
+ # The +dir+ String is a relative path used to call this method
207
+ # recursively as it descends through directories
208
+ #
209
+ # Returns nothing
210
+ def transform_pages(dir = '')
211
+ base = File.join(self.source, dir)
212
+ entries = filter_entries(Dir.entries(base))
213
+ directories = entries.select { |e| File.directory?(File.join(base, e)) }
214
+ files = entries.reject { |e| File.directory?(File.join(base, e)) }
215
+
216
+ # we need to make sure to process _posts *first* otherwise they
217
+ # might not be available yet to other templates as {{ site.posts }}
218
+ if directories.include?('_posts')
219
+ directories.delete('_posts')
220
+ read_posts(dir)
221
+ end
222
+ [directories, files].each do |entries|
223
+ entries.each do |f|
224
+ if File.directory?(File.join(base, f))
225
+ next if self.dest.sub(/\/$/, '') == File.join(base, f)
226
+ transform_pages(File.join(dir, f))
227
+ else
228
+ first3 = File.open(File.join(self.source, dir, f)) { |fd| fd.read(3) }
229
+
230
+ if first3 == "---"
231
+ # file appears to have a YAML header so process it as a page
232
+ page = Page.new(self, self.source, dir, f)
233
+ page.render(self.layouts, site_payload)
234
+ page.write(self.dest)
235
+ else
236
+ # otherwise copy the file without transforming it
237
+ FileUtils.mkdir_p(File.join(self.dest, dir))
238
+ FileUtils.cp(File.join(self.source, dir, f), File.join(self.dest, dir, f))
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
244
+
245
+ # Transform all *.sass files from <dest> to css with the same name
246
+ # and delete source sass files.
247
+ # Returns nothing
248
+ def transform_sass(dir = '')
249
+ base = File.join(self.source, dir)
250
+ entries = Dir.entries(base)
251
+ entries = entries.reject { |e| ['.', '_'].include?(e[0..0]) }
252
+ directories = entries.select { |e| File.directory?(File.join(base, e)) }
253
+ directories.each { |d| transform_sass(File.join(dir, d)) }
254
+ files = entries.reject { |e| File.directory?(File.join(base, e)) }
255
+ files = files.select { |f| File.extname(File.join(base, f)) == ".sass" }
256
+ files.each do |f|
257
+ input = File.open(File.join(base, f), "r")
258
+ result = Sass::Engine.new(input.read, :style => :compact, :load_paths => base).render
259
+ FileUtils.mkdir_p(File.join(self.dest, dir))
260
+ output = File.open(File.join(self.dest, dir, f).gsub(/.sass\Z/, ".css"), "w") do |o|
261
+ o.write(result)
262
+ end
263
+ FileUtils.rm(File.join(self.dest, dir, f))
264
+ end
265
+ end
266
+
267
+ # Constructs a hash map of Posts indexed by the specified Post attribute
268
+ #
269
+ # Returns {post_attr => [<Post>]}
270
+ def post_attr_hash(post_attr)
271
+ # Build a hash map based on the specified post attribute ( post attr => array of posts )
272
+ # then sort each array in reverse order
273
+ hash = Hash.new { |hash, key| hash[key] = Array.new }
274
+ self.posts.each { |p| p.send(post_attr.to_sym).each { |t| hash[t] << p } }
275
+ hash.values.map { |sortme| sortme.sort! { |a, b| b <=> a} }
276
+ return hash
277
+ end
278
+
279
+ # The Hash payload containing site-wide data
280
+ #
281
+ # Returns {"site" => {"time" => <Time>,
282
+ # "posts" => [<Post>],
283
+ # "categories" => [<Post>],
284
+ # "tags" => [<Post>],
285
+ # "topics" => [<Post>] }}
286
+ def site_payload
287
+ {"site" => {
288
+ "time" => Time.now,
289
+ "posts" => self.posts.sort { |a,b| b <=> a },
290
+ "categories" => post_attr_hash('categories'),
291
+ "tags" => post_attr_hash('tags'),
292
+ "topics" => post_attr_hash('topics')
293
+ }}
294
+ end
295
+
296
+ # Filter out any files/directories that are hidden or backup files (start
297
+ # with "." or "#" or end with "~") or contain site content (start with "_")
298
+ # unless they are "_posts" directories or web server files such as
299
+ # '.htaccess'
300
+ def filter_entries(entries)
301
+ entries = entries.reject do |e|
302
+ unless ['_posts', '.htaccess'].include?(e)
303
+ # Reject backup/hidden
304
+ ['.', '_', '#'].include?(e[0..0]) or e[-1..-1] == '~'
305
+ end
306
+ end
307
+ end
308
+
309
+ end
310
+ end
@@ -0,0 +1,68 @@
1
+ module Jekyll
2
+
3
+ class HighlightBlock < Liquid::Block
4
+ include Liquid::StandardFilters
5
+
6
+ # we need a language, but the linenos argument is optional.
7
+ SYNTAX = /(\w+)\s?(:?linenos)?\s?/
8
+
9
+ def initialize(tag_name, markup, tokens)
10
+ super
11
+ if markup =~ SYNTAX
12
+ @lang = $1
13
+ if defined? $2
14
+ # additional options to pass to Albino.
15
+ @options = { 'O' => 'linenos=inline' }
16
+ else
17
+ @options = {}
18
+ end
19
+ else
20
+ raise SyntaxError.new("Syntax Error in 'highlight' - Valid syntax: highlight <lang> [linenos]")
21
+ end
22
+ end
23
+
24
+ def render(context)
25
+ if context.registers[:site].pygments
26
+ render_pygments(context, super.to_s)
27
+ else
28
+ render_codehighlighter(context, super.to_s)
29
+ end
30
+ end
31
+
32
+ def render_pygments(context, code)
33
+ if cache_dir = context.registers[:site].pygments_cache
34
+ path = File.join(cache_dir, "#{@lang}-#{Digest::MD5.hexdigest(code)}.html")
35
+ if File.exist?(path)
36
+ highlighted_code = File.read(path)
37
+ else
38
+ highlighted_code = Albino.new(code, @lang).to_s(@options)
39
+ File.open(path, 'w') {|f| f.print(highlighted_code) }
40
+ end
41
+ else
42
+ highlighted_code = Albino.new(code, @lang).to_s(@options)
43
+ end
44
+
45
+ if context["content_type"] == :markdown
46
+ return "\n" + highlighted_code + "\n"
47
+ elsif context["content_type"] == :textile
48
+ return "<notextile>" + highlighted_code + "</notextile>"
49
+ else
50
+ return highlighted_code
51
+ end
52
+ end
53
+
54
+ def render_codehighlighter(context, code)
55
+ #The div is required because RDiscount blows ass
56
+ <<-HTML
57
+ <div>
58
+ <pre>
59
+ <code class='#{@lang}'>#{h(code).strip}</code>
60
+ </pre>
61
+ </div>
62
+ HTML
63
+ end
64
+ end
65
+
66
+ end
67
+
68
+ Liquid::Template.register_tag('highlight', Jekyll::HighlightBlock)
@@ -0,0 +1,31 @@
1
+ module Jekyll
2
+
3
+ class IncludeTag < Liquid::Tag
4
+ def initialize(tag_name, file, tokens)
5
+ super
6
+ @file = file.strip
7
+ end
8
+
9
+ def render(context)
10
+ if @file !~ /^[a-zA-Z0-9_\/\.-]+$/ || @file =~ /\.\// || @file =~ /\/\./
11
+ return "Include file '#{@file}' contains invalid characters or sequences"
12
+ end
13
+
14
+ Dir.chdir(File.join(context.registers[:site].source, '_includes')) do
15
+ choices = Dir['**/*'].reject { |x| File.symlink?(x) }
16
+ if choices.include?(@file)
17
+ source = File.read(@file)
18
+ partial = Liquid::Template.parse(source)
19
+ context.stack do
20
+ partial.render(context)
21
+ end
22
+ else
23
+ "Included file '#{@file}' not found in _includes directory"
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ end
30
+
31
+ Liquid::Template.register_tag('include', Jekyll::IncludeTag)
data/lib/jekyll.rb ADDED
@@ -0,0 +1,84 @@
1
+ $:.unshift File.dirname(__FILE__) # For use/testing when no gem is installed
2
+
3
+ # rubygems
4
+ require 'rubygems'
5
+
6
+ # core
7
+ require 'fileutils'
8
+ require 'time'
9
+ require 'yaml'
10
+
11
+ # stdlib
12
+
13
+ # 3rd party
14
+ require 'liquid'
15
+ require 'redcloth'
16
+
17
+ # internal requires
18
+ require 'jekyll/core_ext'
19
+ require 'jekyll/site'
20
+ require 'jekyll/convertible'
21
+ require 'jekyll/layout'
22
+ require 'jekyll/page'
23
+ require 'jekyll/post'
24
+ require 'jekyll/filters'
25
+ require 'jekyll/tags/highlight'
26
+ require 'jekyll/tags/include'
27
+ require 'jekyll/albino'
28
+
29
+ module Jekyll
30
+ # Default options. Overriden by values in _config.yml or command-line opts.
31
+ # Strings are used instead of symbols for YAML compatibility.
32
+ DEFAULTS = {
33
+ 'auto' => false,
34
+ 'server' => false,
35
+ 'server_port' => 4000,
36
+
37
+ 'source' => '.',
38
+ 'destination' => File.join('.', '_site'),
39
+
40
+ 'lsi' => false,
41
+ 'pygments' => false,
42
+ 'sass' => false,
43
+ 'markdown' => 'maruku',
44
+ 'permalink' => 'date',
45
+
46
+ 'maruku' => {
47
+ 'use_tex' => false,
48
+ 'use_divs' => false,
49
+ 'png_engine' => 'blahtex',
50
+ 'png_dir' => 'images/latex',
51
+ 'png_url' => '/images/latex'
52
+ }
53
+ }
54
+
55
+ # Generate a Jekyll configuration Hash by merging the default options
56
+ # with anything in _config.yml, and adding the given options on top
57
+ # +override+ is a Hash of config directives
58
+ #
59
+ # Returns Hash
60
+ def self.configuration(override)
61
+ # _config.yml may override default source location, but until
62
+ # then, we need to know where to look for _config.yml
63
+ source = override['source'] || Jekyll::DEFAULTS['source']
64
+
65
+ # Get configuration from <source>/_config.yml
66
+ config = {}
67
+ config_file = File.join(source, '_config.yml')
68
+ begin
69
+ config = YAML.load_file(config_file)
70
+ puts "Configuration from #{config_file}"
71
+ rescue => err
72
+ puts "WARNING: Could not read configuration. Using defaults (and options)."
73
+ puts "\t" + err
74
+ end
75
+
76
+ # Merge DEFAULTS < _config.yml < override
77
+ Jekyll::DEFAULTS.deep_merge(config).deep_merge(override)
78
+ end
79
+
80
+ def self.version
81
+ yml = YAML.load(File.read(File.join(File.dirname(__FILE__), *%w[.. VERSION.yml])))
82
+ "#{yml[:major]}.#{yml[:minor]}.#{yml[:patch]}"
83
+ end
84
+ end