mattmatt-jekyll 0.4.0

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 (48) hide show
  1. data/History.txt +91 -0
  2. data/README.textile +494 -0
  3. data/Rakefile +76 -0
  4. data/TODO +3 -0
  5. data/VERSION.yml +4 -0
  6. data/bin/jekyll +142 -0
  7. data/lib/jekyll.rb +64 -0
  8. data/lib/jekyll/albino.rb +116 -0
  9. data/lib/jekyll/converters/csv.rb +26 -0
  10. data/lib/jekyll/converters/mephisto.rb +79 -0
  11. data/lib/jekyll/converters/mt.rb +59 -0
  12. data/lib/jekyll/converters/textpattern.rb +50 -0
  13. data/lib/jekyll/converters/typo.rb +49 -0
  14. data/lib/jekyll/converters/wordpress.rb +54 -0
  15. data/lib/jekyll/convertible.rb +71 -0
  16. data/lib/jekyll/core_ext.rb +22 -0
  17. data/lib/jekyll/filters.rb +39 -0
  18. data/lib/jekyll/layout.rb +33 -0
  19. data/lib/jekyll/page.rb +64 -0
  20. data/lib/jekyll/post.rb +194 -0
  21. data/lib/jekyll/site.rb +173 -0
  22. data/lib/jekyll/tags/highlight.rb +53 -0
  23. data/lib/jekyll/tags/include.rb +31 -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/css/screen.css +76 -0
  38. data/test/source/foo/_posts/bar/2008-12-12-topical-post.textile +8 -0
  39. data/test/source/index.html +22 -0
  40. data/test/source/z_category/_posts/2008-9-23-categories.textile +6 -0
  41. data/test/suite.rb +9 -0
  42. data/test/test_filters.rb +37 -0
  43. data/test/test_generated_site.rb +32 -0
  44. data/test/test_jekyll.rb +0 -0
  45. data/test/test_post.rb +144 -0
  46. data/test/test_site.rb +36 -0
  47. data/test/test_tags.rb +31 -0
  48. metadata +230 -0
@@ -0,0 +1,194 @@
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
+ if self.categories.empty?
50
+ if self.data.has_key?('category')
51
+ self.categories << self.data['category']
52
+ elsif self.data.has_key?('categories')
53
+ self.categories = self.data['categories'].split
54
+ end
55
+ end
56
+ end
57
+
58
+ # Spaceship is based on Post#date
59
+ #
60
+ # Returns -1, 0, 1
61
+ def <=>(other)
62
+ self.date <=> other.date
63
+ end
64
+
65
+ # Extract information from the post filename
66
+ # +name+ is the String filename of the post file
67
+ #
68
+ # Returns nothing
69
+ def process(name)
70
+ m, cats, date, slug, ext = *name.match(MATCHER)
71
+ self.date = Time.parse(date)
72
+ self.slug = slug
73
+ self.ext = ext
74
+ end
75
+
76
+ # The generated directory into which the post will be placed
77
+ # upon generation. This is derived from the permalink or, if
78
+ # permalink is absent, set to the default date
79
+ # e.g. "/2008/11/05/" if the permalink style is :date, otherwise nothing
80
+ #
81
+ # Returns <String>
82
+ def dir
83
+ if permalink
84
+ permalink.to_s.split("/")[0..-2].join("/") + '/'
85
+ else
86
+ prefix = self.categories.empty? ? '' : '/' + self.categories.join('/')
87
+ case Jekyll.permalink_style
88
+ when :date
89
+ prefix + date.strftime("/%Y/%m/%d/")
90
+ when :shortdate
91
+ prefix + "/#{date.year}/#{date.month}/#{date.day}/"
92
+ else
93
+ prefix + '/'
94
+ end
95
+ end
96
+ end
97
+
98
+ # The full path and filename of the post.
99
+ # Defined in the YAML of the post body
100
+ # (Optional)
101
+ #
102
+ # Returns <String>
103
+ def permalink
104
+ self.data && self.data['permalink']
105
+ end
106
+
107
+ # The generated relative url of this post
108
+ # e.g. /2008/11/05/my-awesome-post.html
109
+ #
110
+ # Returns <String>
111
+ def url
112
+ permalink || self.dir + self.slug + ".html"
113
+ end
114
+
115
+ # The UID for this post (useful in feeds)
116
+ # e.g. /2008/11/05/my-awesome-post
117
+ #
118
+ # Returns <String>
119
+ def id
120
+ self.dir + self.slug
121
+ end
122
+
123
+ # Calculate related posts.
124
+ #
125
+ # Returns [<Post>]
126
+ def related_posts(posts)
127
+ return [] unless posts.size > 1
128
+
129
+ if Jekyll.lsi
130
+ self.class.lsi ||= begin
131
+ puts "Running the classifier... this could take a while."
132
+ lsi = Classifier::LSI.new
133
+ posts.each { |x| $stdout.print(".");$stdout.flush;lsi.add_item(x) }
134
+ puts ""
135
+ lsi
136
+ end
137
+
138
+ related = self.class.lsi.find_related(self.content, 11)
139
+ related - [self]
140
+ else
141
+ (posts - [self])[0..9]
142
+ end
143
+ end
144
+
145
+ # Add any necessary layouts to this post
146
+ # +layouts+ is a Hash of {"name" => "layout"}
147
+ # +site_payload+ is the site payload hash
148
+ #
149
+ # Returns nothing
150
+ def render(layouts, site_payload)
151
+ # construct payload
152
+ payload =
153
+ {
154
+ "site" => { "related_posts" => related_posts(site_payload["site"]["posts"]) },
155
+ "page" => self.to_liquid,
156
+ "previous" => self.previous,
157
+ "next" => self.next
158
+ }
159
+ payload = payload.deep_merge(site_payload)
160
+
161
+ do_layout(payload, layouts)
162
+ end
163
+
164
+ # Write the generated post file to the destination directory.
165
+ # +dest+ is the String path to the destination dir
166
+ #
167
+ # Returns nothing
168
+ def write(dest)
169
+ FileUtils.mkdir_p(File.join(dest, dir))
170
+
171
+ path = File.join(dest, self.url)
172
+ File.open(path, 'w') do |f|
173
+ f.write(self.output)
174
+ end
175
+ end
176
+
177
+ # Convert this post into a Hash for use in Liquid templates.
178
+ #
179
+ # Returns <Hash>
180
+ def to_liquid
181
+ { "title" => self.data["title"] || self.slug.split('-').select {|w| w.capitalize! || w }.join(' '),
182
+ "url" => self.url,
183
+ "date" => self.date,
184
+ "id" => self.id,
185
+ "topics" => self.topics,
186
+ "content" => self.content }.deep_merge(self.data)
187
+ end
188
+
189
+ def inspect
190
+ "<Post: #{self.id}>"
191
+ end
192
+ end
193
+
194
+ end
@@ -0,0 +1,173 @@
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 except backup files
33
+ # (end with "~") into memory for later use.
34
+ #
35
+ # Returns nothing
36
+ def read_layouts
37
+ base = File.join(self.source, "_layouts")
38
+ entries = Dir.entries(base)
39
+ entries = entries.reject { |e| e[-1..-1] == '~' }
40
+ entries = entries.reject { |e| File.directory?(File.join(base, e)) }
41
+
42
+ entries.each do |f|
43
+ name = f.split(".")[0..-2].join(".")
44
+ self.layouts[name] = Layout.new(base, f)
45
+ end
46
+ rescue Errno::ENOENT => e
47
+ # ignore missing layout dir
48
+ end
49
+
50
+ # Read all the files in <base>/_posts except backup files (end with "~")
51
+ # 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
+
57
+ entries = []
58
+ Dir.chdir(base) { entries = Dir['**/*'] }
59
+ entries = entries.reject { |e| e[-1..-1] == '~' }
60
+ entries = entries.reject { |e| File.directory?(File.join(base, e)) }
61
+
62
+ # first pass processes, but does not yet render post content
63
+ entries.each do |f|
64
+ if Post.valid?(f)
65
+ post = Post.new(self.source, dir, f)
66
+
67
+ if post.published
68
+ self.posts << post
69
+ post.categories.each { |c| self.categories[c] << post }
70
+ end
71
+ end
72
+ end
73
+
74
+ self.posts.sort!
75
+
76
+ # second pass renders each post now that full site payload is available
77
+ self.posts.each_with_index do |post, idx|
78
+ post.previous = posts[idx - 1] unless idx - 1 < 0
79
+ post.next = posts[idx + 1] unless idx + 1 >= posts.size
80
+ post.render(self.layouts, site_payload)
81
+ end
82
+
83
+ self.categories.values.map { |cats| cats.sort! { |a, b| b <=> a} }
84
+ rescue Errno::ENOENT => e
85
+ # ignore missing layout dir
86
+ end
87
+
88
+ # Write each post to <dest>/<year>/<month>/<day>/<slug>
89
+ #
90
+ # Returns nothing
91
+ def write_posts
92
+ self.posts.each do |post|
93
+ post.write(self.dest)
94
+ end
95
+ end
96
+
97
+ # Copy all regular files from <source> to <dest>/ ignoring
98
+ # any files/directories that are hidden or backup files (start
99
+ # with "." or end with "~") or contain site content (start with "_")
100
+ # unless they are "_posts" directories or web server files such as
101
+ # '.htaccess'
102
+ # The +dir+ String is a relative path used to call this method
103
+ # recursively as it descends through directories
104
+ #
105
+ # Returns nothing
106
+ def transform_pages(dir = '')
107
+ base = File.join(self.source, dir)
108
+ entries = Dir.entries(base)
109
+ entries = entries.reject { |e| e[-1..-1] == '~' }
110
+ entries = entries.reject do |e|
111
+ (e != '_posts') and ['.', '_'].include?(e[0..0]) unless ['.htaccess'].include?(e)
112
+ end
113
+ directories = entries.select { |e| File.directory?(File.join(base, e)) }
114
+ files = entries.reject { |e| File.directory?(File.join(base, e)) }
115
+
116
+ # we need to make sure to process _posts *first* otherwise they
117
+ # might not be available yet to other templates as {{ site.posts }}
118
+ if entries.include?('_posts')
119
+ entries.delete('_posts')
120
+ read_posts(dir)
121
+ end
122
+ [directories, files].each do |entries|
123
+ entries.each do |f|
124
+ if File.directory?(File.join(base, f))
125
+ next if self.dest.sub(/\/$/, '') == File.join(base, f)
126
+ transform_pages(File.join(dir, f))
127
+ else
128
+ first3 = File.open(File.join(self.source, dir, f)) { |fd| fd.read(3) }
129
+
130
+ if first3 == "---"
131
+ # file appears to have a YAML header so process it as a page
132
+ page = Page.new(self.source, dir, f)
133
+ page.render(self.layouts, site_payload)
134
+ page.write(self.dest)
135
+ else
136
+ # otherwise copy the file without transforming it
137
+ FileUtils.mkdir_p(File.join(self.dest, dir))
138
+ FileUtils.cp(File.join(self.source, dir, f), File.join(self.dest, dir, f))
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+ # Constructs a hash map of Posts indexed by the specified Post attribute
146
+ #
147
+ # Returns {post_attr => [<Post>]}
148
+ def post_attr_hash(post_attr)
149
+ # Build a hash map based on the specified post attribute ( post attr => array of posts )
150
+ # then sort each array in reverse order
151
+ hash = Hash.new { |hash, key| hash[key] = Array.new }
152
+ self.posts.each { |p| p.send(post_attr.to_sym).each { |t| hash[t] << p } }
153
+ hash.values.map { |sortme| sortme.sort! { |a, b| b <=> a} }
154
+ return hash
155
+ end
156
+
157
+ # The Hash payload containing site-wide data
158
+ #
159
+ # Returns {"site" => {"time" => <Time>,
160
+ # "posts" => [<Post>],
161
+ # "categories" => [<Post>],
162
+ # "topics" => [<Post>] }}
163
+ def site_payload
164
+ {"site" => {
165
+ "time" => Time.now,
166
+ "posts" => self.posts.sort { |a,b| b <=> a },
167
+ "categories" => post_attr_hash('categories'),
168
+ "topics" => post_attr_hash('topics')
169
+ }}
170
+ end
171
+ end
172
+
173
+ 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)
@@ -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(Jekyll.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)