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