jekyll-reloaded 0.12

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 (109) 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 +161 -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/jekyll.gemspec +146 -0
  19. data/lib/guard/jekyll.rb +57 -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 +125 -0
  23. data/lib/jekyll/converters/textile.rb +50 -0
  24. data/lib/jekyll/convertible.rb +116 -0
  25. data/lib/jekyll/core_ext.rb +52 -0
  26. data/lib/jekyll/errors.rb +6 -0
  27. data/lib/jekyll/filters.rb +118 -0
  28. data/lib/jekyll/generator.rb +7 -0
  29. data/lib/jekyll/generators/pagination.rb +113 -0
  30. data/lib/jekyll/layout.rb +51 -0
  31. data/lib/jekyll/live_site.rb +216 -0
  32. data/lib/jekyll/migrators/csv.rb +26 -0
  33. data/lib/jekyll/migrators/drupal.rb +103 -0
  34. data/lib/jekyll/migrators/enki.rb +49 -0
  35. data/lib/jekyll/migrators/joomla.rb +53 -0
  36. data/lib/jekyll/migrators/marley.rb +52 -0
  37. data/lib/jekyll/migrators/mephisto.rb +84 -0
  38. data/lib/jekyll/migrators/mt.rb +86 -0
  39. data/lib/jekyll/migrators/posterous.rb +67 -0
  40. data/lib/jekyll/migrators/rss.rb +47 -0
  41. data/lib/jekyll/migrators/textpattern.rb +58 -0
  42. data/lib/jekyll/migrators/tumblr.rb +195 -0
  43. data/lib/jekyll/migrators/typo.rb +51 -0
  44. data/lib/jekyll/migrators/wordpress.rb +294 -0
  45. data/lib/jekyll/migrators/wordpressdotcom.rb +70 -0
  46. data/lib/jekyll/page.rb +160 -0
  47. data/lib/jekyll/plugin.rb +77 -0
  48. data/lib/jekyll/post.rb +262 -0
  49. data/lib/jekyll/site.rb +339 -0
  50. data/lib/jekyll/static_file.rb +77 -0
  51. data/lib/jekyll/tags/highlight.rb +118 -0
  52. data/lib/jekyll/tags/include.rb +37 -0
  53. data/lib/jekyll/tags/post_url.rb +38 -0
  54. data/lib/jekyll.rb +134 -0
  55. data/test/helper.rb +34 -0
  56. data/test/source/.htaccess +8 -0
  57. data/test/source/_includes/sig.markdown +3 -0
  58. data/test/source/_layouts/default.html +27 -0
  59. data/test/source/_layouts/simple.html +1 -0
  60. data/test/source/_posts/2008-02-02-not-published.textile +8 -0
  61. data/test/source/_posts/2008-02-02-published.textile +8 -0
  62. data/test/source/_posts/2008-10-18-foo-bar.textile +8 -0
  63. data/test/source/_posts/2008-11-21-complex.textile +8 -0
  64. data/test/source/_posts/2008-12-03-permalinked-post.textile +9 -0
  65. data/test/source/_posts/2008-12-13-include.markdown +8 -0
  66. data/test/source/_posts/2009-01-27-array-categories.textile +10 -0
  67. data/test/source/_posts/2009-01-27-categories.textile +7 -0
  68. data/test/source/_posts/2009-01-27-category.textile +7 -0
  69. data/test/source/_posts/2009-01-27-empty-categories.textile +7 -0
  70. data/test/source/_posts/2009-01-27-empty-category.textile +7 -0
  71. data/test/source/_posts/2009-03-12-hash-#1.markdown +6 -0
  72. data/test/source/_posts/2009-05-18-empty-tag.textile +6 -0
  73. data/test/source/_posts/2009-05-18-empty-tags.textile +6 -0
  74. data/test/source/_posts/2009-05-18-tag.textile +6 -0
  75. data/test/source/_posts/2009-05-18-tags.textile +9 -0
  76. data/test/source/_posts/2009-06-22-empty-yaml.textile +3 -0
  77. data/test/source/_posts/2009-06-22-no-yaml.textile +1 -0
  78. data/test/source/_posts/2010-01-08-triple-dash.markdown +5 -0
  79. data/test/source/_posts/2010-01-09-date-override.textile +7 -0
  80. data/test/source/_posts/2010-01-09-time-override.textile +7 -0
  81. data/test/source/_posts/2010-01-09-timezone-override.textile +7 -0
  82. data/test/source/_posts/2010-01-16-override-data.textile +4 -0
  83. data/test/source/_posts/2011-04-12-md-extension.md +7 -0
  84. data/test/source/_posts/2011-04-12-text-extension.text +0 -0
  85. data/test/source/about.html +6 -0
  86. data/test/source/category/_posts/2008-9-23-categories.textile +6 -0
  87. data/test/source/contacts.html +5 -0
  88. data/test/source/css/screen.css +76 -0
  89. data/test/source/deal.with.dots.html +7 -0
  90. data/test/source/foo/_posts/bar/2008-12-12-topical-post.textile +8 -0
  91. data/test/source/index.html +22 -0
  92. data/test/source/sitemap.xml +32 -0
  93. data/test/source/win/_posts/2009-05-24-yaml-linebreak.markdown +7 -0
  94. data/test/source/z_category/_posts/2008-9-23-categories.textile +6 -0
  95. data/test/suite.rb +11 -0
  96. data/test/test_configuration.rb +29 -0
  97. data/test/test_core_ext.rb +66 -0
  98. data/test/test_filters.rb +62 -0
  99. data/test/test_generated_site.rb +72 -0
  100. data/test/test_kramdown.rb +23 -0
  101. data/test/test_page.rb +117 -0
  102. data/test/test_pager.rb +113 -0
  103. data/test/test_post.rb +450 -0
  104. data/test/test_rdiscount.rb +18 -0
  105. data/test/test_redcarpet.rb +21 -0
  106. data/test/test_redcloth.rb +86 -0
  107. data/test/test_site.rb +220 -0
  108. data/test/test_tags.rb +201 -0
  109. metadata +332 -0
@@ -0,0 +1,118 @@
1
+ require 'uri'
2
+
3
+ module Jekyll
4
+
5
+ module Filters
6
+ # Convert a Textile string into HTML output.
7
+ #
8
+ # input - The Textile String to convert.
9
+ #
10
+ # Returns the HTML formatted String.
11
+ def textilize(input)
12
+ site = @context.registers[:site]
13
+ converter = site.getConverterImpl(Jekyll::TextileConverter)
14
+ converter.convert(input)
15
+ end
16
+
17
+ # Convert a Markdown string into HTML output.
18
+ #
19
+ # input - The Markdown String to convert.
20
+ #
21
+ # Returns the HTML formatted String.
22
+ def markdownify(input)
23
+ site = @context.registers[:site]
24
+ converter = site.getConverterImpl(Jekyll::MarkdownConverter)
25
+ converter.convert(input)
26
+ end
27
+
28
+ # Format a date in short format e.g. "27 Jan 2011".
29
+ #
30
+ # date - the Time to format.
31
+ #
32
+ # Returns the formatting String.
33
+ def date_to_string(date)
34
+ date.strftime("%d %b %Y")
35
+ end
36
+
37
+ # Format a date in long format e.g. "27 January 2011".
38
+ #
39
+ # date - The Time to format.
40
+ #
41
+ # Returns the formatted String.
42
+ def date_to_long_string(date)
43
+ date.strftime("%d %B %Y")
44
+ end
45
+
46
+ # Format a date for use in XML.
47
+ #
48
+ # date - The Time to format.
49
+ #
50
+ # Examples
51
+ #
52
+ # date_to_xmlschema(Time.now)
53
+ # # => "2011-04-24T20:34:46+08:00"
54
+ #
55
+ # Returns the formatted String.
56
+ def date_to_xmlschema(date)
57
+ date.xmlschema
58
+ end
59
+
60
+ def xml_escape(input)
61
+ CGI.escapeHTML(input)
62
+ end
63
+
64
+ # CGI escape a string for use in a URL. Replaces any special characters
65
+ # with appropriate %XX replacements.
66
+ #
67
+ # input - The String to escape.
68
+ #
69
+ # Examples
70
+ #
71
+ # cgi_escape('foo,bar;baz?')
72
+ # # => "foo%2Cbar%3Bbaz%3F"
73
+ #
74
+ # Returns the escaped String.
75
+ def cgi_escape(input)
76
+ CGI::escape(input)
77
+ end
78
+
79
+ def uri_escape(input)
80
+ URI.escape(input)
81
+ end
82
+
83
+ # Count the number of words in the input string.
84
+ #
85
+ # input - The String on which to operate.
86
+ #
87
+ # Returns the Integer word count.
88
+ def number_of_words(input)
89
+ input.split.length
90
+ end
91
+
92
+ # Join an array of things into a string by separating with commes and the
93
+ # word "and" for the last one.
94
+ #
95
+ # array - The Array of Strings to join.
96
+ #
97
+ # Examples
98
+ #
99
+ # array_to_sentence_string(["apples", "oranges", "grapes"])
100
+ # # => "apples, oranges, and grapes"
101
+ #
102
+ # Returns the formatted String.
103
+ def array_to_sentence_string(array)
104
+ connector = "and"
105
+ case array.length
106
+ when 0
107
+ ""
108
+ when 1
109
+ array[0].to_s
110
+ when 2
111
+ "#{array[0]} #{connector} #{array[1]}"
112
+ else
113
+ "#{array[0...-1].join(', ')}, #{connector} #{array[-1]}"
114
+ end
115
+ end
116
+
117
+ end
118
+ end
@@ -0,0 +1,7 @@
1
+ module Jekyll
2
+
3
+ class Generator < Plugin
4
+
5
+ end
6
+
7
+ end
@@ -0,0 +1,113 @@
1
+ module Jekyll
2
+
3
+ class Pagination < Generator
4
+ # This generator is safe from arbitrary code execution.
5
+ safe true
6
+
7
+ # Generate paginated pages if necessary.
8
+ #
9
+ # site - The Site.
10
+ #
11
+ # Returns nothing.
12
+ def generate(site)
13
+ site.pages.dup.each do |page|
14
+ paginate(site, page) if Pager.pagination_enabled?(site.config, page.name)
15
+ end
16
+ end
17
+
18
+ # Paginates the blog's posts. Renders the index.html file into paginated
19
+ # directories, e.g.: page2/index.html, page3/index.html, etc and adds more
20
+ # site-wide data.
21
+ #
22
+ # site - The Site.
23
+ # page - The index.html Page that requires pagination.
24
+ #
25
+ # {"paginator" => { "page" => <Number>,
26
+ # "per_page" => <Number>,
27
+ # "posts" => [<Post>],
28
+ # "total_posts" => <Number>,
29
+ # "total_pages" => <Number>,
30
+ # "previous_page" => <Number>,
31
+ # "next_page" => <Number> }}
32
+ def paginate(site, page)
33
+ all_posts = site.site_payload['site']['posts']
34
+ pages = Pager.calculate_pages(all_posts, site.config['paginate'].to_i)
35
+ (1..pages).each do |num_page|
36
+ pager = Pager.new(site.config, num_page, all_posts, pages)
37
+ if num_page > 1
38
+ newpage = Page.new(site, site.source, page.dir, page.name)
39
+ newpage.pager = pager
40
+ newpage.dir = File.join(page.dir, "page#{num_page}")
41
+ site.pages << newpage
42
+ else
43
+ page.pager = pager
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ class Pager
50
+ attr_reader :page, :per_page, :posts, :total_posts, :total_pages, :previous_page, :next_page
51
+
52
+ # Calculate the number of pages.
53
+ #
54
+ # all_posts - The Array of all Posts.
55
+ # per_page - The Integer of entries per page.
56
+ #
57
+ # Returns the Integer number of pages.
58
+ def self.calculate_pages(all_posts, per_page)
59
+ (all_posts.size.to_f / per_page.to_i).ceil
60
+ end
61
+
62
+ # Determine if pagination is enabled for a given file.
63
+ #
64
+ # config - The configuration Hash.
65
+ # file - The String filename of the file.
66
+ #
67
+ # Returns true if pagination is enabled, false otherwise.
68
+ def self.pagination_enabled?(config, file)
69
+ file == 'index.html' && !config['paginate'].nil?
70
+ end
71
+
72
+ # Initialize a new Pager.
73
+ #
74
+ # config - The Hash configuration of the site.
75
+ # page - The Integer page number.
76
+ # all_posts - The Array of all the site's Posts.
77
+ # num_pages - The Integer number of pages or nil if you'd like the number
78
+ # of pages calculated.
79
+ def initialize(config, page, all_posts, num_pages = nil)
80
+ @page = page
81
+ @per_page = config['paginate'].to_i
82
+ @total_pages = num_pages || Pager.calculate_pages(all_posts, @per_page)
83
+
84
+ if @page > @total_pages
85
+ raise RuntimeError, "page number can't be greater than total pages: #{@page} > #{@total_pages}"
86
+ end
87
+
88
+ init = (@page - 1) * @per_page
89
+ offset = (init + @per_page - 1) >= all_posts.size ? all_posts.size : (init + @per_page - 1)
90
+
91
+ @total_posts = all_posts.size
92
+ @posts = all_posts[init..offset]
93
+ @previous_page = @page != 1 ? @page - 1 : nil
94
+ @next_page = @page != @total_pages ? @page + 1 : nil
95
+ end
96
+
97
+ # Convert this Pager's data to a Hash suitable for use by Liquid.
98
+ #
99
+ # Returns the Hash representation of this Pager.
100
+ def to_liquid
101
+ {
102
+ 'page' => page,
103
+ 'per_page' => per_page,
104
+ 'posts' => posts,
105
+ 'total_posts' => total_posts,
106
+ 'total_pages' => total_pages,
107
+ 'previous_page' => previous_page,
108
+ 'next_page' => next_page
109
+ }
110
+ end
111
+ end
112
+
113
+ end
@@ -0,0 +1,51 @@
1
+ module Jekyll
2
+
3
+ class Layout
4
+ include Convertible
5
+
6
+ # Gets the Site object.
7
+ attr_reader :site
8
+
9
+ # Gets/Sets the extension of this layout.
10
+ attr_accessor :ext
11
+
12
+ # Gets/Sets the Hash that holds the metadata for this layout.
13
+ attr_accessor :data
14
+
15
+ # Gets/Sets the content of this layout.
16
+ attr_accessor :content
17
+
18
+ # Initialize a new Layout.
19
+ #
20
+ # site - The Site.
21
+ # base - The String path to the source.
22
+ # name - The String filename of the post file.
23
+ def initialize(site, base, name)
24
+ @site = site
25
+ @base = base
26
+ @name = name
27
+
28
+ self.process(name)
29
+ self.read_yaml(base, name)
30
+ end
31
+
32
+ # The source filename for this layout.
33
+ def filename
34
+ File.join(@base, @name)
35
+ end
36
+
37
+ def inspect
38
+ "<Layout: @name=#{@name.inspect}>"
39
+ end
40
+
41
+ # Extract information from the layout filename.
42
+ #
43
+ # name - The String filename of the layout file.
44
+ #
45
+ # Returns nothing.
46
+ def process(name)
47
+ self.ext = File.extname(name)
48
+ end
49
+ end
50
+
51
+ end
@@ -0,0 +1,216 @@
1
+ require 'jekyll'
2
+ require 'jekyll/site'
3
+
4
+ module Jekyll
5
+
6
+ class LiveSite < Site
7
+ def process_files files
8
+ pages = []
9
+ Array(files).each do |filename|
10
+ resolve_file(filename) { |page| pages << page }
11
+ end
12
+
13
+ return false if pages.empty?
14
+
15
+ payload = nil
16
+
17
+ for page in pages
18
+ if page.respond_to? :render
19
+ payload ||= self.site_payload
20
+ page.read_yaml('', page.filename)
21
+ # TODO: remove post if stopped being published?
22
+ page.render(self.layouts, payload)
23
+ end
24
+
25
+ if page.write(self.dest)
26
+ yield page.destination(self.dest)
27
+ end
28
+ end
29
+ end
30
+
31
+ def resolve_file filename
32
+ path = File.expand_path filename, self.source
33
+ file = ContentFile.new path, self.source, self.dest, self.method(:allow_file?)
34
+ unless file.valid?
35
+ warn "invalid file: #{file.path}"
36
+ return
37
+ end
38
+
39
+ case file.type
40
+ when :post
41
+ page = self.posts.find { |p| p.filename == file.path }
42
+ page ||= create_post(file.relative_dir, file.name)
43
+ yield page
44
+ when :layout
45
+ if pair = self.layouts.find { |_, p| p.filename == file.path }
46
+ layout_name = pair[0]
47
+ else
48
+ layout_name = file.name.sub(/\.[^.]+$/, '')
49
+ self.layouts[layout_name] = Layout.new(self, file.dir, file.name)
50
+ end
51
+ [self.posts, self.pages].each do |group|
52
+ group.each { |p| yield p if using_layout?(layout_name, p) }
53
+ end
54
+ when :file
55
+ page = nil
56
+ [self.pages, self.static_files].each do |group|
57
+ if page = group.find { |p| p.filename == file.path }
58
+ break
59
+ end
60
+ end
61
+
62
+ page ||= if has_yaml_header? file.path
63
+ new_page = Page.new(self, self.source, file.dir, file.name)
64
+ self.pages << new_page
65
+ new_page
66
+ else
67
+ new_file = StaticFile.new(self, self.source, file.dir, file.name)
68
+ self.static_files << new_file
69
+ new_file
70
+ end
71
+
72
+ yield page
73
+ else
74
+ raise "unknown file type: #{file.type}"
75
+ end
76
+ rescue InvalidPost => e
77
+ warn e.message
78
+ end
79
+
80
+ class ContentFile
81
+ attr_reader :path, :dir, :name
82
+ alias to_s path
83
+
84
+ TYPE_MAPPING = {
85
+ '_layouts' => :layout,
86
+ '_includes' => :include,
87
+ '_posts' => :post
88
+ }
89
+
90
+ def initialize path, source, dest, allowed_checker = nil
91
+ @path = path
92
+ @source = source
93
+ @destination = dest
94
+ @allowed_checker = allowed_checker || lambda {|f| true }
95
+ @dir, @name = File.split @path
96
+ @type = :file
97
+ @valid_dir = nil
98
+ end
99
+
100
+ def type
101
+ deep_check
102
+ @type
103
+ end
104
+
105
+ def relative_dir
106
+ @relative_dir ||= self.dir.sub(File.join(@source, ''), '')
107
+ end
108
+
109
+ def valid?
110
+ !File.symlink? path and
111
+ @allowed_checker.call(name) and
112
+ deep_check
113
+ end
114
+
115
+ def deep_check
116
+ return @valid_dir unless @valid_dir.nil?
117
+ @valid_dir = valid_dir? self.dir
118
+ end
119
+
120
+ def valid_dir? path
121
+ if path == '/'
122
+ false
123
+ elsif path == @destination
124
+ false
125
+ elsif path == @source
126
+ true
127
+ elsif File.symlink? path
128
+ false
129
+ else
130
+ dir, name = File.split path
131
+ if type = TYPE_MAPPING[name]
132
+ @type = type
133
+ elsif !@allowed_checker.call(name)
134
+ return false
135
+ end
136
+ valid_dir? dir
137
+ end
138
+ end
139
+ end
140
+
141
+ class InvalidPost < StandardError; end
142
+
143
+ def create_post dir, name
144
+ path = File.join dir, name
145
+ # TODO: hack
146
+ dir, name = path.split('/_posts/', 2)
147
+
148
+ if Post.valid? name
149
+ post = Post.new(self, self.source, dir, name)
150
+
151
+ if publish_post? post
152
+ self.posts << post
153
+ post.categories.each { |c| self.categories[c] << post }
154
+ post.tags.each { |c| self.tags[c] << post }
155
+ post
156
+ else
157
+ raise InvalidPost, "post not publishable: #{post.inspect}"
158
+ end
159
+ else
160
+ raise InvalidPost, "not a valid post: #{name.inspect}"
161
+ end
162
+ end
163
+
164
+ def publish_post? post
165
+ post.published && (self.future || post.date <= self.time)
166
+ end
167
+
168
+ def using_layout? name, page, seen = nil
169
+ if using = page.data['layout'] and (seen.nil? or !seen.include?(using))
170
+ using == name or using_layout?(name, self.layouts[using], (seen || []) << using)
171
+ end
172
+ end
173
+
174
+ def has_yaml_header? file
175
+ '---' == File.open(file) { |fd| fd.read(3) }
176
+ end
177
+
178
+ FILTER_PREFIXES = %w[ . _ # ]
179
+ FILTER_SUFFIXES = %w[ ~ ]
180
+
181
+ # Filter out any files/directories that are hidden or backup files (start
182
+ # with ".", "_", or "#", or end with "~"), or are excluded by the site
183
+ # configuration, unless they are whitelisted by the site configuration.
184
+ #
185
+ # To be able to check symlinks, this method expects to be run from the
186
+ # directory where entries originate.
187
+ #
188
+ # entries - The Array of file/directory entries to filter.
189
+ #
190
+ # Returns the Array of filtered entries.
191
+ def filter_entries(entries)
192
+ entries.select { |entry|
193
+ allow_file? entry and !File.symlink? entry
194
+ }
195
+ end
196
+
197
+ def allow_file? entry
198
+ whitelisted_file? entry or
199
+ (allow_file_name? entry and !blacklisted_file? entry)
200
+ end
201
+
202
+ def allow_file_name? entry
203
+ !(FILTER_PREFIXES.include? entry[0,1] or
204
+ FILTER_SUFFIXES.include? entry[-1,1])
205
+ end
206
+
207
+ def whitelisted_file? entry
208
+ self.include.include? entry
209
+ end
210
+
211
+ def blacklisted_file? entry
212
+ self.exclude.include? entry
213
+ end
214
+ end
215
+
216
+ end
@@ -0,0 +1,26 @@
1
+ module Jekyll
2
+ module CSV
3
+ # Reads a csv with title, permalink, body, published_at, and filter.
4
+ # It creates a post file for each row in the csv
5
+ def self.process(file = "posts.csv")
6
+ FileUtils.mkdir_p "_posts"
7
+ posts = 0
8
+ FasterCSV.foreach(file) do |row|
9
+ next if row[0] == "title"
10
+ posts += 1
11
+ name = row[3].split(" ")[0]+"-"+row[1]+(row[4] =~ /markdown/ ? ".markdown" : ".textile")
12
+ File.open("_posts/#{name}", "w") do |f|
13
+ f.puts <<-HEADER
14
+ ---
15
+ layout: post
16
+ title: #{row[0]}
17
+ ---
18
+
19
+ HEADER
20
+ f.puts row[2]
21
+ end
22
+ end
23
+ "Created #{posts} posts!"
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,103 @@
1
+ require 'rubygems'
2
+ require 'sequel'
3
+ require 'fileutils'
4
+ require 'yaml'
5
+
6
+ # NOTE: This converter requires Sequel and the MySQL gems.
7
+ # The MySQL gem can be difficult to install on OS X. Once you have MySQL
8
+ # installed, running the following commands should work:
9
+ # $ sudo gem install sequel
10
+ # $ sudo gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config
11
+
12
+ module Jekyll
13
+ module Drupal
14
+ # Reads a MySQL database via Sequel and creates a post file for each post
15
+ # in wp_posts that has post_status = 'publish'. This restriction is made
16
+ # because 'draft' posts are not guaranteed to have valid dates.
17
+ QUERY = "SELECT n.nid, \
18
+ n.title, \
19
+ nr.body, \
20
+ n.created, \
21
+ n.status \
22
+ FROM node AS n, \
23
+ node_revisions AS nr \
24
+ WHERE (n.type = 'blog' OR n.type = 'story') \
25
+ AND n.vid = nr.vid"
26
+
27
+ def self.process(dbname, user, pass, host = 'localhost', prefix = '')
28
+ db = Sequel.mysql(dbname, :user => user, :password => pass, :host => host, :encoding => 'utf8')
29
+
30
+ if prefix != ''
31
+ QUERY[" node "] = " " + prefix + "node "
32
+ QUERY[" node_revisions "] = " " + prefix + "node_revisions "
33
+ end
34
+
35
+ FileUtils.mkdir_p "_posts"
36
+ FileUtils.mkdir_p "_drafts"
37
+
38
+ # Create the refresh layout
39
+ # Change the refresh url if you customized your permalink config
40
+ File.open("_layouts/refresh.html", "w") do |f|
41
+ f.puts <<EOF
42
+ <!DOCTYPE html>
43
+ <html>
44
+ <head>
45
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
46
+ <meta http-equiv="refresh" content="0;url={{ page.refresh_to_post_id }}.html" />
47
+ </head>
48
+ </html>
49
+ EOF
50
+ end
51
+
52
+ db[QUERY].each do |post|
53
+ # Get required fields and construct Jekyll compatible name
54
+ node_id = post[:nid]
55
+ title = post[:title]
56
+ content = post[:body]
57
+ created = post[:created]
58
+ time = Time.at(created)
59
+ is_published = post[:status] == 1
60
+ dir = is_published ? "_posts" : "_drafts"
61
+ slug = title.strip.downcase.gsub(/(&|&amp;)/, ' and ').gsub(/[\s\.\/\\]/, '-').gsub(/[^\w-]/, '').gsub(/[-_]{2,}/, '-').gsub(/^[-_]/, '').gsub(/[-_]$/, '')
62
+ name = time.strftime("%Y-%m-%d-") + slug + '.md'
63
+
64
+ # Get the relevant fields as a hash, delete empty fields and convert
65
+ # to YAML for the header
66
+ data = {
67
+ 'layout' => 'post',
68
+ 'title' => title.to_s,
69
+ 'created' => created,
70
+ }.delete_if { |k,v| v.nil? || v == ''}.to_yaml
71
+
72
+ # Write out the data and content to file
73
+ File.open("#{dir}/#{name}", "w") do |f|
74
+ f.puts data
75
+ f.puts "---"
76
+ f.puts content
77
+ end
78
+
79
+ # Make a file to redirect from the old Drupal URL
80
+ if is_published
81
+ aliases = db["SELECT dst FROM #{prefix}url_alias WHERE src = ?", "node/#{node_id}"].all
82
+
83
+ aliases.push(:dst => "node/#{node_id}")
84
+
85
+ aliases.each do |url_alias|
86
+ FileUtils.mkdir_p url_alias[:dst]
87
+ File.open("#{url_alias[:dst]}/index.md", "w") do |f|
88
+ f.puts "---"
89
+ f.puts "layout: refresh"
90
+ f.puts "refresh_to_post_id: /#{time.strftime("%Y/%m/%d/") + slug}"
91
+ f.puts "---"
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ # TODO: Make dirs & files for nodes of type 'page'
98
+ # Make refresh pages for these as well
99
+
100
+ # TODO: Make refresh dirs & files according to entries in url_alias table
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,49 @@
1
+ # Adapted by Rodrigo Pinto <rodrigopqn@gmail.com>
2
+ # Based on typo.rb by Toby DiPasquale
3
+
4
+ require 'fileutils'
5
+ require 'rubygems'
6
+ require 'sequel'
7
+
8
+ module Jekyll
9
+ module Enki
10
+ SQL = <<-EOS
11
+ SELECT p.id,
12
+ p.title,
13
+ p.slug,
14
+ p.body,
15
+ p.published_at as date,
16
+ p.cached_tag_list as tags
17
+ FROM posts p
18
+ EOS
19
+
20
+ # Just working with postgres, but can be easily adapted
21
+ # to work with both mysql and postgres.
22
+ def self.process(dbname, user, pass, host = 'localhost')
23
+ FileUtils.mkdir_p('_posts')
24
+ db = Sequel.postgres(:database => dbname,
25
+ :user => user,
26
+ :password => pass,
27
+ :host => host,
28
+ :encoding => 'utf8')
29
+
30
+ db[SQL].each do |post|
31
+ name = [ sprintf("%.04d", post[:date].year),
32
+ sprintf("%.02d", post[:date].month),
33
+ sprintf("%.02d", post[:date].day),
34
+ post[:slug].strip ].join('-')
35
+ name += '.textile'
36
+
37
+ File.open("_posts/#{name}", 'w') do |f|
38
+ f.puts({ 'layout' => 'post',
39
+ 'title' => post[:title].to_s,
40
+ 'enki_id' => post[:id],
41
+ 'categories' => post[:tags]
42
+ }.delete_if { |k, v| v.nil? || v == '' }.to_yaml)
43
+ f.puts '---'
44
+ f.puts post[:body].delete("\r")
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end