jberkel-jekyll 0.5.4

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 (73) hide show
  1. data/.gitignore +6 -0
  2. data/History.txt +151 -0
  3. data/README.textile +42 -0
  4. data/Rakefile +90 -0
  5. data/VERSION.yml +4 -0
  6. data/bin/jekyll +168 -0
  7. data/features/create_sites.feature +46 -0
  8. data/features/embed_filters.feature +60 -0
  9. data/features/pagination.feature +40 -0
  10. data/features/permalinks.feature +65 -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 +141 -0
  17. data/lib/jekyll/albino.rb +122 -0
  18. data/lib/jekyll/converters/csv.rb +26 -0
  19. data/lib/jekyll/converters/marley.rb +53 -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 +117 -0
  26. data/lib/jekyll/core_ext.rb +38 -0
  27. data/lib/jekyll/filters.rb +51 -0
  28. data/lib/jekyll/haml_helpers.rb +15 -0
  29. data/lib/jekyll/layout.rb +37 -0
  30. data/lib/jekyll/page.rb +112 -0
  31. data/lib/jekyll/pager.rb +45 -0
  32. data/lib/jekyll/post.rb +310 -0
  33. data/lib/jekyll/site.rb +316 -0
  34. data/lib/jekyll/tags/highlight.rb +56 -0
  35. data/lib/jekyll/tags/include.rb +31 -0
  36. data/lib/jekyll.rb +86 -0
  37. data/test/helper.rb +27 -0
  38. data/test/source/_includes/sig.markdown +3 -0
  39. data/test/source/_layouts/default.html +27 -0
  40. data/test/source/_layouts/simple.html +1 -0
  41. data/test/source/_posts/2008-02-02-not-published.textile +8 -0
  42. data/test/source/_posts/2008-02-02-published.textile +8 -0
  43. data/test/source/_posts/2008-10-18-foo-bar.textile +8 -0
  44. data/test/source/_posts/2008-11-21-complex.textile +8 -0
  45. data/test/source/_posts/2008-12-03-permalinked-post.textile +9 -0
  46. data/test/source/_posts/2008-12-13-include.markdown +8 -0
  47. data/test/source/_posts/2009-01-27-array-categories.textile +10 -0
  48. data/test/source/_posts/2009-01-27-categories.textile +7 -0
  49. data/test/source/_posts/2009-01-27-category.textile +7 -0
  50. data/test/source/_posts/2009-03-12-hash-#1.markdown +6 -0
  51. data/test/source/_posts/2009-05-18-tag.textile +6 -0
  52. data/test/source/_posts/2009-05-18-tags.textile +9 -0
  53. data/test/source/_posts/2009-06-22-empty-yaml.textile +3 -0
  54. data/test/source/_posts/2009-06-22-no-yaml.textile +1 -0
  55. data/test/source/about.html +6 -0
  56. data/test/source/category/_posts/2008-9-23-categories.textile +6 -0
  57. data/test/source/contacts.html +5 -0
  58. data/test/source/css/screen.css +76 -0
  59. data/test/source/foo/_posts/bar/2008-12-12-topical-post.textile +8 -0
  60. data/test/source/index.html +22 -0
  61. data/test/source/sitemap.xml +23 -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 +40 -0
  68. data/test/test_page.rb +98 -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_tags.rb +116 -0
  73. metadata +194 -0
@@ -0,0 +1,51 @@
1
+ module Jekyll
2
+
3
+ module Filters
4
+ def textilize(input)
5
+ RedCloth.new(input).to_html
6
+ end
7
+
8
+ def date_to_string(date)
9
+ date.strftime("%d %b %Y")
10
+ end
11
+
12
+ def date_to_long_string(date)
13
+ date.strftime("%d %B %Y")
14
+ end
15
+
16
+ def date_to_xmlschema(date)
17
+ date.xmlschema
18
+ end
19
+
20
+ def date_to_utc(date)
21
+ date.utc
22
+ end
23
+
24
+ def xml_escape(input)
25
+ CGI.escapeHTML(input)
26
+ end
27
+
28
+ def cgi_escape(input)
29
+ CGI::escape(input)
30
+ end
31
+
32
+ def number_of_words(input)
33
+ input.split.length
34
+ end
35
+
36
+ def array_to_sentence_string(array)
37
+ connector = "and"
38
+ case array.length
39
+ when 0
40
+ ""
41
+ when 1
42
+ array[0].to_s
43
+ when 2
44
+ "#{array[0]} #{connector} #{array[1]}"
45
+ else
46
+ "#{array[0...-1].join(', ')}, #{connector} #{array[-1]}"
47
+ end
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,15 @@
1
+ require 'cgi'
2
+
3
+ module Jekyll
4
+ module HamlHelpers
5
+
6
+ def h(text)
7
+ CGI.escapeHTML(text)
8
+ end
9
+
10
+ def link_to(text, url)
11
+ %{<a href="#{h url}">#{text}</a>}
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,37 @@
1
+ module Jekyll
2
+
3
+ class Layout
4
+ include Convertible
5
+
6
+ attr_accessor :site
7
+ attr_accessor :ext
8
+ attr_accessor :data, :content, :name
9
+
10
+ # Initialize a new Layout.
11
+ # +site+ is the Site
12
+ # +base+ is the String path to the <source>
13
+ # +name+ is the String filename of the post file
14
+ #
15
+ # Returns <Page>
16
+ def initialize(site, base, name)
17
+ @site = site
18
+ @base = base
19
+ @name = name
20
+
21
+ self.data = {}
22
+
23
+ self.process(name)
24
+ self.read_yaml(base, name)
25
+ self.transform
26
+ end
27
+
28
+ # Extract information from the layout filename
29
+ # +name+ is the String filename of the layout file
30
+ #
31
+ # Returns nothing
32
+ def process(name)
33
+ self.ext = File.extname(name)
34
+ end
35
+ end
36
+
37
+ end
@@ -0,0 +1,112 @@
1
+ module Jekyll
2
+
3
+ class Page
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.read_yaml(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 full path and filename of the post.
37
+ # Defined in the YAML of the post body
38
+ # (Optional)
39
+ #
40
+ # Returns <String>
41
+ def permalink
42
+ self.data && self.data['permalink']
43
+ end
44
+
45
+ def template
46
+ if self.site.permalink_style == :pretty && !index?
47
+ "/:name/"
48
+ else
49
+ "/:name.html"
50
+ end
51
+ end
52
+
53
+ # The generated relative url of this page
54
+ # e.g. /about.html
55
+ #
56
+ # Returns <String>
57
+ def url
58
+ return permalink if permalink
59
+
60
+ @url ||= (ext == '.html') ? template.gsub(':name', basename) : "/#{name}"
61
+ end
62
+
63
+ # Extract information from the page filename
64
+ # +name+ is the String filename of the page file
65
+ #
66
+ # Returns nothing
67
+ def process(name)
68
+ self.ext = File.extname(name)
69
+ self.basename = name.split('.')[0..-2].first
70
+ end
71
+
72
+ # Add any necessary layouts to this post
73
+ # +layouts+ is a Hash of {"name" => "layout"}
74
+ # +site_payload+ is the site payload hash
75
+ #
76
+ # Returns nothing
77
+ def render(layouts, site_payload)
78
+ payload = {"page" => self.data}.deep_merge(site_payload)
79
+ do_layout(payload, layouts)
80
+ end
81
+
82
+ # Write the generated page file to the destination directory.
83
+ # +dest_prefix+ is the String path to the destination dir
84
+ # +dest_suffix+ is a suffix path to the destination dir
85
+ #
86
+ # Returns nothing
87
+ def write(dest_prefix, dest_suffix = nil)
88
+ dest = File.join(dest_prefix, @dir)
89
+ dest = File.join(dest, dest_suffix) if dest_suffix
90
+ FileUtils.mkdir_p(dest)
91
+
92
+ # The url needs to be unescaped in order to preserve the correct filename
93
+ path = File.join(dest, CGI.unescape(self.url))
94
+ if self.ext == '.html' && self.url[/\.html$/].nil?
95
+ FileUtils.mkdir_p(path)
96
+ path = File.join(path, "index.html")
97
+ end
98
+
99
+ File.open(path, 'w') do |f|
100
+ f.write(self.output)
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ def index?
107
+ basename == 'index'
108
+ end
109
+
110
+ end
111
+
112
+ end
@@ -0,0 +1,45 @@
1
+ module Jekyll
2
+ class Pager
3
+ attr_reader :page, :per_page, :posts, :total_posts, :total_pages, :previous_page, :next_page
4
+
5
+ def self.calculate_pages(all_posts, per_page)
6
+ num_pages = all_posts.size / per_page.to_i
7
+ num_pages.abs + 1 if all_posts.size % per_page.to_i != 0
8
+ num_pages
9
+ end
10
+
11
+ def self.pagination_enabled?(config, file)
12
+ file == 'index.html' && !config['paginate'].nil?
13
+ end
14
+
15
+ def initialize(config, page, all_posts, num_pages = nil)
16
+ @page = page
17
+ @per_page = config['paginate'].to_i
18
+ @total_pages = num_pages || Pager.calculate_pages(all_posts, @per_page)
19
+
20
+ if @page > @total_pages
21
+ raise RuntimeError, "page number can't be greater than total pages: #{@page} > #{@total_pages}"
22
+ end
23
+
24
+ init = (@page - 1) * @per_page
25
+ offset = (init + @per_page - 1) >= all_posts.size ? all_posts.size : (init + @per_page - 1)
26
+
27
+ @total_posts = all_posts.size
28
+ @posts = all_posts[init..offset]
29
+ @previous_page = @page != 1 ? @page - 1 : nil
30
+ @next_page = @page != @total_pages ? @page + 1 : nil
31
+ end
32
+
33
+ def to_hash
34
+ {
35
+ 'page' => page,
36
+ 'per_page' => per_page,
37
+ 'posts' => posts,
38
+ 'total_posts' => total_posts,
39
+ 'total_pages' => total_pages,
40
+ 'previous_page' => previous_page,
41
+ 'next_page' => next_page
42
+ }
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,310 @@
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+(?:_\d+-\d+)?)-(.*)(\.[^.]+)$/
12
+
13
+ # Post name validator. Post filenames must be like:
14
+ # 2008-11-05-my-awesome-post.textile
15
+ # or:
16
+ # 2008-11-05_12-45-my-awesome-post.textile
17
+ #
18
+ # Returns <Bool>
19
+ def self.valid?(name)
20
+ name =~ MATCHER
21
+ end
22
+
23
+ attr_accessor :site, :date, :slug, :ext, :published, :data, :content, :output, :tags
24
+ attr_writer :categories
25
+
26
+ def categories
27
+ @categories ||= []
28
+ end
29
+
30
+ # Initialize this Post instance.
31
+ # +site+ is the Site
32
+ # +base+ is the String path to the dir containing the post file
33
+ # +name+ is the String filename of the post file
34
+ # +categories+ is an Array of Strings for the categories for this post
35
+ # +tags+ is an Array of Strings for the tags for this post
36
+ #
37
+ # Returns <Post>
38
+ def initialize(site, source, dir, name)
39
+ @site = site
40
+ @base = File.join(source, dir, '_posts')
41
+ @name = name
42
+
43
+ self.categories = dir.split('/').reject { |x| x.empty? }
44
+ self.process(name)
45
+ self.data = self.site.post_defaults.dup
46
+ self.read_yaml(@base, name)
47
+
48
+ extract_title_from_first_header_or_slug
49
+
50
+ if self.data.has_key?('published') && self.data['published'] == false
51
+ self.published = false
52
+ else
53
+ self.published = true
54
+ end
55
+
56
+ if self.data.has_key?("tag")
57
+ self.tags = [self.data["tag"]]
58
+ elsif self.data.has_key?("tags")
59
+ self.tags = self.data['tags']
60
+ else
61
+ self.tags = []
62
+ end
63
+
64
+ if self.categories.empty?
65
+ if self.data.has_key?('category')
66
+ self.categories << self.data['category']
67
+ elsif self.data.has_key?('categories')
68
+ # Look for categories in the YAML-header, either specified as
69
+ # an array or a string.
70
+ if self.data['categories'].kind_of? String
71
+ self.categories = self.data['categories'].split
72
+ else
73
+ self.categories = self.data['categories']
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ # Spaceship is based on Post#date, slug
80
+ #
81
+ # Returns -1, 0, 1
82
+ def <=>(other)
83
+ cmp = self.date <=> other.date
84
+ if 0 == cmp
85
+ cmp = self.slug <=> other.slug
86
+ end
87
+ return cmp
88
+ end
89
+
90
+ # Extract information from the post filename
91
+ # +name+ is the String filename of the post file
92
+ #
93
+ # Returns nothing
94
+ def process(name)
95
+ m, cats, date, slug, ext = *name.match(MATCHER)
96
+ date = date.sub(/_(\d+)-(\d+)\Z/, ' \1:\2') # Make optional time part parsable.
97
+ self.date = Time.parse(date)
98
+ self.slug = slug
99
+ self.ext = ext
100
+ end
101
+
102
+ # The generated directory into which the post will be placed
103
+ # upon generation. This is derived from the permalink or, if
104
+ # permalink is absent, set to the default date
105
+ # e.g. "/2008/11/05/" if the permalink style is :date, otherwise nothing
106
+ #
107
+ # Returns <String>
108
+ def dir
109
+ File.dirname(generated_path)
110
+ end
111
+
112
+ # The full path and filename of the post.
113
+ # Defined in the YAML of the post body
114
+ # (Optional)
115
+ #
116
+ # Returns <String>
117
+ def permalink
118
+ self.data && self.data['permalink']
119
+ end
120
+
121
+ def template
122
+ case self.site.permalink_style
123
+ when :pretty
124
+ "/:categories/:year/:month/:day/:title/"
125
+ when :none
126
+ "/:categories/:title.html"
127
+ when :date
128
+ "/:categories/:year/:month/:day/:title.html"
129
+ else
130
+ self.site.permalink_style.to_s
131
+ end
132
+ end
133
+
134
+ # The generated relative path of this post
135
+ # e.g. /2008/11/05/my-awesome-post.html
136
+ #
137
+ # Returns <String>
138
+ def generated_path
139
+ return permalink if permalink
140
+
141
+ @generated_path ||= {
142
+ "year" => date.strftime("%Y"),
143
+ "month" => date.strftime("%m"),
144
+ "day" => date.strftime("%d"),
145
+ "title" => CGI.escape(slug),
146
+ "categories" => categories.sort.join('/')
147
+ }.inject(template) { |result, token|
148
+ result.gsub(/:#{token.first}/, token.last)
149
+ }.gsub("//", "/")
150
+ end
151
+
152
+ # The generated relative url of this post
153
+ # e.g. /2008/11/05/my-awesome-post
154
+ #
155
+ # Returns <String>
156
+ def url
157
+ site.config['multiviews'] ? generated_path.sub(/\.html$/, '') : generated_path
158
+ end
159
+
160
+ # The UID for this post (useful in feeds)
161
+ # e.g. /2008/11/05/my-awesome-post
162
+ #
163
+ # Returns <String>
164
+ def id
165
+ File.join(self.dir, self.slug)
166
+ end
167
+
168
+ # The post title
169
+ #
170
+ # Returns <String>
171
+ def title
172
+ self.data && self.data["title"]
173
+ end
174
+
175
+ # The post date and time
176
+ #
177
+ # Returns <Time>
178
+ def date
179
+ @date_with_time ||= begin
180
+ if self.data && self.data.key?("time")
181
+ time = Time.parse(self.data["time"])
182
+ Time.mktime(@date.year, @date.month, @date.day, time.hour, time.min)
183
+ else
184
+ @date
185
+ end
186
+ end
187
+ end
188
+
189
+ # The path to the post file.
190
+ #
191
+ # Returns <String>
192
+ def path
193
+ File.expand_path(File.join(@base, @name))
194
+ end
195
+
196
+ # Calculate related posts.
197
+ #
198
+ # Returns [<Post>]
199
+ def related_posts(posts)
200
+ return [] unless posts.size > 1
201
+
202
+ if self.site.lsi
203
+ self.class.lsi ||= begin
204
+ puts "Running the classifier... this could take a while."
205
+ lsi = Classifier::LSI.new
206
+ posts.each { |x| $stdout.print(".");$stdout.flush;lsi.add_item(x) }
207
+ puts ""
208
+ lsi
209
+ end
210
+
211
+ related = self.class.lsi.find_related(self.content, 11)
212
+ related - [self]
213
+ else
214
+ (posts - [self])[0..9]
215
+ end
216
+ end
217
+
218
+ # Add any necessary layouts to this post
219
+ # +layouts+ is a Hash of {"name" => "layout"}
220
+ # +site_payload+ is the site payload hash
221
+ #
222
+ # Returns nothing
223
+ def render(layouts, site_payload)
224
+ # construct payload
225
+ payload =
226
+ {
227
+ "site" => { "related_posts" => related_posts(site_payload["site"]["posts"]) },
228
+ "page" => self.to_liquid
229
+ }
230
+ payload = payload.deep_merge(site_payload)
231
+
232
+ do_layout(payload, layouts)
233
+ end
234
+
235
+ # Write the generated post file to the destination directory.
236
+ # +dest+ is the String path to the destination dir
237
+ #
238
+ # Returns nothing
239
+ def write(dest)
240
+ FileUtils.mkdir_p(File.join(dest, dir))
241
+
242
+ # The url needs to be unescaped in order to preserve the correct filename
243
+ path = File.join(dest, CGI.unescape(self.url))
244
+
245
+ if template[/\.html$/].nil?
246
+ FileUtils.mkdir_p(path)
247
+ path = File.join(path, "index.html")
248
+ end
249
+
250
+ File.open(path, 'w') do |f|
251
+ f.write(self.output)
252
+ end
253
+ end
254
+
255
+ # Attempt to extract title from topmost header or slug.
256
+ #
257
+ # Returns <String>
258
+ def extract_title_from_first_header_or_slug
259
+ # Done before the transformation to HTML, or it won't go into <title>s.
260
+ self.data["title"] ||=
261
+ case content_type
262
+ when 'textile'
263
+ self.content[/\A\s*h\d\.\s*(.+)/, 1] # h1. Header
264
+ when 'markdown'
265
+ self.content[/\A\s*#+\s*(.+)\s*#*$/, 1] || # "# Header"
266
+ self.content[/\A\s*(\S.*)\r?\n\s*(-+|=+)\s*$/, 1] # "Header\n====="
267
+ end
268
+ self.data["title"] ||= self.slug.split('-').select {|w| w.capitalize! || w }.join(' ')
269
+ end
270
+
271
+ # Convert this post into a Hash for use in Liquid templates.
272
+ #
273
+ # Returns <Hash>
274
+ def to_liquid
275
+ { "title" => self.data["title"] || self.slug.split('-').select {|w| w.capitalize! || w }.join(' '),
276
+ "url" => self.url,
277
+ "date" => self.date,
278
+ "id" => self.id,
279
+ "categories" => self.categories,
280
+ "next" => self.next,
281
+ "previous" => self.previous,
282
+ "tags" => self.tags,
283
+ "content" => self.content }.deep_merge(self.data)
284
+ end
285
+
286
+ def inspect
287
+ "<Post: #{self.id}>"
288
+ end
289
+
290
+ def next
291
+ pos = self.site.posts.index(self)
292
+
293
+ if pos && pos < self.site.posts.length-1
294
+ self.site.posts[pos+1]
295
+ else
296
+ nil
297
+ end
298
+ end
299
+
300
+ def previous
301
+ pos = self.site.posts.index(self)
302
+ if pos && pos > 0
303
+ self.site.posts[pos-1]
304
+ else
305
+ nil
306
+ end
307
+ end
308
+ end
309
+
310
+ end