spinto-jekyll 0.11.2.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 (108) hide show
  1. data/Gemfile +2 -0
  2. data/History.txt +321 -0
  3. data/LICENSE +21 -0
  4. data/README.textile +41 -0
  5. data/Rakefile +166 -0
  6. data/bin/jekyll +289 -0
  7. data/cucumber.yml +1 -0
  8. data/features/create_sites.feature +112 -0
  9. data/features/embed_filters.feature +60 -0
  10. data/features/markdown.feature +30 -0
  11. data/features/pagination.feature +27 -0
  12. data/features/permalinks.feature +65 -0
  13. data/features/post_data.feature +153 -0
  14. data/features/site_configuration.feature +145 -0
  15. data/features/site_data.feature +82 -0
  16. data/features/step_definitions/jekyll_steps.rb +145 -0
  17. data/features/support/env.rb +19 -0
  18. data/lib/jekyll.rb +136 -0
  19. data/lib/jekyll/converter.rb +50 -0
  20. data/lib/jekyll/converters/identity.rb +22 -0
  21. data/lib/jekyll/converters/markdown.rb +125 -0
  22. data/lib/jekyll/converters/textile.rb +50 -0
  23. data/lib/jekyll/convertible.rb +112 -0
  24. data/lib/jekyll/core_ext.rb +52 -0
  25. data/lib/jekyll/errors.rb +6 -0
  26. data/lib/jekyll/filters.rb +118 -0
  27. data/lib/jekyll/generator.rb +7 -0
  28. data/lib/jekyll/generators/pagination.rb +113 -0
  29. data/lib/jekyll/layout.rb +44 -0
  30. data/lib/jekyll/migrators/csv.rb +26 -0
  31. data/lib/jekyll/migrators/drupal.rb +103 -0
  32. data/lib/jekyll/migrators/enki.rb +49 -0
  33. data/lib/jekyll/migrators/joomla.rb +53 -0
  34. data/lib/jekyll/migrators/marley.rb +52 -0
  35. data/lib/jekyll/migrators/mephisto.rb +84 -0
  36. data/lib/jekyll/migrators/mt.rb +86 -0
  37. data/lib/jekyll/migrators/posterous.rb +67 -0
  38. data/lib/jekyll/migrators/rss.rb +47 -0
  39. data/lib/jekyll/migrators/textpattern.rb +58 -0
  40. data/lib/jekyll/migrators/tumblr.rb +195 -0
  41. data/lib/jekyll/migrators/typo.rb +51 -0
  42. data/lib/jekyll/migrators/wordpress.rb +294 -0
  43. data/lib/jekyll/migrators/wordpressdotcom.rb +70 -0
  44. data/lib/jekyll/page.rb +155 -0
  45. data/lib/jekyll/plugin.rb +77 -0
  46. data/lib/jekyll/post.rb +257 -0
  47. data/lib/jekyll/site.rb +337 -0
  48. data/lib/jekyll/static_file.rb +72 -0
  49. data/lib/jekyll/tags/highlight.rb +76 -0
  50. data/lib/jekyll/tags/include.rb +37 -0
  51. data/lib/jekyll/tags/post_url.rb +38 -0
  52. data/lib/spinto-jekyll.rb +3 -0
  53. data/spinto-jekyll.gemspec +155 -0
  54. data/test/helper.rb +34 -0
  55. data/test/source/.htaccess +8 -0
  56. data/test/source/_includes/sig.markdown +3 -0
  57. data/test/source/_layouts/default.html +27 -0
  58. data/test/source/_layouts/simple.html +1 -0
  59. data/test/source/_posts/2008-02-02-not-published.textile +8 -0
  60. data/test/source/_posts/2008-02-02-published.textile +8 -0
  61. data/test/source/_posts/2008-10-18-foo-bar.textile +8 -0
  62. data/test/source/_posts/2008-11-21-complex.textile +8 -0
  63. data/test/source/_posts/2008-12-03-permalinked-post.textile +9 -0
  64. data/test/source/_posts/2008-12-13-include.markdown +8 -0
  65. data/test/source/_posts/2009-01-27-array-categories.textile +10 -0
  66. data/test/source/_posts/2009-01-27-categories.textile +7 -0
  67. data/test/source/_posts/2009-01-27-category.textile +7 -0
  68. data/test/source/_posts/2009-01-27-empty-categories.textile +7 -0
  69. data/test/source/_posts/2009-01-27-empty-category.textile +7 -0
  70. data/test/source/_posts/2009-03-12-hash-#1.markdown +6 -0
  71. data/test/source/_posts/2009-05-18-empty-tag.textile +6 -0
  72. data/test/source/_posts/2009-05-18-empty-tags.textile +6 -0
  73. data/test/source/_posts/2009-05-18-tag.textile +6 -0
  74. data/test/source/_posts/2009-05-18-tags.textile +9 -0
  75. data/test/source/_posts/2009-06-22-empty-yaml.textile +3 -0
  76. data/test/source/_posts/2009-06-22-no-yaml.textile +1 -0
  77. data/test/source/_posts/2010-01-08-triple-dash.markdown +5 -0
  78. data/test/source/_posts/2010-01-09-date-override.textile +7 -0
  79. data/test/source/_posts/2010-01-09-time-override.textile +7 -0
  80. data/test/source/_posts/2010-01-09-timezone-override.textile +7 -0
  81. data/test/source/_posts/2010-01-16-override-data.textile +4 -0
  82. data/test/source/_posts/2011-04-12-md-extension.md +7 -0
  83. data/test/source/_posts/2011-04-12-text-extension.text +0 -0
  84. data/test/source/about.html +6 -0
  85. data/test/source/category/_posts/2008-9-23-categories.textile +6 -0
  86. data/test/source/contacts.html +5 -0
  87. data/test/source/css/screen.css +76 -0
  88. data/test/source/deal.with.dots.html +7 -0
  89. data/test/source/foo/_posts/bar/2008-12-12-topical-post.textile +8 -0
  90. data/test/source/index.html +22 -0
  91. data/test/source/sitemap.xml +32 -0
  92. data/test/source/win/_posts/2009-05-24-yaml-linebreak.markdown +7 -0
  93. data/test/source/z_category/_posts/2008-9-23-categories.textile +6 -0
  94. data/test/suite.rb +11 -0
  95. data/test/test_configuration.rb +29 -0
  96. data/test/test_core_ext.rb +66 -0
  97. data/test/test_filters.rb +62 -0
  98. data/test/test_generated_site.rb +72 -0
  99. data/test/test_kramdown.rb +23 -0
  100. data/test/test_page.rb +117 -0
  101. data/test/test_pager.rb +113 -0
  102. data/test/test_post.rb +456 -0
  103. data/test/test_rdiscount.rb +18 -0
  104. data/test/test_redcarpet.rb +21 -0
  105. data/test/test_redcloth.rb +86 -0
  106. data/test/test_site.rb +220 -0
  107. data/test/test_tags.rb +201 -0
  108. metadata +336 -0
@@ -0,0 +1,337 @@
1
+ require 'set'
2
+
3
+ module Jekyll
4
+
5
+ class Site
6
+ attr_accessor :config, :layouts, :posts, :pages, :static_files,
7
+ :categories, :exclude, :include, :source, :dest, :lsi, :pygments,
8
+ :permalink_style, :tags, :time, :future, :safe, :plugins, :limit_posts
9
+
10
+ attr_accessor :converters, :generators
11
+
12
+ # Public: Initialize a new Site.
13
+ #
14
+ # config - A Hash containing site configuration details.
15
+ def initialize(config)
16
+ self.config = config.clone
17
+
18
+ self.safe = config['safe']
19
+ self.source = File.expand_path(config['source'])
20
+ self.dest = File.expand_path(config['destination'])
21
+ self.plugins = Array(config['plugins']).map { |d| File.expand_path(d) }
22
+ self.lsi = config['lsi']
23
+ self.pygments = config['pygments']
24
+ self.permalink_style = config['permalink'].to_sym
25
+ self.exclude = config['exclude'] || []
26
+ self.include = config['include'] || []
27
+ self.future = config['future']
28
+ self.limit_posts = config['limit_posts'] || nil
29
+
30
+ self.reset
31
+ self.setup
32
+ end
33
+
34
+ # Public: Read, process, and write this Site to output.
35
+ #
36
+ # Returns nothing.
37
+ def process
38
+ self.reset
39
+ self.read
40
+ self.generate
41
+ self.render
42
+ self.cleanup
43
+ self.write
44
+ end
45
+
46
+ # Reset Site details.
47
+ #
48
+ # Returns nothing
49
+ def reset
50
+ self.time = if self.config['time']
51
+ Time.parse(self.config['time'].to_s)
52
+ else
53
+ Time.now
54
+ end
55
+ self.layouts = {}
56
+ self.posts = []
57
+ self.pages = []
58
+ self.static_files = []
59
+ self.categories = Hash.new { |hash, key| hash[key] = [] }
60
+ self.tags = Hash.new { |hash, key| hash[key] = [] }
61
+
62
+ if !self.limit_posts.nil? && self.limit_posts < 1
63
+ raise ArgumentError, "Limit posts must be nil or >= 1"
64
+ end
65
+ end
66
+
67
+ # Load necessary libraries, plugins, converters, and generators.
68
+ #
69
+ # Returns nothing.
70
+ def setup
71
+ require 'classifier' if self.lsi
72
+
73
+ # If safe mode is off, load in any Ruby files under the plugins
74
+ # directory.
75
+ unless self.safe
76
+ self.plugins.each do |plugins|
77
+ Dir[File.join(plugins, "**/*.rb")].each do |f|
78
+ require f
79
+ end
80
+ end
81
+ end
82
+
83
+ self.converters = Jekyll::Converter.subclasses.select do |c|
84
+ !self.safe || c.safe
85
+ end.map do |c|
86
+ c.new(self.config)
87
+ end
88
+
89
+ self.generators = Jekyll::Generator.subclasses.select do |c|
90
+ !self.safe || c.safe
91
+ end.map do |c|
92
+ c.new(self.config)
93
+ end
94
+ end
95
+
96
+ # Read Site data from disk and load it into internal data structures.
97
+ #
98
+ # Returns nothing.
99
+ def read
100
+ self.read_layouts
101
+ self.read_directories
102
+ end
103
+
104
+ # Read all the files in <source>/<dir>/_layouts and create a new Layout
105
+ # object with each one.
106
+ #
107
+ # Returns nothing.
108
+ def read_layouts(dir = '')
109
+ base = File.join(self.source, dir, "_layouts")
110
+ return unless File.exists?(base)
111
+ entries = []
112
+ Dir.chdir(base) { entries = filter_entries(Dir['*.*']) }
113
+
114
+ entries.each do |f|
115
+ name = f.split(".")[0..-2].join(".")
116
+ self.layouts[name] = Layout.new(self, base, f)
117
+ end
118
+ end
119
+
120
+ # Recursively traverse directories to find posts, pages and static files
121
+ # that will become part of the site according to the rules in
122
+ # filter_entries.
123
+ #
124
+ # dir - The String relative path of the directory to read.
125
+ #
126
+ # Returns nothing.
127
+ def read_directories(dir = '')
128
+ base = File.join(self.source, dir)
129
+ entries = Dir.chdir(base) { filter_entries(Dir.entries('.')) }
130
+
131
+ self.read_posts(dir)
132
+
133
+ entries.each do |f|
134
+ f_abs = File.join(base, f)
135
+ f_rel = File.join(dir, f)
136
+ if File.directory?(f_abs)
137
+ next if self.dest.sub(/\/$/, '') == f_abs
138
+ read_directories(f_rel)
139
+ elsif !File.symlink?(f_abs)
140
+ first3 = File.open(f_abs) { |fd| fd.read(3) }
141
+ if first3 == "---"
142
+ # file appears to have a YAML header so process it as a page
143
+ pages << Page.new(self, self.source, dir, f)
144
+ else
145
+ # otherwise treat it as a static file
146
+ static_files << StaticFile.new(self, self.source, dir, f)
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ # Read all the files in <source>/<dir>/_posts and create a new Post
153
+ # object with each one.
154
+ #
155
+ # dir - The String relative path of the directory to read.
156
+ #
157
+ # Returns nothing.
158
+ def read_posts(dir)
159
+ base = File.join(self.source, dir, '_posts')
160
+ return unless File.exists?(base)
161
+ entries = Dir.chdir(base) { filter_entries(Dir['**/*']) }
162
+
163
+ # first pass processes, but does not yet render post content
164
+ entries.each do |f|
165
+ if Post.valid?(f)
166
+ post = Post.new(self, self.source, dir, f)
167
+
168
+ if post.published && (self.future || post.date <= self.time)
169
+ self.posts << post
170
+ post.categories.each { |c| self.categories[c] << post }
171
+ post.tags.each { |c| self.tags[c] << post }
172
+ end
173
+ end
174
+ end
175
+
176
+ self.posts.sort!
177
+
178
+ # limit the posts if :limit_posts option is set
179
+ if limit_posts
180
+ limit = self.posts.length < limit_posts ? self.posts.length : limit_posts
181
+ self.posts = self.posts[-limit, limit]
182
+ end
183
+ end
184
+
185
+ # Run each of the Generators.
186
+ #
187
+ # Returns nothing.
188
+ def generate
189
+ self.generators.each do |generator|
190
+ generator.generate(self)
191
+ end
192
+ end
193
+
194
+ # Render the site to the destination.
195
+ #
196
+ # Returns nothing.
197
+ def render
198
+ self.posts.each do |post|
199
+ post.render(self.layouts, site_payload)
200
+ end
201
+
202
+ self.pages.each do |page|
203
+ page.render(self.layouts, site_payload)
204
+ end
205
+
206
+ self.categories.values.map { |ps| ps.sort! { |a, b| b <=> a } }
207
+ self.tags.values.map { |ps| ps.sort! { |a, b| b <=> a } }
208
+ rescue Errno::ENOENT => e
209
+ # ignore missing layout dir
210
+ end
211
+
212
+ # Remove orphaned files and empty directories in destination.
213
+ #
214
+ # Returns nothing.
215
+ def cleanup
216
+ # all files and directories in destination, including hidden ones
217
+ dest_files = Set.new
218
+ Dir.glob(File.join(self.dest, "**", "*"), File::FNM_DOTMATCH) do |file|
219
+ dest_files << file unless file =~ /\/\.{1,2}$/
220
+ end
221
+
222
+ # files to be written
223
+ files = Set.new
224
+ self.posts.each do |post|
225
+ files << post.destination(self.dest)
226
+ end
227
+ self.pages.each do |page|
228
+ files << page.destination(self.dest)
229
+ end
230
+ self.static_files.each do |sf|
231
+ files << sf.destination(self.dest)
232
+ end
233
+
234
+ # adding files' parent directories
235
+ dirs = Set.new
236
+ files.each { |file| dirs << File.dirname(file) }
237
+ files.merge(dirs)
238
+
239
+ obsolete_files = dest_files - files
240
+
241
+ FileUtils.rm_rf(obsolete_files.to_a)
242
+ end
243
+
244
+ # Write static files, pages, and posts.
245
+ #
246
+ # Returns nothing.
247
+ def write
248
+ self.posts.each do |post|
249
+ post.write(self.dest)
250
+ end
251
+ self.pages.each do |page|
252
+ page.write(self.dest)
253
+ end
254
+ self.static_files.each do |sf|
255
+ sf.write(self.dest)
256
+ end
257
+ end
258
+
259
+ # Constructs a Hash of Posts indexed by the specified Post attribute.
260
+ #
261
+ # post_attr - The String name of the Post attribute.
262
+ #
263
+ # Examples
264
+ #
265
+ # post_attr_hash('categories')
266
+ # # => { 'tech' => [<Post A>, <Post B>],
267
+ # # 'ruby' => [<Post B>] }
268
+ #
269
+ # Returns the Hash: { attr => posts } where
270
+ # attr - One of the values for the requested attribute.
271
+ # posts - The Array of Posts with the given attr value.
272
+ def post_attr_hash(post_attr)
273
+ # Build a hash map based on the specified post attribute ( post attr =>
274
+ # array of posts ) then sort each array in reverse order.
275
+ hash = Hash.new { |hsh, key| hsh[key] = Array.new }
276
+ self.posts.each { |p| p.send(post_attr.to_sym).each { |t| hash[t] << p } }
277
+ hash.values.map { |sortme| sortme.sort! { |a, b| b <=> a } }
278
+ hash
279
+ end
280
+
281
+ # The Hash payload containing site-wide data.
282
+ #
283
+ # Returns the Hash: { "site" => data } where data is a Hash with keys:
284
+ # "time" - The Time as specified in the configuration or the
285
+ # current time if none was specified.
286
+ # "posts" - The Array of Posts, sorted chronologically by post date
287
+ # and then title.
288
+ # "pages" - The Array of all Pages.
289
+ # "html_pages" - The Array of HTML Pages.
290
+ # "categories" - The Hash of category values and Posts.
291
+ # See Site#post_attr_hash for type info.
292
+ # "tags" - The Hash of tag values and Posts.
293
+ # See Site#post_attr_hash for type info.
294
+ def site_payload
295
+ {"site" => self.config.merge({
296
+ "time" => self.time,
297
+ "posts" => self.posts.sort { |a, b| b <=> a },
298
+ "pages" => self.pages,
299
+ "html_pages" => self.pages.reject { |page| !page.html? },
300
+ "categories" => post_attr_hash('categories'),
301
+ "tags" => post_attr_hash('tags')})}
302
+ end
303
+
304
+ # Filter out any files/directories that are hidden or backup files (start
305
+ # with "." or "#" or end with "~"), or contain site content (start with "_"),
306
+ # or are excluded in the site configuration, unless they are web server
307
+ # files such as '.htaccess'.
308
+ #
309
+ # entries - The Array of file/directory entries to filter.
310
+ #
311
+ # Returns the Array of filtered entries.
312
+ def filter_entries(entries)
313
+ entries = entries.reject do |e|
314
+ unless self.include.include?(e)
315
+ ['.', '_', '#'].include?(e[0..0]) ||
316
+ e[-1..-1] == '~' ||
317
+ self.exclude.include?(e) ||
318
+ File.symlink?(e)
319
+ end
320
+ end
321
+ end
322
+
323
+ # Get the implementation class for the given Converter.
324
+ #
325
+ # klass - The Class of the Converter to fetch.
326
+ #
327
+ # Returns the Converter instance implementing the given Converter.
328
+ def getConverterImpl(klass)
329
+ matches = self.converters.select { |c| c.class == klass }
330
+ if impl = matches.first
331
+ impl
332
+ else
333
+ raise "Converter implementation not found for #{klass}"
334
+ end
335
+ end
336
+ end
337
+ end
@@ -0,0 +1,72 @@
1
+ module Jekyll
2
+
3
+ class StaticFile
4
+ # The cache of last modification times [path] -> mtime.
5
+ @@mtimes = Hash.new
6
+
7
+ # Initialize a new StaticFile.
8
+ #
9
+ # site - The Site.
10
+ # base - The String path to the <source>.
11
+ # dir - The String path between <source> and the file.
12
+ # name - The String filename of the file.
13
+ def initialize(site, base, dir, name)
14
+ @site = site
15
+ @base = base
16
+ @dir = dir
17
+ @name = name
18
+ end
19
+
20
+ # Returns source file path.
21
+ def path
22
+ File.join(@base, @dir, @name)
23
+ end
24
+
25
+ # Obtain destination path.
26
+ #
27
+ # dest - The String path to the destination dir.
28
+ #
29
+ # Returns destination file path.
30
+ def destination(dest)
31
+ File.join(dest, @dir, @name)
32
+ end
33
+
34
+ # Returns last modification time for this file.
35
+ def mtime
36
+ File.stat(path).mtime.to_i
37
+ end
38
+
39
+ # Is source path modified?
40
+ #
41
+ # Returns true if modified since last write.
42
+ def modified?
43
+ @@mtimes[path] != mtime
44
+ end
45
+
46
+ # Write the static file to the destination directory (if modified).
47
+ #
48
+ # dest - The String path to the destination dir.
49
+ #
50
+ # Returns false if the file was not modified since last time (no-op).
51
+ def write(dest)
52
+ dest_path = destination(dest)
53
+
54
+ return false if File.exist?(dest_path) and !modified?
55
+ @@mtimes[path] = mtime
56
+
57
+ FileUtils.mkdir_p(File.dirname(dest_path))
58
+ FileUtils.cp(path, dest_path)
59
+
60
+ true
61
+ end
62
+
63
+ # Reset the mtimes cache (for testing purposes).
64
+ #
65
+ # Returns nothing.
66
+ def self.reset_cache
67
+ @@mtimes = Hash.new
68
+ nil
69
+ end
70
+ end
71
+
72
+ end
@@ -0,0 +1,76 @@
1
+ module Jekyll
2
+
3
+ class HighlightBlock < Liquid::Block
4
+ include Liquid::StandardFilters
5
+
6
+ # The regular expression syntax checker. Start with the language specifier.
7
+ # Follow that by zero or more space separated options that take one of two
8
+ # forms:
9
+ #
10
+ # 1. name
11
+ # 2. name=value
12
+ SYNTAX = /^([a-zA-Z0-9.+#-]+)((\s+\w+(=\w+)?)*)$/
13
+
14
+ def initialize(tag_name, markup, tokens)
15
+ super
16
+ if markup.strip =~ SYNTAX
17
+ @lang = $1
18
+ if defined?($2) && $2 != ''
19
+ tmp_options = {}
20
+ $2.split.each do |opt|
21
+ key, value = opt.split('=')
22
+ if value.nil?
23
+ if key == 'linenos'
24
+ value = 'inline'
25
+ else
26
+ value = true
27
+ end
28
+ end
29
+ tmp_options[key] = value
30
+ end
31
+ tmp_options = tmp_options.to_a.sort.collect { |opt| opt.join('=') }
32
+ # additional options to pass to Albino
33
+ @options = { 'O' => tmp_options.join(',') }
34
+ else
35
+ @options = {}
36
+ end
37
+ else
38
+ raise SyntaxError.new("Syntax Error in 'highlight' - Valid syntax: highlight <lang> [linenos]")
39
+ end
40
+ end
41
+
42
+ def render(context)
43
+ if context.registers[:site].pygments
44
+ render_pygments(context, super)
45
+ else
46
+ render_codehighlighter(context, super)
47
+ end
48
+ end
49
+
50
+ def render_pygments(context, code)
51
+ output = add_code_tags(Albino.new(code, @lang).to_s(@options), @lang)
52
+ output = context["pygments_prefix"] + output if context["pygments_prefix"]
53
+ output = output + context["pygments_suffix"] if context["pygments_suffix"]
54
+ output
55
+ end
56
+
57
+ def render_codehighlighter(context, code)
58
+ #The div is required because RDiscount blows ass
59
+ <<-HTML
60
+ <div>
61
+ <pre><code class='#{@lang}'>#{h(code).strip}</code></pre>
62
+ </div>
63
+ HTML
64
+ end
65
+
66
+ def add_code_tags(code, lang)
67
+ # Add nested <code> tags to code blocks
68
+ code = code.sub(/<pre>/,'<pre><code class="' + lang + '">')
69
+ code = code.sub(/<\/pre>/,"</code></pre>")
70
+ end
71
+
72
+ end
73
+
74
+ end
75
+
76
+ Liquid::Template.register_tag('highlight', Jekyll::HighlightBlock)