fagiani-jekyll 0.10.1

Sign up to get free protection for your applications and to get access to all the features.
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)