nirvdrum-jekyll 0.5.2

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 (74) hide show
  1. data/.gitignore +7 -0
  2. data/History.txt +143 -0
  3. data/README.textile +42 -0
  4. data/Rakefile +92 -0
  5. data/VERSION.yml +4 -0
  6. data/bin/jekyll +150 -0
  7. data/features/create_sites.feature +60 -0
  8. data/features/embed_filters.feature +60 -0
  9. data/features/pagination.feature +40 -0
  10. data/features/permalinks.feature +63 -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/jekyll.gemspec +134 -0
  17. data/lib/jekyll.rb +86 -0
  18. data/lib/jekyll/albino.rb +122 -0
  19. data/lib/jekyll/converters/csv.rb +26 -0
  20. data/lib/jekyll/converters/mephisto.rb +79 -0
  21. data/lib/jekyll/converters/mt.rb +59 -0
  22. data/lib/jekyll/converters/textpattern.rb +50 -0
  23. data/lib/jekyll/converters/typo.rb +49 -0
  24. data/lib/jekyll/converters/wordpress.rb +54 -0
  25. data/lib/jekyll/convertible.rb +89 -0
  26. data/lib/jekyll/core_ext.rb +30 -0
  27. data/lib/jekyll/filters.rb +47 -0
  28. data/lib/jekyll/layout.rb +36 -0
  29. data/lib/jekyll/page.rb +112 -0
  30. data/lib/jekyll/pager.rb +45 -0
  31. data/lib/jekyll/post.rb +251 -0
  32. data/lib/jekyll/site.rb +295 -0
  33. data/lib/jekyll/stylesheet.rb +88 -0
  34. data/lib/jekyll/tags/highlight.rb +56 -0
  35. data/lib/jekyll/tags/include.rb +31 -0
  36. data/test/helper.rb +27 -0
  37. data/test/source/_includes/sig.markdown +3 -0
  38. data/test/source/_layouts/default.html +27 -0
  39. data/test/source/_layouts/simple.html +1 -0
  40. data/test/source/_posts/2008-02-02-not-published.textile +8 -0
  41. data/test/source/_posts/2008-02-02-published.textile +8 -0
  42. data/test/source/_posts/2008-10-18-foo-bar.textile +8 -0
  43. data/test/source/_posts/2008-11-21-complex.textile +8 -0
  44. data/test/source/_posts/2008-12-03-permalinked-post.textile +9 -0
  45. data/test/source/_posts/2008-12-13-include.markdown +8 -0
  46. data/test/source/_posts/2009-01-27-array-categories.textile +10 -0
  47. data/test/source/_posts/2009-01-27-categories.textile +7 -0
  48. data/test/source/_posts/2009-01-27-category.textile +7 -0
  49. data/test/source/_posts/2009-03-12-hash-#1.markdown +6 -0
  50. data/test/source/_posts/2009-05-18-tag.textile +6 -0
  51. data/test/source/_posts/2009-05-18-tags.textile +9 -0
  52. data/test/source/_posts/2009-06-22-empty-yaml.textile +3 -0
  53. data/test/source/_posts/2009-06-22-no-yaml.textile +1 -0
  54. data/test/source/_stylesheets/nested/override.less +1 -0
  55. data/test/source/_stylesheets/simple.less +3 -0
  56. data/test/source/about.html +6 -0
  57. data/test/source/category/_posts/2008-9-23-categories.textile +6 -0
  58. data/test/source/contacts.html +5 -0
  59. data/test/source/css/screen.css +76 -0
  60. data/test/source/foo/_posts/bar/2008-12-12-topical-post.textile +8 -0
  61. data/test/source/index.html +22 -0
  62. data/test/source/win/_posts/2009-05-24-yaml-linebreak.markdown +7 -0
  63. data/test/source/z_category/_posts/2008-9-23-categories.textile +6 -0
  64. data/test/suite.rb +9 -0
  65. data/test/test_configuration.rb +29 -0
  66. data/test/test_filters.rb +49 -0
  67. data/test/test_generated_site.rb +53 -0
  68. data/test/test_page.rb +87 -0
  69. data/test/test_pager.rb +47 -0
  70. data/test/test_post.rb +302 -0
  71. data/test/test_site.rb +85 -0
  72. data/test/test_stylesheet.rb +67 -0
  73. data/test/test_tags.rb +116 -0
  74. metadata +196 -0
@@ -0,0 +1,295 @@
1
+ module Jekyll
2
+
3
+ class Site
4
+ attr_accessor :config, :layouts, :posts, :categories, :exclude,
5
+ :source, :dest, :lsi, :pygments, :permalink_style, :tags
6
+
7
+ # Initialize the site
8
+ # +config+ is a Hash containing site configurations details
9
+ #
10
+ # Returns <Site>
11
+ def initialize(config)
12
+ self.config = config.clone
13
+
14
+ self.source = config['source']
15
+ self.dest = config['destination']
16
+ self.lsi = config['lsi']
17
+ self.pygments = config['pygments']
18
+ self.permalink_style = config['permalink'].to_sym
19
+ self.exclude = config['exclude'] || []
20
+
21
+ self.reset
22
+ self.setup
23
+ end
24
+
25
+ def reset
26
+ self.layouts = {}
27
+ self.posts = []
28
+ self.categories = Hash.new { |hash, key| hash[key] = [] }
29
+ self.tags = Hash.new { |hash, key| hash[key] = [] }
30
+ end
31
+
32
+ def setup
33
+ # Check to see if LSI is enabled.
34
+ require 'classifier' if self.lsi
35
+
36
+ # Set the Markdown interpreter (and Maruku self.config, if necessary)
37
+ case self.config['markdown']
38
+ when 'rdiscount'
39
+ begin
40
+ require 'rdiscount'
41
+
42
+ def markdown(content)
43
+ RDiscount.new(content).to_html
44
+ end
45
+
46
+ rescue LoadError
47
+ puts 'You must have the rdiscount gem installed first'
48
+ end
49
+ when 'maruku'
50
+ begin
51
+ require 'maruku'
52
+
53
+ def markdown(content)
54
+ Maruku.new(content).to_html
55
+ end
56
+
57
+ if self.config['maruku']['use_divs']
58
+ require 'maruku/ext/div'
59
+ puts 'Maruku: Using extended syntax for div elements.'
60
+ end
61
+
62
+ if self.config['maruku']['use_tex']
63
+ require 'maruku/ext/math'
64
+ puts "Maruku: Using LaTeX extension. Images in `#{self.config['maruku']['png_dir']}`."
65
+
66
+ # Switch off MathML output
67
+ MaRuKu::Globals[:html_math_output_mathml] = false
68
+ MaRuKu::Globals[:html_math_engine] = 'none'
69
+
70
+ # Turn on math to PNG support with blahtex
71
+ # Resulting PNGs stored in `images/latex`
72
+ MaRuKu::Globals[:html_math_output_png] = true
73
+ MaRuKu::Globals[:html_png_engine] = self.config['maruku']['png_engine']
74
+ MaRuKu::Globals[:html_png_dir] = self.config['maruku']['png_dir']
75
+ MaRuKu::Globals[:html_png_url] = self.config['maruku']['png_url']
76
+ end
77
+ rescue LoadError
78
+ puts "The maruku gem is required for markdown support!"
79
+ end
80
+ else
81
+ raise "Invalid Markdown processor: '#{self.config['markdown']}' -- did you mean 'maruku' or 'rdiscount'?"
82
+ end
83
+
84
+ begin
85
+ require 'less'
86
+
87
+ def less(content)
88
+ Less::Engine.new(content).to_css
89
+ end
90
+ rescue LoadError
91
+ # Less is not required for stylesheet handling.
92
+ def less(content)
93
+ raise "The less gem is required for stylesheet support!"
94
+ end
95
+ end
96
+ end
97
+
98
+ def textile(content)
99
+ RedCloth.new(content).to_html
100
+ end
101
+
102
+ # Do the actual work of processing the site and generating the
103
+ # real deal.
104
+ #
105
+ # Returns nothing
106
+ def process
107
+ self.reset
108
+ self.read_layouts
109
+ self.transform_stylesheets
110
+ self.transform_pages
111
+ self.write_posts
112
+ end
113
+
114
+ # Read all the files in <source>/_layouts into memory for later use.
115
+ #
116
+ # Returns nothing
117
+ def read_layouts
118
+ base = File.join(self.source, "_layouts")
119
+ entries = []
120
+ Dir.chdir(base) { entries = filter_entries(Dir['*.*']) }
121
+
122
+ entries.each do |f|
123
+ name = f.split(".")[0..-2].join(".")
124
+ self.layouts[name] = Layout.new(self, base, f)
125
+ end
126
+ rescue Errno::ENOENT => e
127
+ # ignore missing layout dir
128
+ end
129
+
130
+ # Read all the files in <base>/_posts and create a new Post object with each one.
131
+ #
132
+ # Returns nothing
133
+ def read_posts(dir)
134
+ base = File.join(self.source, dir, '_posts')
135
+ entries = []
136
+ Dir.chdir(base) { entries = filter_entries(Dir['**/*']) }
137
+
138
+ # first pass processes, but does not yet render post content
139
+ entries.each do |f|
140
+ if Post.valid?(f)
141
+ post = Post.new(self, self.source, dir, f)
142
+
143
+ if post.published
144
+ self.posts << post
145
+ post.categories.each { |c| self.categories[c] << post }
146
+ post.tags.each { |c| self.tags[c] << post }
147
+ end
148
+ end
149
+ end
150
+
151
+ self.posts.sort!
152
+
153
+ # second pass renders each post now that full site payload is available
154
+ self.posts.each do |post|
155
+ post.render(self.layouts, site_payload)
156
+ end
157
+
158
+ self.categories.values.map { |ps| ps.sort! { |a, b| b <=> a} }
159
+ self.tags.values.map { |ps| ps.sort! { |a, b| b <=> a} }
160
+ rescue Errno::ENOENT => e
161
+ # ignore missing layout dir
162
+ end
163
+
164
+ # Write each post to <dest>/<year>/<month>/<day>/<slug>
165
+ #
166
+ # Returns nothing
167
+ def write_posts
168
+ self.posts.each do |post|
169
+ post.write(self.dest)
170
+ end
171
+ end
172
+
173
+ # Copy all regular files from <source> to <dest>/ ignoring
174
+ # any files/directories that are hidden or backup files (start
175
+ # with "." or "#" or end with "~") or contain site content (start with "_")
176
+ # unless they are "_posts" directories or web server files such as
177
+ # '.htaccess'
178
+ # The +dir+ String is a relative path used to call this method
179
+ # recursively as it descends through directories
180
+ #
181
+ # Returns nothing
182
+ def transform_pages(dir = '')
183
+ base = File.join(self.source, dir)
184
+ entries = filter_entries(Dir.entries(base))
185
+ directories = entries.select { |e| File.directory?(File.join(base, e)) }
186
+ files = entries.reject { |e| File.directory?(File.join(base, e)) }
187
+
188
+ # we need to make sure to process _posts *first* otherwise they
189
+ # might not be available yet to other templates as {{ site.posts }}
190
+ if directories.include?('_posts')
191
+ directories.delete('_posts')
192
+ read_posts(dir)
193
+ end
194
+
195
+ [directories, files].each do |entries|
196
+ entries.each do |f|
197
+ if File.directory?(File.join(base, f))
198
+ next if self.dest.sub(/\/$/, '') == File.join(base, f)
199
+ transform_pages(File.join(dir, f))
200
+ elsif Pager.pagination_enabled?(self.config, f)
201
+ paginate_posts(f, dir)
202
+ else
203
+ first3 = File.open(File.join(self.source, dir, f)) { |fd| fd.read(3) }
204
+
205
+ if first3 == "---"
206
+ # file appears to have a YAML header so process it as a page
207
+ page = Page.new(self, self.source, dir, f)
208
+ page.render(self.layouts, site_payload)
209
+ page.write(self.dest)
210
+ else
211
+ # otherwise copy the file without transforming it
212
+ FileUtils.mkdir_p(File.join(self.dest, dir))
213
+ FileUtils.cp(File.join(self.source, dir, f), File.join(self.dest, dir, f))
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end
219
+
220
+ def transform_stylesheets
221
+ dir = "_stylesheets"
222
+ base = File.join(self.source, dir)
223
+ entries = []
224
+ Dir.chdir(base) { entries = filter_entries(Dir['**/*.*']) }
225
+
226
+ entries.each do |f|
227
+ stylesheet = Stylesheet.new(self, self.source, dir, f)
228
+ stylesheet.render()
229
+ stylesheet.write(self.dest)
230
+ end
231
+ rescue Errno::ENOENT => e
232
+ # ignore missing stylesheet dir
233
+ end
234
+
235
+ # Constructs a hash map of Posts indexed by the specified Post attribute
236
+ #
237
+ # Returns {post_attr => [<Post>]}
238
+ def post_attr_hash(post_attr)
239
+ # Build a hash map based on the specified post attribute ( post attr => array of posts )
240
+ # then sort each array in reverse order
241
+ hash = Hash.new { |hash, key| hash[key] = Array.new }
242
+ self.posts.each { |p| p.send(post_attr.to_sym).each { |t| hash[t] << p } }
243
+ hash.values.map { |sortme| sortme.sort! { |a, b| b <=> a} }
244
+ return hash
245
+ end
246
+
247
+ # The Hash payload containing site-wide data
248
+ #
249
+ # Returns {"site" => {"time" => <Time>,
250
+ # "posts" => [<Post>],
251
+ # "categories" => [<Post>]}
252
+ def site_payload
253
+ {"site" => self.config.merge({
254
+ "time" => Time.now,
255
+ "posts" => self.posts.sort { |a,b| b <=> a },
256
+ "categories" => post_attr_hash('categories'),
257
+ "tags" => post_attr_hash('tags')})}
258
+ end
259
+
260
+ # Filter out any files/directories that are hidden or backup files (start
261
+ # with "." or "#" or end with "~") or contain site content (start with "_")
262
+ # unless they are "_posts" directories or web server files such as
263
+ # '.htaccess'
264
+ def filter_entries(entries)
265
+ entries = entries.reject do |e|
266
+ unless ['_posts', '.htaccess'].include?(e)
267
+ ['.', '_', '#'].include?(e[0..0]) || e[-1..-1] == '~' || self.exclude.include?(e)
268
+ end
269
+ end
270
+ end
271
+
272
+ # Paginates the blog's posts. Renders the index.html file into paginated directories, ie: page2, page3...
273
+ # and adds more wite-wide data
274
+ #
275
+ # {"paginator" => { "page" => <Number>,
276
+ # "per_page" => <Number>,
277
+ # "posts" => [<Post>],
278
+ # "total_posts" => <Number>,
279
+ # "total_pages" => <Number>,
280
+ # "previous_page" => <Number>,
281
+ # "next_page" => <Number> }}
282
+ def paginate_posts(file, dir)
283
+ all_posts = self.posts.sort { |a,b| b <=> a }
284
+ pages = Pager.calculate_pages(all_posts, self.config['paginate'].to_i)
285
+ pages += 1
286
+ (1..pages).each do |num_page|
287
+ pager = Pager.new(self.config, num_page, all_posts, pages)
288
+ page = Page.new(self, self.source, dir, file)
289
+ page.render(self.layouts, site_payload.merge({'paginator' => pager.to_hash}))
290
+ suffix = "page#{num_page}" if num_page > 1
291
+ page.write(self.dest, suffix)
292
+ end
293
+ end
294
+ end
295
+ end
@@ -0,0 +1,88 @@
1
+ module Jekyll
2
+
3
+ class Stylesheet
4
+ include Convertible
5
+
6
+ attr_accessor :site
7
+ attr_accessor :name, :ext, :basename
8
+ attr_accessor :data, :content, :output
9
+
10
+ # Initialize a new Page.
11
+ # +site+ is the Site
12
+ # +base+ is the String path to the <source>
13
+ # +dir+ is the String path between <source> and the file
14
+ # +name+ is the String filename of the file
15
+ #
16
+ # Returns <Page>
17
+ def initialize(site, base, dir, name)
18
+ @site = site
19
+ @base = base
20
+ @dir = dir
21
+ @name = name
22
+
23
+ self.process(name)
24
+ self.content = File.read(File.join(File.join(base, dir), name))
25
+ end
26
+
27
+ # The generated directory into which the page will be placed
28
+ # upon generation. This is derived from the permalink or, if
29
+ # permalink is absent, set to '/'
30
+ #
31
+ # Returns <String>
32
+ def dir
33
+ url[-1, 1] == '/' ? url : File.dirname(url)
34
+ end
35
+
36
+ # The generated relative url of this stylesheet
37
+ # e.g. /css/site.css
38
+ #
39
+ # Returns <String>
40
+ def url
41
+ @url ||= "/css/#{basename}.css"
42
+ end
43
+
44
+ # Extract information from the page filename
45
+ # +name+ is the String filename of the page file
46
+ #
47
+ # Returns nothing
48
+ def process(name)
49
+ self.ext = File.extname(name)
50
+ self.basename = name.split('.')[0..-2].first
51
+ end
52
+
53
+ # Add any necessary layouts to this post
54
+ # +layouts+ is a Hash of {"name" => "layout"}
55
+ # +site_payload+ is the site payload hash
56
+ #
57
+ # Returns nothing
58
+ def render()
59
+ self.transform
60
+
61
+ # output keeps track of what will finally be written
62
+ self.output = self.content
63
+ end
64
+
65
+ # Write the generated page file to the destination directory.
66
+ # +dest_prefix+ is the String path to the destination dir
67
+ # +dest_suffix+ is a suffix path to the destination dir
68
+ #
69
+ # Returns nothing
70
+ def write(dest_prefix, dest_suffix = nil)
71
+ dest = dest_suffix ? File.join(dest_prefix, dest_suffix) : dest_prefix
72
+ FileUtils.mkdir_p(dest)
73
+
74
+ # The url needs to be unescaped in order to preserve the correct filename
75
+ path = File.join(dest, CGI.unescape(self.url))
76
+ dirname = File.dirname(path)
77
+ unless File.exists?(dirname)
78
+ FileUtils.mkdir_p(dirname)
79
+ end
80
+
81
+ File.open(path, 'w') do |f|
82
+ f.write(self.output)
83
+ end
84
+ end
85
+
86
+ end
87
+
88
+ end
@@ -0,0 +1,56 @@
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 context["content_type"] == "markdown"
34
+ return "\n" + Albino.new(code, @lang).to_s(@options) + "\n"
35
+ elsif context["content_type"] == "textile"
36
+ return "<notextile>" + Albino.new(code, @lang).to_s(@options) + "</notextile>"
37
+ else
38
+ return Albino.new(code, @lang).to_s(@options)
39
+ end
40
+ end
41
+
42
+ def render_codehighlighter(context, code)
43
+ #The div is required because RDiscount blows ass
44
+ <<-HTML
45
+ <div>
46
+ <pre>
47
+ <code class='#{@lang}'>#{h(code).strip}</code>
48
+ </pre>
49
+ </div>
50
+ HTML
51
+ end
52
+ end
53
+
54
+ end
55
+
56
+ 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)