calavera-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 +548 -0
  3. data/VERSION.yml +4 -0
  4. data/bin/jekyll +150 -0
  5. data/lib/jekyll.rb +68 -0
  6. data/lib/jekyll/albino.rb +120 -0
  7. data/lib/jekyll/converters/csv.rb +26 -0
  8. data/lib/jekyll/converters/mephisto.rb +78 -0
  9. data/lib/jekyll/converters/mt.rb +59 -0
  10. data/lib/jekyll/converters/textpattern.rb +50 -0
  11. data/lib/jekyll/converters/typo.rb +49 -0
  12. data/lib/jekyll/converters/wordpress.rb +55 -0
  13. data/lib/jekyll/convertible.rb +71 -0
  14. data/lib/jekyll/core_ext.rb +29 -0
  15. data/lib/jekyll/filters.rb +55 -0
  16. data/lib/jekyll/layout.rb +33 -0
  17. data/lib/jekyll/page.rb +67 -0
  18. data/lib/jekyll/pager.rb +47 -0
  19. data/lib/jekyll/post.rb +201 -0
  20. data/lib/jekyll/site.rb +198 -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 +23 -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 +36 -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 +212 -0
@@ -0,0 +1,47 @@
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 = Jekyll.paginate)
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
+ end
9
+
10
+ def self.pagination_enabled?(base, file)
11
+ File.basename(file) == 'index.html' &&
12
+ (!Jekyll.paginate.nil? ||
13
+ (File.yaml?(File.join(base, file)) &&
14
+ Page.new(base, '', file).data.key?('paginate')))
15
+ end
16
+
17
+ def initialize(page, all_posts, num_pages = nil, per_page = nil)
18
+ @page = page
19
+ @per_page = per_page || Jekyll.paginate
20
+ @total_pages = num_pages || Pager.calculate_pages(all_posts)
21
+
22
+ if @page > @total_pages
23
+ raise RuntimeError, "page number can't be grater than total pages: #{@page} > #{@total_pages}"
24
+ end
25
+
26
+ init = (@page - 1) * @per_page
27
+ offset = (init + @per_page - 1) >= all_posts.size ? all_posts.size : (init + @per_page - 1)
28
+
29
+ @total_posts = all_posts.size
30
+ @posts = all_posts[init..offset]
31
+ @previous_page = @page != 1 ? @page - 1 : nil
32
+ @next_page = @page != @total_pages ? @page + 1 : nil
33
+ end
34
+
35
+ def to_hash
36
+ {
37
+ 'page' => page,
38
+ 'per_page' => per_page,
39
+ 'posts' => posts,
40
+ 'total_posts' => total_posts,
41
+ 'total_pages' => total_pages,
42
+ 'previous_page' => previous_page,
43
+ 'next_page' => next_page
44
+ }
45
+ end
46
+ end
47
+ end
@@ -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,198 @@
1
+ module Jekyll
2
+
3
+ class Site
4
+ attr_accessor :source, :dest
5
+ attr_accessor :layouts, :posts, :categories
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
+ end
21
+
22
+ # Do the actual work of processing the site and generating the
23
+ # real deal.
24
+ #
25
+ # Returns nothing
26
+ def process
27
+ self.read_layouts
28
+ self.transform_pages
29
+ self.write_posts
30
+ end
31
+
32
+ # Read all the files in <source>/_layouts into memory for later use.
33
+ #
34
+ # Returns nothing
35
+ def read_layouts
36
+ base = File.join(self.source, "_layouts")
37
+ entries = []
38
+ Dir.chdir(base) { entries = filter_entries(Dir['*.*']) }
39
+
40
+ entries.each do |f|
41
+ name = f.split(".")[0..-2].join(".")
42
+ self.layouts[name] = Layout.new(base, f)
43
+ end
44
+ rescue Errno::ENOENT => e
45
+ # ignore missing layout dir
46
+ end
47
+
48
+ # Read all the files in <base>/_posts and create a new Post object with each one.
49
+ #
50
+ # Returns nothing
51
+ def read_posts(dir)
52
+ base = File.join(self.source, dir, '_posts')
53
+ entries = []
54
+ Dir.chdir(base) { entries = filter_entries(Dir['**/*']) }
55
+
56
+ # first pass processes, but does not yet render post content
57
+ entries.each do |f|
58
+ if Post.valid?(f)
59
+ post = Post.new(self.source, dir, f)
60
+
61
+ if post.published
62
+ self.posts << post
63
+ post.categories.each { |c| self.categories[c] << post }
64
+ end
65
+ end
66
+ end
67
+
68
+ # second pass renders each post now that full site payload is available
69
+ self.posts.each do |post|
70
+ post.render(self.layouts, site_payload)
71
+ end
72
+
73
+ self.posts.sort!
74
+ self.categories.values.map { |cats| cats.sort! { |a, b| b <=> a} }
75
+ rescue Errno::ENOENT => e
76
+ # ignore missing layout dir
77
+ end
78
+
79
+ # Write each post to <dest>/<year>/<month>/<day>/<slug>
80
+ #
81
+ # Returns nothing
82
+ def write_posts
83
+ self.posts.each do |post|
84
+ post.write(self.dest)
85
+ end
86
+ end
87
+
88
+ # Copy all regular files from <source> to <dest>/ ignoring
89
+ # any files/directories that are hidden or backup files (start
90
+ # with "." or "#" or end with "~") or contain site content (start with "_")
91
+ # unless they are "_posts" directories or web server files such as
92
+ # '.htaccess'
93
+ # The +dir+ String is a relative path used to call this method
94
+ # recursively as it descends through directories
95
+ #
96
+ # Returns nothing
97
+ def transform_pages(dir = '')
98
+ base = File.join(self.source, dir)
99
+ entries = filter_entries(Dir.entries(base))
100
+ directories = entries.select { |e| File.directory?(File.join(base, e)) }
101
+ files = entries.reject { |e| File.directory?(File.join(base, e)) }
102
+
103
+ # we need to make sure to process _posts *first* otherwise they
104
+ # might not be available yet to other templates as {{ site.posts }}
105
+ if directories.include?('_posts')
106
+ directories.delete('_posts')
107
+ read_posts(dir)
108
+ end
109
+ [directories, files].each do |entries|
110
+ entries.each do |f|
111
+ if File.directory?(File.join(base, f))
112
+ next if self.dest.sub(/\/$/, '') == File.join(base, f)
113
+ transform_pages(File.join(dir, f))
114
+ elsif Pager.pagination_enabled?(base, f)
115
+ paginate_posts(f, dir)
116
+ elsif File.yaml? File.join(base, f)
117
+ # file appears to have a YAML header so process it as a page
118
+ page = Page.new(self.source, dir, f)
119
+ page.render(self.layouts, site_payload)
120
+ page.write(self.dest)
121
+ else
122
+ # otherwise copy the file without transforming it
123
+ FileUtils.mkdir_p(File.join(self.dest, dir))
124
+ FileUtils.cp(File.join(self.source, dir, f), File.join(self.dest, dir, f))
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ # Constructs a hash map of Posts indexed by the specified Post attribute
131
+ #
132
+ # Returns {post_attr => [<Post>]}
133
+ def post_attr_hash(post_attr)
134
+ # Build a hash map based on the specified post attribute ( post attr => array of posts )
135
+ # then sort each array in reverse order
136
+ hash = Hash.new { |hash, key| hash[key] = Array.new }
137
+ self.posts.each { |p| p.send(post_attr.to_sym).each { |t| hash[t] << p } }
138
+ hash.values.map { |sortme| sortme.sort! { |a, b| b <=> a} }
139
+ return hash
140
+ end
141
+
142
+ # The Hash payload containing site-wide data
143
+ #
144
+ # Returns {"site" => {"time" => <Time>,
145
+ # "posts" => [<Post>],
146
+ # "categories" => [<Post>],
147
+ # "topics" => [<Post>] }}
148
+ def site_payload
149
+ {"site" => {
150
+ "time" => Time.now,
151
+ "posts" => self.posts.sort { |a,b| b <=> a },
152
+ "categories" => post_attr_hash('categories'),
153
+ "topics" => post_attr_hash('topics')
154
+ }}
155
+ end
156
+
157
+ # Paginates the blog's posts. Renders the index.html file into paginated directories, ie: page2, page3...
158
+ # and adds more wite-wide data
159
+ #
160
+ # {"paginator" => { "page" => <Number>,
161
+ # "per_page" => <Number>,
162
+ # "posts" => [<Post>],
163
+ # "total_posts" => <Number>,
164
+ # "total_pages" => <Number>,
165
+ # "previous_page" => <Number>,
166
+ # "next_page" => <Number> }}
167
+ def paginate_posts(file, dir)
168
+ all_posts = self.posts.sort { |a,b| b <=> a }
169
+ page = Page.new(self.source, dir, file)
170
+
171
+ per_page = page.data['paginate'] || Jekyll.paginate
172
+ pages = Pager.calculate_pages(all_posts, per_page)
173
+
174
+ (1..pages).each do |num_page|
175
+ pager = Pager.new(num_page, all_posts, pages, per_page)
176
+
177
+ page.render(self.layouts, site_payload.merge({'paginator' => pager.to_hash}))
178
+ suffix = "page#{num_page}" if num_page > 1
179
+ page.write(self.dest, suffix)
180
+ end
181
+ end
182
+ end
183
+
184
+ # Filter out any files/directories that are hidden or backup files (start
185
+ # with "." or "#" or end with "~") or contain site content (start with "_")
186
+ # unless they are "_posts" directories or web server files such as
187
+ # '.htaccess'
188
+ def filter_entries(entries)
189
+ entries = entries.reject do |e|
190
+ unless ['_posts', '.htaccess'].include?(e)
191
+ # Reject backup/hidden
192
+ ['.', '_', '#'].include?(e[0..0]) or e[-1..-1] == '~'
193
+ end
194
+ end
195
+ end
196
+
197
+ end
198
+ 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)