matflores-jekyll 0.4.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 (43) hide show
  1. data/History.txt +113 -0
  2. data/README.textile +508 -0
  3. data/VERSION.yml +4 -0
  4. data/bin/jekyll +136 -0
  5. data/lib/jekyll.rb +69 -0
  6. data/lib/jekyll/albino.rb +120 -0
  7. data/lib/jekyll/archive.rb +25 -0
  8. data/lib/jekyll/converters/csv.rb +26 -0
  9. data/lib/jekyll/converters/mephisto.rb +79 -0
  10. data/lib/jekyll/converters/mt.rb +59 -0
  11. data/lib/jekyll/converters/textpattern.rb +50 -0
  12. data/lib/jekyll/converters/typo.rb +49 -0
  13. data/lib/jekyll/converters/wordpress.rb +55 -0
  14. data/lib/jekyll/convertible.rb +71 -0
  15. data/lib/jekyll/core_ext.rb +22 -0
  16. data/lib/jekyll/filters.rb +50 -0
  17. data/lib/jekyll/layout.rb +33 -0
  18. data/lib/jekyll/page.rb +64 -0
  19. data/lib/jekyll/post.rb +201 -0
  20. data/lib/jekyll/site.rb +241 -0
  21. data/lib/jekyll/tags/highlight.rb +53 -0
  22. data/lib/jekyll/tags/include.rb +31 -0
  23. data/test/helper.rb +14 -0
  24. data/test/source/_includes/sig.markdown +3 -0
  25. data/test/source/_layouts/default.html +27 -0
  26. data/test/source/_layouts/simple.html +1 -0
  27. data/test/source/_posts/2008-10-18-foo-bar.textile +8 -0
  28. data/test/source/_posts/2008-11-21-complex.textile +8 -0
  29. data/test/source/_posts/2008-12-03-permalinked-post.textile +9 -0
  30. data/test/source/_posts/2008-12-13-include.markdown +8 -0
  31. data/test/source/category/_posts/2008-9-23-categories.textile +6 -0
  32. data/test/source/css/screen.css +76 -0
  33. data/test/source/foo/_posts/bar/2008-12-12-topical-post.textile +8 -0
  34. data/test/source/index.html +22 -0
  35. data/test/source/z_category/_posts/2008-9-23-categories.textile +6 -0
  36. data/test/suite.rb +9 -0
  37. data/test/test_filters.rb +41 -0
  38. data/test/test_generated_site.rb +60 -0
  39. data/test/test_jekyll.rb +0 -0
  40. data/test/test_post.rb +141 -0
  41. data/test/test_site.rb +45 -0
  42. data/test/test_tags.rb +31 -0
  43. metadata +170 -0
@@ -0,0 +1,201 @@
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 :date, :slug, :ext, :categories, :topics, :published
22
+ attr_accessor :data, :content, :output
23
+
24
+ # Initialize this Post instance.
25
+ # +base+ is the String path to the dir containing the post file
26
+ # +name+ is the String filename of the post file
27
+ # +categories+ is an Array of Strings for the categories for this post
28
+ #
29
+ # Returns <Post>
30
+ def initialize(source, dir, name)
31
+ @base = File.join(source, dir, '_posts')
32
+ @name = name
33
+
34
+ self.categories = dir.split('/').reject { |x| x.empty? }
35
+
36
+ parts = name.split('/')
37
+ self.topics = parts.size > 1 ? parts[0..-2] : []
38
+
39
+ self.process(name)
40
+ self.read_yaml(@base, name)
41
+
42
+ if self.data.has_key?('published') && self.data['published'] == false
43
+ self.published = false
44
+ else
45
+ self.published = true
46
+ end
47
+
48
+ if self.categories.empty?
49
+ if self.data.has_key?('category')
50
+ self.categories << self.data['category']
51
+ elsif self.data.has_key?('categories')
52
+ # Look for categories in the YAML-header, either specified as
53
+ # an array or a string.
54
+ if self.data['categories'].kind_of? String
55
+ self.categories = self.data['categories'].split
56
+ else
57
+ self.categories = self.data['categories']
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ # Spaceship is based on Post#date
64
+ #
65
+ # Returns -1, 0, 1
66
+ def <=>(other)
67
+ self.date <=> other.date
68
+ end
69
+
70
+ # Extract information from the post filename
71
+ # +name+ is the String filename of the post file
72
+ #
73
+ # Returns nothing
74
+ def process(name)
75
+ m, cats, date, slug, ext = *name.match(MATCHER)
76
+ self.date = Time.parse(date)
77
+ self.slug = slug
78
+ self.ext = ext
79
+ end
80
+
81
+ # The generated directory into which the post will be placed
82
+ # upon generation. This is derived from the permalink or, if
83
+ # permalink is absent, set to the default date
84
+ # e.g. "/2008/11/05/" if the permalink style is :date, otherwise nothing
85
+ #
86
+ # Returns <String>
87
+ def dir
88
+ if permalink
89
+ permalink.to_s.split("/")[0..-2].join("/") + '/'
90
+ else
91
+ prefix = self.categories.empty? ? '' : '/' + self.categories.join('/')
92
+ if [:date, :pretty].include?(Jekyll.permalink_style)
93
+ prefix + date.strftime("/%Y/%m/%d/")
94
+ else
95
+ prefix + '/'
96
+ end
97
+ end
98
+ end
99
+
100
+ # The full path and filename of the post.
101
+ # Defined in the YAML of the post body
102
+ # (Optional)
103
+ #
104
+ # Returns <String>
105
+ def permalink
106
+ self.data && self.data['permalink']
107
+ end
108
+
109
+ # The generated relative url of this post
110
+ # e.g. /2008/11/05/my-awesome-post.html
111
+ #
112
+ # Returns <String>
113
+ def url
114
+ ext = Jekyll.permalink_style == :pretty ? '' : '.html'
115
+ permalink || self.id + ext
116
+ end
117
+
118
+ # The UID for this post (useful in feeds)
119
+ # e.g. /2008/11/05/my-awesome-post
120
+ #
121
+ # Returns <String>
122
+ def id
123
+ self.dir + self.slug
124
+ end
125
+
126
+ # Calculate related posts.
127
+ #
128
+ # Returns [<Post>]
129
+ def related_posts(posts)
130
+ return [] unless posts.size > 1
131
+
132
+ if Jekyll.lsi
133
+ self.class.lsi ||= begin
134
+ puts "Running the classifier... this could take a while."
135
+ lsi = Classifier::LSI.new
136
+ posts.each { |x| $stdout.print(".");$stdout.flush;lsi.add_item(x) }
137
+ puts ""
138
+ lsi
139
+ end
140
+
141
+ related = self.class.lsi.find_related(self.content, 11)
142
+ related - [self]
143
+ else
144
+ (posts - [self])[0..9]
145
+ end
146
+ end
147
+
148
+ # Add any necessary layouts to this post
149
+ # +layouts+ is a Hash of {"name" => "layout"}
150
+ # +site_payload+ is the site payload hash
151
+ #
152
+ # Returns nothing
153
+ def render(layouts, site_payload)
154
+ # construct payload
155
+ payload =
156
+ {
157
+ "site" => { "related_posts" => related_posts(site_payload["site"]["posts"]) },
158
+ "page" => self.to_liquid
159
+ }
160
+ payload = payload.deep_merge(site_payload)
161
+
162
+ do_layout(payload, layouts)
163
+ end
164
+
165
+ # Write the generated post file to the destination directory.
166
+ # +dest+ is the String path to the destination dir
167
+ #
168
+ # Returns nothing
169
+ def write(dest)
170
+ FileUtils.mkdir_p(File.join(dest, dir))
171
+
172
+ path = File.join(dest, self.url)
173
+
174
+ if Jekyll.permalink_style == :pretty
175
+ FileUtils.mkdir_p(path)
176
+ path = File.join(path, "index.html")
177
+ end
178
+
179
+ File.open(path, 'w') do |f|
180
+ f.write(self.output)
181
+ end
182
+ end
183
+
184
+ # Convert this post into a Hash for use in Liquid templates.
185
+ #
186
+ # Returns <Hash>
187
+ def to_liquid
188
+ { "title" => self.data["title"] || self.slug.split('-').select {|w| w.capitalize! || w }.join(' '),
189
+ "url" => self.url,
190
+ "date" => self.date,
191
+ "id" => self.id,
192
+ "topics" => self.topics,
193
+ "content" => self.content }.deep_merge(self.data)
194
+ end
195
+
196
+ def inspect
197
+ "<Post: #{self.id}>"
198
+ end
199
+ end
200
+
201
+ end
@@ -0,0 +1,241 @@
1
+ module Jekyll
2
+
3
+ class Site
4
+ attr_accessor :source, :dest
5
+ attr_accessor :layouts, :posts, :categories, :collated
6
+
7
+ # Initialize the site
8
+ # +source+ is String path to the source directory containing
9
+ # the proto-site
10
+ # +dest+ is the String path to the directory where the generated
11
+ # site should be written
12
+ #
13
+ # Returns <Site>
14
+ def initialize(source, dest)
15
+ self.source = source
16
+ self.dest = dest
17
+ self.layouts = {}
18
+ self.posts = []
19
+ self.categories = Hash.new { |hash, key| hash[key] = Array.new }
20
+ self.collated = {}
21
+ end
22
+
23
+ # Do the actual work of processing the site and generating the
24
+ # real deal.
25
+ #
26
+ # Returns nothing
27
+ def process
28
+ self.read_layouts
29
+ self.transform_pages
30
+ self.write_posts
31
+ self.write_archives
32
+ self.write_category_indexes
33
+ end
34
+
35
+ # Read all the files in <source>/_layouts into memory for later use.
36
+ #
37
+ # Returns nothing
38
+ def read_layouts
39
+ base = File.join(self.source, "_layouts")
40
+ entries = []
41
+ Dir.chdir(base) { entries = filter_entries(Dir['*.*']) }
42
+
43
+ entries.each do |f|
44
+ name = f.split(".")[0..-2].join(".")
45
+ self.layouts[name] = Layout.new(base, f)
46
+ end
47
+ rescue Errno::ENOENT => e
48
+ # ignore missing layout dir
49
+ end
50
+
51
+ # Read all the files in <base>/_posts and create a new Post object with each one.
52
+ #
53
+ # Returns nothing
54
+ def read_posts(dir)
55
+ base = File.join(self.source, dir, '_posts')
56
+ entries = []
57
+ Dir.chdir(base) { entries = filter_entries(Dir['**/*']) }
58
+
59
+ # first pass processes, but does not yet render post content
60
+ entries.each do |f|
61
+ if Post.valid?(f)
62
+ post = Post.new(self.source, dir, f)
63
+
64
+ if post.published
65
+ self.posts << post
66
+ post.categories.each { |c| self.categories[c] << post }
67
+ end
68
+ end
69
+ end
70
+
71
+ # second pass renders each post now that full site payload is available
72
+ self.posts.each do |post|
73
+ post.render(self.layouts, site_payload)
74
+ end
75
+
76
+ self.posts.sort!
77
+ self.categories.values.map { |cats| cats.sort! { |a, b| b <=> a} }
78
+
79
+ # build collated post structure for archives
80
+ self.posts.reverse.each do |post|
81
+ y, m, d = post.date.year, post.date.month, post.date.day
82
+ unless self.collated.key? y
83
+ self.collated[y] = {}
84
+ end
85
+ unless self.collated[y].key? m
86
+ self.collated[y][m] = {}
87
+ end
88
+ unless self.collated[y][m].key? d
89
+ self.collated[y][m][d] = []
90
+ end
91
+ self.collated[y][m][d] += [post]
92
+ end
93
+ rescue Errno::ENOENT => e
94
+ # ignore missing layout dir
95
+ end
96
+
97
+ # Write each post to <dest>/<year>/<month>/<day>/<slug>
98
+ #
99
+ # Returns nothing
100
+ def write_posts
101
+ self.posts.each do |post|
102
+ post.write(self.dest)
103
+ end
104
+ end
105
+
106
+ def write_archive(dir, type)
107
+ archive = Archive.new(self.source, dir, type)
108
+ archive.render(self.layouts, site_payload)
109
+ archive.write(self.dest)
110
+ end
111
+
112
+ # Write out archive pages based on special layouts. Yearly,
113
+ # monthly, and daily archives will be written if layouts exist.
114
+ # Yearly archives will be in <dest>/<year>/index.html and other archives
115
+ # will be generated similarly.
116
+ #
117
+ # Returns nothing.
118
+ def write_archives
119
+ self.collated.keys.each do |year|
120
+ if self.layouts.key? 'archive_yearly'
121
+ self.write_archive(year.to_s, 'archive_yearly')
122
+ end
123
+
124
+ self.collated[year].keys.each do |month|
125
+ if self.layouts.key? 'archive_monthly'
126
+ self.write_archive(File.join(year.to_s, "%02d" % month), 'archive_monthly')
127
+ end
128
+
129
+ self.collated[year][month].keys.each do |day|
130
+ if self.layouts.key? 'archive_daily'
131
+ self.write_archive(File.join(year.to_s, "%02d" % month, "%02d" % day), 'archive_daily')
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ def write_category_index(dir, category)
139
+ index = CategoryIndex.new(self.source, dir, category)
140
+ index.render(self.layouts, site_payload)
141
+ index.write(self.dest)
142
+ end
143
+
144
+ # Write out category indexes if a layout called category_index.html exists
145
+ # The category indexes will be created in <dest>/category/<category>/index.html
146
+ def write_category_indexes
147
+ if self.layouts.key? 'category_index'
148
+ self.categories.keys.each do |category|
149
+ self.write_category_index(File.join('category', category), category)
150
+ end
151
+ end
152
+ end
153
+
154
+ # Copy all regular files from <source> to <dest>/ ignoring
155
+ # any files/directories that are hidden or backup files (start
156
+ # with "." or "#" or end with "~") or contain site content (start with "_")
157
+ # unless they are "_posts" directories or web server files such as
158
+ # '.htaccess'
159
+ # The +dir+ String is a relative path used to call this method
160
+ # recursively as it descends through directories
161
+ #
162
+ # Returns nothing
163
+ def transform_pages(dir = '')
164
+ base = File.join(self.source, dir)
165
+ entries = filter_entries(Dir.entries(base))
166
+ directories = entries.select { |e| File.directory?(File.join(base, e)) }
167
+ files = entries.reject { |e| File.directory?(File.join(base, e)) }
168
+
169
+ # we need to make sure to process _posts *first* otherwise they
170
+ # might not be available yet to other templates as {{ site.posts }}
171
+ if directories.include?('_posts')
172
+ directories.delete('_posts')
173
+ read_posts(dir)
174
+ end
175
+ [directories, files].each do |entries|
176
+ entries.each do |f|
177
+ if File.directory?(File.join(base, f))
178
+ next if self.dest.sub(/\/$/, '') == File.join(base, f)
179
+ transform_pages(File.join(dir, f))
180
+ else
181
+ first3 = File.open(File.join(self.source, dir, f)) { |fd| fd.read(3) }
182
+
183
+ if first3 == "---"
184
+ # file appears to have a YAML header so process it as a page
185
+ page = Page.new(self.source, dir, f)
186
+ page.render(self.layouts, site_payload)
187
+ page.write(self.dest)
188
+ else
189
+ # otherwise copy the file without transforming it
190
+ FileUtils.mkdir_p(File.join(self.dest, dir))
191
+ FileUtils.cp(File.join(self.source, dir, f), File.join(self.dest, dir, f))
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
197
+
198
+ # Constructs a hash map of Posts indexed by the specified Post attribute
199
+ #
200
+ # Returns {post_attr => [<Post>]}
201
+ def post_attr_hash(post_attr)
202
+ # Build a hash map based on the specified post attribute ( post attr => array of posts )
203
+ # then sort each array in reverse order
204
+ hash = Hash.new { |hash, key| hash[key] = Array.new }
205
+ self.posts.each { |p| p.send(post_attr.to_sym).each { |t| hash[t] << p } }
206
+ hash.values.map { |sortme| sortme.sort! { |a, b| b <=> a} }
207
+ return hash
208
+ end
209
+
210
+ # The Hash payload containing site-wide data
211
+ #
212
+ # Returns {"site" => {"time" => <Time>,
213
+ # "posts" => [<Post>],
214
+ # "collated_posts" => [<Post>],
215
+ # "categories" => [<Post>],
216
+ # "topics" => [<Post>] }}
217
+ def site_payload
218
+ {"site" => {
219
+ "time" => Time.now,
220
+ "posts" => self.posts.sort { |a,b| b <=> a },
221
+ "collated_posts" => self.collated,
222
+ "categories" => post_attr_hash('categories'),
223
+ "topics" => post_attr_hash('topics')
224
+ }}
225
+ end
226
+
227
+ # Filter out any files/directories that are hidden or backup files (start
228
+ # with "." or "#" or end with "~") or contain site content (start with "_")
229
+ # unless they are "_posts" directories or web server files such as
230
+ # '.htaccess'
231
+ def filter_entries(entries)
232
+ entries = entries.reject do |e|
233
+ unless ['_posts', '.htaccess'].include?(e)
234
+ # Reject backup/hidden
235
+ ['.', '_', '#'].include?(e[0..0]) or e[-1..-1] == '~'
236
+ end
237
+ end
238
+ end
239
+
240
+ end
241
+ end
@@ -0,0 +1,53 @@
1
+ module Jekyll
2
+
3
+ class HighlightBlock < Liquid::Block
4
+ include Liquid::StandardFilters
5
+ # we need a language, but the linenos argument is optional.
6
+ SYNTAX = /(\w+)\s?(:?linenos)?\s?/
7
+
8
+ def initialize(tag_name, markup, tokens)
9
+ super
10
+ if markup =~ SYNTAX
11
+ @lang = $1
12
+ if defined? $2
13
+ # additional options to pass to Albino.
14
+ @options = { 'O' => 'linenos=inline' }
15
+ else
16
+ @options = {}
17
+ end
18
+ else
19
+ raise SyntaxError.new("Syntax Error in 'highlight' - Valid syntax: highlight <lang> [linenos]")
20
+ end
21
+ end
22
+
23
+ def render(context)
24
+ if Jekyll.pygments
25
+ render_pygments(context, super.to_s)
26
+ else
27
+ render_codehighlighter(context, super.to_s)
28
+ end
29
+ end
30
+
31
+ def render_pygments(context, code)
32
+ if Jekyll.content_type == :markdown
33
+ return "\n" + Albino.new(code, @lang).to_s(@options) + "\n"
34
+ else
35
+ "<notextile>" + Albino.new(code, @lang).to_s(@options) + "</notextile>"
36
+ end
37
+ end
38
+
39
+ def render_codehighlighter(context, code)
40
+ #The div is required because RDiscount blows ass
41
+ <<-HTML
42
+ <div>
43
+ <pre>
44
+ <code class='#{@lang}'>#{h(code).strip}</code>
45
+ </pre>
46
+ </div>
47
+ HTML
48
+ end
49
+ end
50
+
51
+ end
52
+
53
+ Liquid::Template.register_tag('highlight', Jekyll::HighlightBlock)