fagiani-jekyll 0.10.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 (97) hide show
  1. data/History.txt +284 -0
  2. data/LICENSE +21 -0
  3. data/README.textile +41 -0
  4. data/Rakefile +159 -0
  5. data/bin/jekyll +192 -0
  6. data/cucumber.yml +1 -0
  7. data/features/create_sites.feature +94 -0
  8. data/features/embed_filters.feature +60 -0
  9. data/features/markdown.feature +30 -0
  10. data/features/pagination.feature +27 -0
  11. data/features/permalinks.feature +65 -0
  12. data/features/post_data.feature +153 -0
  13. data/features/site_configuration.feature +126 -0
  14. data/features/site_data.feature +82 -0
  15. data/features/step_definitions/jekyll_steps.rb +145 -0
  16. data/features/support/env.rb +16 -0
  17. data/jekyll.gemspec +140 -0
  18. data/lib/jekyll.rb +125 -0
  19. data/lib/jekyll/albino.rb +120 -0
  20. data/lib/jekyll/converter.rb +50 -0
  21. data/lib/jekyll/converters/identity.rb +22 -0
  22. data/lib/jekyll/converters/markdown.rb +113 -0
  23. data/lib/jekyll/converters/textile.rb +33 -0
  24. data/lib/jekyll/convertible.rb +98 -0
  25. data/lib/jekyll/core_ext.rb +52 -0
  26. data/lib/jekyll/errors.rb +6 -0
  27. data/lib/jekyll/filters.rb +53 -0
  28. data/lib/jekyll/generator.rb +7 -0
  29. data/lib/jekyll/generators/pagination.rb +87 -0
  30. data/lib/jekyll/layout.rb +36 -0
  31. data/lib/jekyll/migrators/csv.rb +26 -0
  32. data/lib/jekyll/migrators/drupal.rb +86 -0
  33. data/lib/jekyll/migrators/marley.rb +53 -0
  34. data/lib/jekyll/migrators/mephisto.rb +79 -0
  35. data/lib/jekyll/migrators/mt.rb +77 -0
  36. data/lib/jekyll/migrators/textpattern.rb +50 -0
  37. data/lib/jekyll/migrators/typo.rb +49 -0
  38. data/lib/jekyll/migrators/wordpress.com.rb +38 -0
  39. data/lib/jekyll/migrators/wordpress.rb +56 -0
  40. data/lib/jekyll/page.rb +134 -0
  41. data/lib/jekyll/plugin.rb +76 -0
  42. data/lib/jekyll/post.rb +244 -0
  43. data/lib/jekyll/site.rb +273 -0
  44. data/lib/jekyll/static_file.rb +75 -0
  45. data/lib/jekyll/tags/highlight.rb +73 -0
  46. data/lib/jekyll/tags/include.rb +37 -0
  47. data/test/helper.rb +34 -0
  48. data/test/source/.htaccess +8 -0
  49. data/test/source/_includes/sig.markdown +3 -0
  50. data/test/source/_layouts/default.html +27 -0
  51. data/test/source/_layouts/simple.html +1 -0
  52. data/test/source/_posts/2008-02-02-not-published.textile +8 -0
  53. data/test/source/_posts/2008-02-02-published.textile +8 -0
  54. data/test/source/_posts/2008-10-18-foo-bar.textile +8 -0
  55. data/test/source/_posts/2008-11-21-complex.textile +8 -0
  56. data/test/source/_posts/2008-12-03-permalinked-post.textile +9 -0
  57. data/test/source/_posts/2008-12-13-include.markdown +8 -0
  58. data/test/source/_posts/2009-01-27-array-categories.textile +10 -0
  59. data/test/source/_posts/2009-01-27-categories.textile +7 -0
  60. data/test/source/_posts/2009-01-27-category.textile +7 -0
  61. data/test/source/_posts/2009-01-27-empty-categories.textile +7 -0
  62. data/test/source/_posts/2009-01-27-empty-category.textile +7 -0
  63. data/test/source/_posts/2009-03-12-hash-#1.markdown +6 -0
  64. data/test/source/_posts/2009-05-18-empty-tag.textile +6 -0
  65. data/test/source/_posts/2009-05-18-empty-tags.textile +6 -0
  66. data/test/source/_posts/2009-05-18-tag.textile +6 -0
  67. data/test/source/_posts/2009-05-18-tags.textile +9 -0
  68. data/test/source/_posts/2009-06-22-empty-yaml.textile +3 -0
  69. data/test/source/_posts/2009-06-22-no-yaml.textile +1 -0
  70. data/test/source/_posts/2010-01-08-triple-dash.markdown +5 -0
  71. data/test/source/_posts/2010-01-09-date-override.textile +7 -0
  72. data/test/source/_posts/2010-01-09-time-override.textile +7 -0
  73. data/test/source/_posts/2010-01-09-timezone-override.textile +7 -0
  74. data/test/source/_posts/2010-01-16-override-data.textile +4 -0
  75. data/test/source/about.html +6 -0
  76. data/test/source/category/_posts/2008-9-23-categories.textile +6 -0
  77. data/test/source/contacts.html +5 -0
  78. data/test/source/css/screen.css +76 -0
  79. data/test/source/deal.with.dots.html +7 -0
  80. data/test/source/foo/_posts/bar/2008-12-12-topical-post.textile +8 -0
  81. data/test/source/index.html +22 -0
  82. data/test/source/sitemap.xml +32 -0
  83. data/test/source/win/_posts/2009-05-24-yaml-linebreak.markdown +7 -0
  84. data/test/source/z_category/_posts/2008-9-23-categories.textile +6 -0
  85. data/test/suite.rb +9 -0
  86. data/test/test_configuration.rb +29 -0
  87. data/test/test_core_ext.rb +66 -0
  88. data/test/test_filters.rb +53 -0
  89. data/test/test_generated_site.rb +72 -0
  90. data/test/test_kramdown.rb +23 -0
  91. data/test/test_page.rb +117 -0
  92. data/test/test_pager.rb +113 -0
  93. data/test/test_post.rb +396 -0
  94. data/test/test_rdiscount.rb +18 -0
  95. data/test/test_site.rb +186 -0
  96. data/test/test_tags.rb +127 -0
  97. metadata +332 -0
@@ -0,0 +1,273 @@
1
+ module Jekyll
2
+
3
+ class Site
4
+ attr_accessor :config, :layouts, :posts, :pages, :static_files,
5
+ :categories, :exclude, :source, :dest, :lsi, :pygments,
6
+ :permalink_style, :tags, :time, :future, :safe, :plugins, :limit_posts
7
+
8
+ attr_accessor :converters, :generators
9
+
10
+ # Initialize the site
11
+ # +config+ is a Hash containing site configurations details
12
+ #
13
+ # Returns <Site>
14
+ def initialize(config)
15
+ self.config = config.clone
16
+
17
+ self.safe = config['safe']
18
+ self.source = File.expand_path(config['source'])
19
+ self.dest = File.expand_path(config['destination'])
20
+ self.plugins = File.expand_path(config['plugins'])
21
+ self.lsi = config['lsi']
22
+ self.pygments = config['pygments']
23
+ self.permalink_style = config['permalink'].to_sym
24
+ self.exclude = config['exclude'] || []
25
+ self.future = config['future']
26
+ self.limit_posts = config['limit_posts'] || nil
27
+
28
+ self.reset
29
+ self.setup
30
+ end
31
+
32
+ def reset
33
+ self.time = if self.config['time']
34
+ Time.parse(self.config['time'].to_s)
35
+ else
36
+ Time.now
37
+ end
38
+ self.layouts = {}
39
+ self.posts = []
40
+ self.pages = []
41
+ self.static_files = []
42
+ self.categories = Hash.new { |hash, key| hash[key] = [] }
43
+ self.tags = Hash.new { |hash, key| hash[key] = [] }
44
+
45
+ raise ArgumentError, "Limit posts must be nil or >= 1" if !self.limit_posts.nil? && self.limit_posts < 1
46
+ end
47
+
48
+ def setup
49
+ require 'classifier' if self.lsi
50
+
51
+ # If safe mode is off, load in any ruby files under the plugins
52
+ # directory.
53
+ unless self.safe
54
+ Dir[File.join(self.plugins, "**/*.rb")].each do |f|
55
+ require f
56
+ end
57
+ end
58
+
59
+ self.converters = Jekyll::Converter.subclasses.select do |c|
60
+ !self.safe || c.safe
61
+ end.map do |c|
62
+ c.new(self.config)
63
+ end
64
+
65
+ self.generators = Jekyll::Generator.subclasses.select do |c|
66
+ !self.safe || c.safe
67
+ end.map do |c|
68
+ c.new(self.config)
69
+ end
70
+ end
71
+
72
+ # Do the actual work of processing the site and generating the
73
+ # real deal. 5 phases; reset, read, generate, render, write. This allows
74
+ # rendering to have full site payload available.
75
+ #
76
+ # Returns nothing
77
+ def process
78
+ self.reset
79
+ self.read
80
+ self.generate
81
+ self.render
82
+ self.cleanup
83
+ self.write
84
+ end
85
+
86
+ def read
87
+ self.read_layouts # existing implementation did this at top level only so preserved that
88
+ self.read_directories
89
+ end
90
+
91
+ # Read all the files in <source>/<dir>/_layouts and create a new Layout
92
+ # object with each one.
93
+ #
94
+ # Returns nothing
95
+ def read_layouts(dir = '')
96
+ base = File.join(self.source, dir, "_layouts")
97
+ return unless File.exists?(base)
98
+ entries = []
99
+ Dir.chdir(base) { entries = filter_entries(Dir['*.*']) }
100
+
101
+ entries.each do |f|
102
+ name = f.split(".")[0..-2].join(".")
103
+ self.layouts[name] = Layout.new(self, base, f)
104
+ end
105
+ end
106
+
107
+ # Read all the files in <source>/<dir>/_posts and create a new Post
108
+ # object with each one.
109
+ #
110
+ # Returns nothing
111
+ def read_posts(dir)
112
+ base = File.join(self.source, dir, '_posts')
113
+ return unless File.exists?(base)
114
+ entries = Dir.chdir(base) { filter_entries(Dir['**/*']) }
115
+
116
+ # first pass processes, but does not yet render post content
117
+ entries.each do |f|
118
+ if Post.valid?(f)
119
+ post = Post.new(self, self.source, dir, f)
120
+
121
+ if post.published && (self.future || post.date <= self.time)
122
+ self.posts << post
123
+ post.categories.each { |c| self.categories[c] << post }
124
+ post.tags.each { |c| self.tags[c] << post }
125
+ end
126
+ end
127
+ end
128
+
129
+ self.posts.sort!
130
+
131
+ # limit the posts if :limit_posts option is set
132
+ self.posts = self.posts[-limit_posts, limit_posts] if limit_posts
133
+ end
134
+
135
+ def generate
136
+ self.generators.each do |generator|
137
+ generator.generate(self)
138
+ end
139
+ end
140
+
141
+ def render
142
+ self.posts.each do |post|
143
+ post.render(self.layouts, site_payload)
144
+ end
145
+
146
+ self.pages.each do |page|
147
+ page.render(self.layouts, site_payload)
148
+ end
149
+
150
+ self.categories.values.map { |ps| ps.sort! { |a, b| b <=> a} }
151
+ self.tags.values.map { |ps| ps.sort! { |a, b| b <=> a} }
152
+ rescue Errno::ENOENT => e
153
+ # ignore missing layout dir
154
+ end
155
+
156
+ # Remove orphaned files and empty directories in destination
157
+ #
158
+ # Returns nothing
159
+ def cleanup
160
+ # all files and directories in destination, including hidden ones
161
+ dest_files = []
162
+ Dir.glob(File.join(self.dest, "**", "*"), File::FNM_DOTMATCH) do |file|
163
+ dest_files << file unless file =~ /\/\.{1,2}$/
164
+ end
165
+
166
+ # files to be written
167
+ files = []
168
+ self.posts.each do |post|
169
+ files << post.destination(self.dest)
170
+ end
171
+ self.pages.each do |page|
172
+ files << page.destination(self.dest)
173
+ end
174
+ self.static_files.each do |sf|
175
+ files << sf.destination(self.dest)
176
+ end
177
+
178
+ # adding files' parent directories
179
+ files.each { |file| files << File.dirname(file) unless files.include? File.dirname(file) }
180
+
181
+ obsolete_files = dest_files - files
182
+
183
+ FileUtils.rm_rf(obsolete_files)
184
+ end
185
+
186
+ # Write static files, pages and posts
187
+ #
188
+ # Returns nothing
189
+ def write
190
+ self.posts.each do |post|
191
+ post.write(self.dest)
192
+ end
193
+ self.pages.each do |page|
194
+ page.write(self.dest)
195
+ end
196
+ self.static_files.each do |sf|
197
+ sf.write(self.dest)
198
+ end
199
+ end
200
+
201
+ # Reads the directories and finds posts, pages and static files that will
202
+ # become part of the valid site according to the rules in +filter_entries+.
203
+ # The +dir+ String is a relative path used to call this method
204
+ # recursively as it descends through directories
205
+ #
206
+ # Returns nothing
207
+ def read_directories(dir = '')
208
+ base = File.join(self.source, dir)
209
+ entries = filter_entries(Dir.entries(base))
210
+
211
+ self.read_posts(dir)
212
+
213
+ entries.each do |f|
214
+ f_abs = File.join(base, f)
215
+ f_rel = File.join(dir, f)
216
+ if File.directory?(f_abs)
217
+ next if self.dest.sub(/\/$/, '') == f_abs
218
+ read_directories(f_rel)
219
+ elsif !File.symlink?(f_abs)
220
+ first3 = File.open(f_abs) { |fd| fd.read(3) }
221
+ if first3 == "---"
222
+ # file appears to have a YAML header so process it as a page
223
+ pages << Page.new(self, self.source, dir, f)
224
+ else
225
+ # otherwise treat it as a static file
226
+ static_files << StaticFile.new(self, self.source, dir, f)
227
+ end
228
+ end
229
+ end
230
+ end
231
+
232
+ # Constructs a hash map of Posts indexed by the specified Post attribute
233
+ #
234
+ # Returns {post_attr => [<Post>]}
235
+ def post_attr_hash(post_attr)
236
+ # Build a hash map based on the specified post attribute ( post attr => array of posts )
237
+ # then sort each array in reverse order
238
+ hash = Hash.new { |hash, key| hash[key] = Array.new }
239
+ self.posts.each { |p| p.send(post_attr.to_sym).each { |t| hash[t] << p } }
240
+ hash.values.map { |sortme| sortme.sort! { |a, b| b <=> a} }
241
+ return hash
242
+ end
243
+
244
+ # The Hash payload containing site-wide data
245
+ #
246
+ # Returns {"site" => {"time" => <Time>,
247
+ # "posts" => [<Post>],
248
+ # "pages" => [<Page>],
249
+ # "categories" => [<Post>]}
250
+ def site_payload
251
+ {"site" => self.config.merge({
252
+ "time" => self.time,
253
+ "posts" => self.posts.sort { |a,b| b <=> a },
254
+ "pages" => self.pages,
255
+ "html_pages" => self.pages.reject { |page| !page.html? },
256
+ "categories" => post_attr_hash('categories'),
257
+ "tags" => post_attr_hash('tags')})}
258
+ end
259
+
260
+ # Filter out any files/directories that are hidden or backup files (start
261
+ # with "." or "#" or end with "~"), or contain site content (start with "_"),
262
+ # or are excluded in the site configuration, unless they are web server
263
+ # files such as '.htaccess'
264
+ def filter_entries(entries)
265
+ entries = entries.reject do |e|
266
+ unless ['.htaccess'].include?(e)
267
+ ['.', '_', '#'].include?(e[0..0]) || e[-1..-1] == '~' || self.exclude.include?(e)
268
+ end
269
+ end
270
+ end
271
+
272
+ end
273
+ end
@@ -0,0 +1,75 @@
1
+ module Jekyll
2
+
3
+ class StaticFile
4
+ @@mtimes = Hash.new # the cache of last modification times [path] -> mtime
5
+
6
+ # Initialize a new StaticFile.
7
+ # +site+ is the Site
8
+ # +base+ is the String path to the <source>
9
+ # +dir+ is the String path between <source> and the file
10
+ # +name+ is the String filename of the file
11
+ #
12
+ # Returns <StaticFile>
13
+ def initialize(site, base, dir, name)
14
+ @site = site
15
+ @base = base
16
+ @dir = dir
17
+ @name = name
18
+ end
19
+
20
+ # Obtains source file path.
21
+ #
22
+ # Returns source file path.
23
+ def path
24
+ File.join(@base, @dir, @name)
25
+ end
26
+
27
+ # Obtain destination path.
28
+ # +dest+ is the String path to the destination dir
29
+ #
30
+ # Returns destination file path.
31
+ def destination(dest)
32
+ File.join(dest, @dir, @name)
33
+ end
34
+
35
+ # Obtain mtime of the source path.
36
+ #
37
+ # Returns last modifiaction time for this file.
38
+ def mtime
39
+ File.stat(path).mtime.to_i
40
+ end
41
+
42
+ # Is source path modified?
43
+ #
44
+ # Returns true if modified since last write.
45
+ def modified?
46
+ @@mtimes[path] != mtime
47
+ end
48
+
49
+ # Write the static file to the destination directory (if modified).
50
+ # +dest+ is the String path to the destination dir
51
+ #
52
+ # Returns false if the file was not modified since last time (no-op).
53
+ def write(dest)
54
+ dest_path = destination(dest)
55
+
56
+ return false if File.exist? dest_path and !modified?
57
+ @@mtimes[path] = mtime
58
+
59
+ FileUtils.mkdir_p(File.dirname(dest_path))
60
+ FileUtils.cp(path, dest_path)
61
+
62
+ true
63
+ end
64
+
65
+ # Reset the mtimes cache (for testing purposes).
66
+ #
67
+ # Returns nothing.
68
+ def self.reset_cache
69
+ @@mtimes = Hash.new
70
+
71
+ nil
72
+ end
73
+ end
74
+
75
+ end
@@ -0,0 +1,73 @@
1
+ module Jekyll
2
+
3
+ class HighlightBlock < Liquid::Block
4
+ include Liquid::StandardFilters
5
+
6
+ # we need a language, but the linenos argument is optional.
7
+ SYNTAX = /(\w+)\s?([\w\s=]+)*/
8
+
9
+ def initialize(tag_name, markup, tokens)
10
+ super
11
+ if markup =~ SYNTAX
12
+ @lang = $1
13
+ if defined? $2
14
+ tmp_options = {}
15
+ $2.split.each do |opt|
16
+ key, value = opt.split('=')
17
+ if value.nil?
18
+ if key == 'linenos'
19
+ value = 'inline'
20
+ else
21
+ value = true
22
+ end
23
+ end
24
+ tmp_options[key] = value
25
+ end
26
+ tmp_options = tmp_options.to_a.collect { |opt| opt.join('=') }
27
+ # additional options to pass to Albino.
28
+ @options = { 'O' => tmp_options.join(',') }
29
+ else
30
+ @options = {}
31
+ end
32
+ else
33
+ raise SyntaxError.new("Syntax Error in 'highlight' - Valid syntax: highlight <lang> [linenos]")
34
+ end
35
+ end
36
+
37
+ def render(context)
38
+ if context.registers[:site].pygments
39
+ render_pygments(context, super.join)
40
+ else
41
+ render_codehighlighter(context, super.join)
42
+ end
43
+ end
44
+
45
+ def render_pygments(context, code)
46
+ output = add_code_tags(Albino.new(code, @lang).to_s(@options), @lang)
47
+ output = context["pygments_prefix"] + output if context["pygments_prefix"]
48
+ output = output + context["pygments_suffix"] if context["pygments_suffix"]
49
+ output
50
+ end
51
+
52
+ def render_codehighlighter(context, code)
53
+ #The div is required because RDiscount blows ass
54
+ <<-HTML
55
+ <div>
56
+ <pre>
57
+ <code class='#{@lang}'>#{h(code).strip}</code>
58
+ </pre>
59
+ </div>
60
+ HTML
61
+ end
62
+
63
+ def add_code_tags(code, lang)
64
+ # Add nested <code> tags to code blocks
65
+ code = code.sub(/<pre>/,'<pre><code class="' + lang + '">')
66
+ code = code.sub(/<\/pre>/,"</code></pre>")
67
+ end
68
+
69
+ end
70
+
71
+ end
72
+
73
+ Liquid::Template.register_tag('highlight', Jekyll::HighlightBlock)
@@ -0,0 +1,37 @@
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
+ includes_dir = File.join(context.registers[:site].source, '_includes')
11
+
12
+ if File.symlink?(includes_dir)
13
+ return "Includes directory '#{includes_dir}' cannot be a symlink"
14
+ end
15
+
16
+ if @file !~ /^[a-zA-Z0-9_\/\.-]+$/ || @file =~ /\.\// || @file =~ /\/\./
17
+ return "Include file '#{@file}' contains invalid characters or sequences"
18
+ end
19
+
20
+ Dir.chdir(includes_dir) do
21
+ choices = Dir['**/*'].reject { |x| File.symlink?(x) }
22
+ if choices.include?(@file)
23
+ source = File.read(@file)
24
+ partial = Liquid::Template.parse(source)
25
+ context.stack do
26
+ partial.render(context)
27
+ end
28
+ else
29
+ "Included file '#{@file}' not found in _includes directory"
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ end
36
+
37
+ Liquid::Template.register_tag('include', Jekyll::IncludeTag)