diff --git a/History.txt b/History.txt index a0326f16d..bb0887b49 100644 --- a/History.txt +++ b/History.txt @@ -1,3 +1,30 @@ +== + * Minor Enhancements + * Type importer [github.com/codeslinger] + * site.topics accessor [github.com/baz] + * Add array_to_sentence_string filter [github.com/mchung] + +== 0.3.0 / 2008-12-24 + * Major Enhancements + * Added --server option to start a simple WEBrick server on destination directory [github.com/johnreilly and github.com/mchung] + * Minor Enhancements + * Added post categories based on directories containing _posts [github.com/mreid] + * Added post topics based on directories underneath _posts + * Added new date filter that shows the full month name [github.com/mreid] + * Merge Post's YAML front matter into its to_liquid payload [github.com/remi] + * Restrict includes to regular files underneath _includes + * Bug Fixes + * Change YAML delimiter matcher so as to not chew up 2nd level markdown headers [github.com/mreid] + * Fix bug that meant page data (such as the date) was not available in templates [github.com/mreid] + * Properly reject directories in _layouts + +== 0.2.1 / 2008-12-15 + * Major Changes + * Use Maruku (pure Ruby) for Markdown by default [github.com/mreid] + * Allow use of RDiscount with --rdiscount flag + * Minor Enhancements + * Don't load directory_watcher unless it's needed [github.com/pjhyett] + == 0.2.0 / 2008-12-14 * Major Changes * related_posts is now found in site.related_posts diff --git a/Manifest.txt b/Manifest.txt index f30590130..9ed6da420 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -8,7 +8,11 @@ lib/jekyll.rb lib/jekyll/albino.rb lib/jekyll/converters/csv.rb lib/jekyll/converters/mephisto.rb +lib/jekyll/converters/mt.rb +lib/jekyll/converters/typo.rb +lib/jekyll/converters/wordpress.rb lib/jekyll/convertible.rb +lib/jekyll/core_ext.rb lib/jekyll/filters.rb lib/jekyll/layout.rb lib/jekyll/page.rb @@ -17,16 +21,18 @@ lib/jekyll/site.rb lib/jekyll/tags/highlight.rb lib/jekyll/tags/include.rb test/helper.rb -test/source/_includes/sig.textile +test/source/_includes/sig.markdown test/source/_layouts/default.html test/source/_layouts/simple.html test/source/_posts/2008-10-18-foo-bar.textile test/source/_posts/2008-11-21-complex.textile -test/source/_posts/2008-12-13-include.textile +test/source/_posts/2008-12-03-permalinked-post.textile +test/source/_posts/2008-12-13-include.markdown test/source/css/screen.css test/source/index.html -test/source/posts/2008-12-03-permalinked-post.textile test/suite.rb +test/test_filters.rb +test/test_generated_site.rb test/test_jekyll.rb test/test_post.rb test/test_site.rb diff --git a/README.textile b/README.textile index 5bfdc4dfb..4fa4c4061 100644 --- a/README.textile +++ b/README.textile @@ -53,12 +53,11 @@ filename is used to construct the URL in the generated site. The example post, for instance, ends up at http://tom.preston-werner.com/2008/11/17/blogging-like-a-hacker.html. -Categories for posts are derived from the directory structure the posts were -found within. -A post that appears in the directory foo/bar/_posts is placed in the categories -'foo' and 'bar'. -By selecting posts from particular categories in your Liquid templates, you will -be able to host multiple blogs within a site. +Categories for posts are derived from the directory structure the posts were +found within. A post that appears in the directory foo/bar/_posts is placed in +the categories 'foo' and 'bar'. By selecting posts from particular categories +in your Liquid templates, you will be able to host multiple blogs within a +site. Files that do not reside in directories prefixed with an underscore are mirrored into a corresponding directory structure in the generated site. If a @@ -88,7 +87,7 @@ The best way to install Jekyll is via RubyGems: $ sudo gem install mojombo-jekyll -s http://gems.github.com/ Jekyll requires the gems `directory_watcher`, `liquid`, `open4`, -and `maruku` for markdown support. These are automatically +and `maruku` (for markdown support). These are automatically installed by the gem install command. Maruku comes with optional support for LaTeX to PNG rendering via @@ -131,6 +130,21 @@ during the conversion: $ jekyll --pygments +By default, Jekyll uses "Maruku":http://maruku.rubyforge.org (pure Ruby) for +Markdown support. If you'd like to use RDiscount (faster, but requires +compilation), you must install it (gem install rdiscount) and then you can +have it used instead: + + $ jekyll --rdiscount + +When previewing complex sites locally, simply opening the site in a web +browser (using file://) can cause problems with links that are relative to +the site root (e.g., "/stylesheets/style.css"). To get around this, Jekyll +can launch a simple WEBrick server (works well in conjunction with --auto). +Default port is 4000: + + $ jekyll --server [PORT] + h2. Data Jekyll traverses your site looking for files to process. Any files with YAML @@ -150,7 +164,7 @@ h3. Global content In layout files, this contains the content of the subview(s). In Posts or - pages, this is undefined. + Pages, this is undefined. h3. Site @@ -167,7 +181,7 @@ h3. Site --lsi (latent semantic indexing) option. site.categories.CATEGORY - The list of all posts in category CATEGORY. + The list of all Posts in category CATEGORY. h3. Post @@ -185,12 +199,21 @@ h3. Post An identifier unique to the Post (useful in RSS feeds). e.g. /2008/12/14/my-post + post.categories + The list of categories to which this post belongs. Categories are + derived from the directory structure above the _posts directory. For + example, a post at /work/code/_posts/2008-12-24-closures.textile + would have this field set to ['work', 'code']. + + post.topics + The list of topics for this Post. Topics are derived from the directory + structure beneath the _posts directory. For example, a post at + /_posts/music/metal/2008-12-24-metalocalypse.textile would have this field + set to ['music', 'metal']. + post.content The content of the Post. - post.categories - The list of categories to which this post belongs. - h2. YAML Front Matter Any files that contain a YAML front matter block will be processed by Jekyll @@ -263,9 +286,20 @@ becomes 1337 +h3. Array to Sentence String + +Convert an array into a sentence. + + {{ page.tags | array_to_sentence_string }} + +becomes + + foo, bar, and baz + h3. Include (Tag) -If you have small page fragments that you wish to include in multiple places on your site, you can use the include tag. +If you have small page fragments that you wish to include in multiple places +on your site, you can use the include tag.
{% include sig.textile %}
@@ -332,11 +366,49 @@ The best way to get your changes merged back into core is as follows: # Clone down your fork # Create a topic branch to contain your change # Hack away +# If you are adding new functionality, document it in README.textile # Do not change the version number, I will do that on my end # If necessary, rebase your commits into logical chunks, without errors # Push the branch up to GitHub # Send me (mojombo) a pull request for your branch +h2. Blog migrations + +h3. Movable Type + +To migrate your MT blog into Jekyll, you'll need read access to the database. +The lib/jekyll/converters/mt.rb module provides a simple convert to create +.markdown files in a _posts directory based on the entries contained therein. + + $ export DB=my_mtdb + $ export USER=dbuser + $ export PASS=dbpass + $ ruby -r './lib/jekyll/converters/mt' -e 'Jekyll::MT.process( \ + "#{ENV["DB"]}", "#{ENV["USER"]}", "#{ENV["PASS"]}")' + +You may need to adjust the SQL query used to retrieve MT entries. Left alone, +it will attempt to pull all entries across all blogs regardless of status. +Please check the results and verify the posts before publishing. + +h3. Typo 4+ + +To migrate your Typo blog into Jekyll, you'll need read access to the MySQL +database. The lib/jekyll/converters/typo.rb module provides a simple convert +to create .html, .textile, or .markdown files in a _posts directory based on +the entries contained therein. + + $ export DB=my_typo_db + $ export USER=dbuser + $ export PASS=dbpass + $ ruby -r './lib/jekyll/converters/typo' -e 'Jekyll::Typo.process( \ + "#{ENV["DB"]}", "#{ENV["USER"]}", "#{ENV["PASS"]}")' + +You may need to adjust the code used to filter Typo entries. Left alone, +it will attempt to pull all entries across all blogs that were published. +This code also has only been tested with Typo version 4+. Previous versions +of Typo may not convert correctly. Please check the results and verify the +posts before publishing. + h2. License (The MIT License) diff --git a/Rakefile b/Rakefile index 3241e9935..3f301a771 100644 --- a/Rakefile +++ b/Rakefile @@ -18,4 +18,12 @@ namespace :convert do task :mephisto do sh %q(ruby -r './lib/jekyll/converters/mephisto' -e 'Jekyll::Mephisto.postgres(:database => "#{ENV["DB"]}")') end + desc "Migrate from Movable Type in the current directory" + task :mt do + sh %q(ruby -r './lib/jekyll/converters/mt' -e 'Jekyll::MT.process("#{ENV["DB"]}", "#{ENV["USER"]}", "#{ENV["PASS"]}")') + end + desc "Migrate from Typo in the current directory" + task :typo do + sh %q(ruby -r './lib/jekyll/converters/typo' -e 'Jekyll::Typo.process("#{ENV["DB"]}", "#{ENV["USER"]}", "#{ENV["PASS"]}")') + end end diff --git a/bin/jekyll b/bin/jekyll index 6a641f6b6..d746806f7 100755 --- a/bin/jekyll +++ b/bin/jekyll @@ -25,6 +25,11 @@ opts = OptionParser.new do |opts| options[:auto] = true end + opts.on("--server [PORT]", "Start web server (default port 4000)") do |port| + options[:server] = true + options[:server_port] = port || 4000 + end + opts.on("--lsi", "Use LSI for better related posts") do Jekyll.lsi = true end @@ -32,6 +37,16 @@ opts = OptionParser.new do |opts| opts.on("--pygments", "Use pygments to highlight code") do Jekyll.pygments = true end + + opts.on("--rdiscount", "Use rdiscount gem for Markdown") do + begin + require 'rdiscount' + Jekyll.markdown_proc = Proc.new { |x| RDiscount.new(x).to_html } + puts 'Using rdiscount for Markdown' + rescue LoadError + puts 'You must have the rdiscount gem installed first' + end + end end opts.parse! @@ -69,6 +84,8 @@ case ARGV.size end if options[:auto] + require 'directory_watcher' + puts "Auto-regenerating enabled: #{source} -> #{destination}" dw = DirectoryWatcher.new(source) @@ -83,7 +100,28 @@ if options[:auto] dw.start - loop { sleep 1000 } + unless options[:server] + loop { sleep 1000 } + end else Jekyll.process(source, destination) + puts "Successfully generated site in #{destination}" +end + +if options[:server] + require 'webrick' + include WEBrick + + FileUtils.mkdir_p(destination) + + s = HTTPServer.new( + :Port => options[:server_port], + :DocumentRoot => destination + ) + t = Thread.new { + s.start + } + + trap("INT") { s.shutdown } + t.join() end \ No newline at end of file diff --git a/jekyll.gemspec b/jekyll.gemspec index 9b75d2693..ff8002f7f 100644 --- a/jekyll.gemspec +++ b/jekyll.gemspec @@ -1,22 +1,22 @@ Gem::Specification.new do |s| s.name = %q{jekyll} - s.version = "0.2.0" + s.version = "0.3.0" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Tom Preston-Werner"] - s.date = %q{2008-12-14} + s.date = %q{2008-12-24} s.default_executable = %q{jekyll} s.email = ["tom@mojombo.com"] s.executables = ["jekyll"] s.extra_rdoc_files = ["History.txt", "Manifest.txt"] - s.files = ["History.txt", "Manifest.txt", "README.textile", "Rakefile", "bin/jekyll", "jekyll.gemspec", "lib/jekyll.rb", "lib/jekyll/albino.rb", "lib/jekyll/converters/csv.rb", "lib/jekyll/converters/mephisto.rb", "lib/jekyll/convertible.rb", "lib/jekyll/filters.rb", "lib/jekyll/layout.rb", "lib/jekyll/page.rb", "lib/jekyll/post.rb", "lib/jekyll/site.rb", "lib/jekyll/tags/highlight.rb", "lib/jekyll/tags/include.rb", "test/helper.rb", "test/source/_includes/sig.textile", "test/source/_layouts/default.html", "test/source/_layouts/simple.html", "test/source/_posts/2008-10-18-foo-bar.textile", "test/source/_posts/2008-11-21-complex.textile", "test/source/_posts/2008-12-13-include.textile", "test/source/css/screen.css", "test/source/index.html", "test/source/posts/2008-12-03-permalinked-post.textile", "test/suite.rb", "test/test_jekyll.rb", "test/test_post.rb", "test/test_site.rb"] + s.files = ["History.txt", "Manifest.txt", "README.textile", "Rakefile", "bin/jekyll", "jekyll.gemspec", "lib/jekyll.rb", "lib/jekyll/albino.rb", "lib/jekyll/converters/csv.rb", "lib/jekyll/converters/mephisto.rb", "lib/jekyll/converters/mt.rb", "lib/jekyll/converters/wordpress.rb", "lib/jekyll/convertible.rb", "lib/jekyll/core_ext.rb", "lib/jekyll/filters.rb", "lib/jekyll/layout.rb", "lib/jekyll/page.rb", "lib/jekyll/post.rb", "lib/jekyll/site.rb", "lib/jekyll/tags/highlight.rb", "lib/jekyll/tags/include.rb", "test/helper.rb", "test/source/_includes/sig.markdown", "test/source/_layouts/default.html", "test/source/_layouts/simple.html", "test/source/_posts/2008-10-18-foo-bar.textile", "test/source/_posts/2008-11-21-complex.textile", "test/source/_posts/2008-12-03-permalinked-post.textile", "test/source/_posts/2008-12-13-include.markdown", "test/source/css/screen.css", "test/source/index.html", "test/suite.rb", "test/test_generated_site.rb", "test/test_jekyll.rb", "test/test_post.rb", "test/test_site.rb"] s.has_rdoc = true s.rdoc_options = ["--main", "README.txt"] s.require_paths = ["lib"] s.rubyforge_project = %q{jekyll} s.rubygems_version = %q{1.3.0} s.summary = %q{Jekyll is a simple, blog aware, static site generator.} - s.test_files = ["test/test_jekyll.rb", "test/test_post.rb", "test/test_site.rb"] + s.test_files = ["test/test_generated_site.rb", "test/test_jekyll.rb", "test/test_post.rb", "test/test_site.rb"] if s.respond_to? :specification_version then current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION diff --git a/lib/jekyll.rb b/lib/jekyll.rb index aa67073e2..829d1d4bd 100644 --- a/lib/jekyll.rb +++ b/lib/jekyll.rb @@ -29,9 +29,9 @@ begin rescue LoadError puts "The maruku gem is required for markdown support!" end -require 'directory_watcher' # internal requires +require 'jekyll/core_ext' require 'jekyll/site' require 'jekyll/convertible' require 'jekyll/layout' @@ -43,14 +43,15 @@ require 'jekyll/tags/include' require 'jekyll/albino' module Jekyll - VERSION = '0.2.0' + VERSION = '0.3.0' class << self - attr_accessor :source, :dest, :lsi, :pygments + attr_accessor :source, :dest, :lsi, :pygments, :markdown_proc end Jekyll.lsi = false Jekyll.pygments = false + Jekyll.markdown_proc = Proc.new { |x| Maruku.new(x).to_html } def self.process(source, dest) require 'classifier' if Jekyll.lsi diff --git a/lib/jekyll/converters/mt.rb b/lib/jekyll/converters/mt.rb new file mode 100644 index 000000000..3b98b139f --- /dev/null +++ b/lib/jekyll/converters/mt.rb @@ -0,0 +1,59 @@ +# Created by Nick Gerakines, open source and publically available under the +# MIT license. Use this module at your own risk. +# I'm an Erlang/Perl/C++ guy so please forgive my dirty ruby. + +require 'rubygems' +require 'sequel' +require 'fileutils' + +# NOTE: This converter requires Sequel and the MySQL gems. +# The MySQL gem can be difficult to install on OS X. Once you have MySQL +# installed, running the following commands should work: +# $ sudo gem install sequel +# $ sudo gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config + +module Jekyll + module MT + # This query will pull blog posts from all entries across all blogs. If + # you've got unpublished, deleted or otherwise hidden posts please sift + # through the created posts to make sure nothing is accidently published. + QUERY = "SELECT entry_id, entry_basename, entry_text, entry_text_more, entry_created_on, entry_title FROM mt_entry" + + def self.process(dbname, user, pass, host = 'localhost') + db = Sequel.mysql(dbname, :user => user, :password => pass, :host => host) + + FileUtils.mkdir_p "_posts" + + db[QUERY].each do |post| + title = post[:entry_title] + slug = post[:entry_basename] + date = post[:entry_created_on] + content = post[:entry_text] + more_content = post[:entry_text_more] + + # Be sure to include the body and extended body. + if more_content != nil + content = content + " \n" + more_content + end + + # Ideally, this script would determine the post format (markdown, html + # , etc) and create files with proper extensions. At this point it + # just assumes that markdown will be acceptable. + name = [date.year, date.month, date.day, slug].join('-') + ".markdown" + + data = { + 'layout' => 'post', + 'title' => title.to_s, + 'mt_id' => post[:entry_id], + }.delete_if { |k,v| v.nil? || v == ''}.to_yaml + + File.open("_posts/#{name}", "w") do |f| + f.puts data + f.puts "---" + f.puts content + end + end + + end + end +end diff --git a/lib/jekyll/converters/typo.rb b/lib/jekyll/converters/typo.rb new file mode 100644 index 000000000..febac65dc --- /dev/null +++ b/lib/jekyll/converters/typo.rb @@ -0,0 +1,49 @@ +# Author: Toby DiPasquale +require 'fileutils' +require 'rubygems' +require 'sequel' + +module Jekyll + module Typo + # this SQL *should* work for both MySQL and PostgreSQL, but I haven't + # tested PostgreSQL yet (as of 2008-12-16) + SQL = <<-EOS + SELECT c.id id, + c.title title, + c.permalink slug, + c.body body, + c.published_at date, + c.state state, + COALESCE(tf.name, 'html') filter + FROM contents c + LEFT OUTER JOIN text_filters tf + ON c.text_filter_id = tf.id + EOS + + def self.process dbname, user, pass, host='localhost' + FileUtils.mkdir_p '_posts' + db = Sequel.mysql dbname, :user => user, :password => pass, :host => host + db[SQL].each do |post| + next unless post[:state] =~ /Published/ + + name = [ sprintf("%.04d", post[:date].year), + sprintf("%.02d", post[:date].month), + sprintf("%.02d", post[:date].day), + post[:slug].strip ].join('-') + # Can have more than one text filter in this field, but we just want + # the first one for this + name += '.' + post[:filter].split(' ')[0] + + File.open("_posts/#{name}", 'w') do |f| + f.puts({ 'layout' => 'post', + 'title' => post[:title].to_s, + 'typo_id' => post[:id] + }.delete_if { |k, v| v.nil? || v == '' }.to_yaml) + f.puts '---' + f.puts post[:body].delete("\r") + end + end + end + + end # module Typo +end # module Jekyll diff --git a/lib/jekyll/convertible.rb b/lib/jekyll/convertible.rb index b8b9d82c1..d68d6039c 100644 --- a/lib/jekyll/convertible.rb +++ b/lib/jekyll/convertible.rb @@ -19,18 +19,18 @@ module Jekyll self.data = YAML.load($1) end end - + # Transform the contents based on the file extension. # # Returns nothing def transform - case self.ext - when ".textile": + case self.ext[1..-1] + when /textile/i self.ext = ".html" self.content = RedCloth.new(self.content).to_html - when ".markdown": + when /markdown/i, /mkdn/i, /md/i self.ext = ".html" - self.content = Maruku.new(self.content).to_html + self.content = Jekyll.markdown_proc.call(self.content) end end @@ -39,10 +39,8 @@ module Jekyll # +site_payload+ is the site payload hash # # Returns nothing - def do_layout(payload, layouts, site_payload) - # construct payload - payload = payload.merge(site_payload) - # render content + def do_layout(payload, layouts) + # render and transform content (this becomes the final content of the object) self.content = Liquid::Template.parse(self.content).render(payload, [Jekyll::Filters]) self.transform @@ -52,7 +50,7 @@ module Jekyll # recursively render layouts layout = layouts[self.data["layout"]] while layout - payload = payload.merge({"content" => self.output, "page" => payload['page']}) + payload = payload.deep_merge({"content" => self.output, "page" => layout.data}) self.output = Liquid::Template.parse(layout.content).render(payload, [Jekyll::Filters]) layout = layouts[layout.data["layout"]] diff --git a/lib/jekyll/core_ext.rb b/lib/jekyll/core_ext.rb new file mode 100644 index 000000000..de5847110 --- /dev/null +++ b/lib/jekyll/core_ext.rb @@ -0,0 +1,22 @@ +class Hash + # Merges self with another hash, recursively. + # + # This code was lovingly stolen from some random gem: + # http://gemjack.com/gems/tartan-0.1.1/classes/Hash.html + # + # Thanks to whoever made it. + def deep_merge(hash) + target = dup + + hash.keys.each do |key| + if hash[key].is_a? Hash and self[key].is_a? Hash + target[key] = target[key].deep_merge(hash[key]) + next + end + + target[key] = hash[key] + end + + target + end +end \ No newline at end of file diff --git a/lib/jekyll/filters.rb b/lib/jekyll/filters.rb index 6051626a8..6cfa6361d 100644 --- a/lib/jekyll/filters.rb +++ b/lib/jekyll/filters.rb @@ -19,6 +19,21 @@ module Jekyll def number_of_words(input) input.split.length - end + end + + def array_to_sentence_string(array) + connector = "and" + case array.length + when 0 + "" + when 1 + array[0].to_s + when 2 + "#{array[0]} #{connector} #{array[1]}" + else + "#{array[0...-1].join(', ')}, #{connector} #{array[-1]}" + end + end + end end \ No newline at end of file diff --git a/lib/jekyll/layout.rb b/lib/jekyll/layout.rb index 50397c40c..391cf69ce 100644 --- a/lib/jekyll/layout.rb +++ b/lib/jekyll/layout.rb @@ -28,21 +28,6 @@ module Jekyll def process(name) self.ext = File.extname(name) end - - # Add any necessary layouts to this post - # +layouts+ is a Hash of {"name" => "layout"} - # +site_payload+ is the site payload hash - # - # Returns nothing - def add_layout(layouts, site_payload) - payload = {"page" => self.data}.merge(site_payload) - self.content = Liquid::Template.parse(self.content).render(payload, [Jekyll::Filters]) - - layout = layouts[self.data["layout"]] || self.content - payload = {"content" => self.content, "page" => self.data} - - self.content = Liquid::Template.parse(layout).render(payload, [Jekyll::Filters]) - end end end \ No newline at end of file diff --git a/lib/jekyll/page.rb b/lib/jekyll/page.rb index 01fe88021..140258fcc 100644 --- a/lib/jekyll/page.rb +++ b/lib/jekyll/page.rb @@ -37,9 +37,9 @@ module Jekyll # +site_payload+ is the site payload hash # # Returns nothing - def add_layout(layouts, site_payload) - payload = {"page" => self.data} - do_layout(payload, layouts, site_payload) + def render(layouts, site_payload) + payload = {"page" => self.data}.deep_merge(site_payload) + do_layout(payload, layouts) end # Write the generated page file to the destination directory. diff --git a/lib/jekyll/post.rb b/lib/jekyll/post.rb index fabbaf85f..890cae8d4 100644 --- a/lib/jekyll/post.rb +++ b/lib/jekyll/post.rb @@ -8,7 +8,7 @@ module Jekyll attr_accessor :lsi end - MATCHER = /^(\d+-\d+-\d+)-(.*)(\.[^.]+)$/ + MATCHER = /^(.+\/)*(\d+-\d+-\d+)-(.*)(\.[^.]+)$/ # Post name validator. Post filenames must be like: # 2008-11-05-my-awesome-post.textile @@ -18,7 +18,7 @@ module Jekyll name =~ MATCHER end - attr_accessor :date, :slug, :ext, :categories + attr_accessor :date, :slug, :ext, :categories, :topics attr_accessor :data, :content, :output # Initialize this Post instance. @@ -27,15 +27,17 @@ module Jekyll # +categories+ is an Array of Strings for the categories for this post # # Returns - def initialize(base, name) - @base = base + def initialize(source, dir, name) + @base = File.join(source, dir, '_posts') @name = name - @categories = base.split('/').reject { |p| ['.', '_posts'].include? p } + + self.categories = dir.split('/').reject { |x| x.empty? } + + parts = name.split('/') + self.topics = parts.size > 1 ? parts[0..-2] : [] self.process(name) - self.read_yaml(base, name) - #Removed to avoid munging of liquid tags, replaced in convertible.rb#48 - #self.transform + self.read_yaml(@base, name) end # Spaceship is based on Post#date @@ -50,7 +52,7 @@ module Jekyll # # Returns nothing def process(name) - m, date, slug, ext = *name.match(MATCHER) + m, cats, date, slug, ext = *name.match(MATCHER) self.date = Time.parse(date) self.slug = slug self.ext = ext @@ -63,10 +65,12 @@ module Jekyll # # Returns def dir - path = @categories ? '/' + @categories.join('/') : '' - permalink ? - permalink.to_s.split("/")[0..-2].join("/") : - "#{path}" + date.strftime("/%Y/%m/%d/") + if permalink + permalink.to_s.split("/")[0..-2].join("/") + else + prefix = self.categories.empty? ? '' : '/' + self.categories.join('/') + prefix + date.strftime("/%Y/%m/%d/") + end end # The full path and filename of the post. @@ -121,11 +125,16 @@ module Jekyll # +site_payload+ is the site payload hash # # Returns nothing - def add_layout(layouts, site_payload) - # construct post payload - related = related_posts(site_payload["site"]["posts"]) - payload = {"page" => self.to_liquid.merge(self.data)} - do_layout(payload, layouts, site_payload.merge({"site" => {"related_posts" => related}})) + def render(layouts, site_payload) + # construct payload + payload = + { + "site" => { "related_posts" => related_posts(site_payload["site"]["posts"]) }, + "page" => self.to_liquid + } + payload = payload.deep_merge(site_payload) + + do_layout(payload, layouts) end # Write the generated post file to the destination directory. @@ -145,11 +154,16 @@ module Jekyll # # Returns def to_liquid - { "title" => self.data["title"] || "", + { "title" => self.data["title"] || self.slug.split('-').select {|w| w.capitalize! || w }.join(' '), "url" => self.url, "date" => self.date, "id" => self.id, - "content" => self.content } + "topics" => self.topics, + "content" => self.content }.deep_merge(self.data) + end + + def inspect + "" end end diff --git a/lib/jekyll/site.rb b/lib/jekyll/site.rb index b808bc59c..a0cdffd44 100644 --- a/lib/jekyll/site.rb +++ b/lib/jekyll/site.rb @@ -28,14 +28,15 @@ module Jekyll self.write_posts end - # Read all the files in /_layouts into memory for - # later use. + # Read all the files in /_layouts except backup files + # (end with "~") into memory for later use. # # Returns nothing def read_layouts base = File.join(self.source, "_layouts") entries = Dir.entries(base) - entries = entries.reject { |e| File.directory?(e) } + entries = entries.reject { |e| e[-1..-1] == '~' } + entries = entries.reject { |e| File.directory?(File.join(base, e)) } entries.each do |f| name = f.split(".")[0..-2].join(".") @@ -45,16 +46,29 @@ module Jekyll # ignore missing layout dir end - # Read all the files in /_posts and create a new Post - # object with each one. + # Read all the files in /_posts except backup files (end with "~") + # and create a new Post object with each one. # # Returns nothing - def read_posts(base) - entries = Dir.entries(base) - entries = entries.reject { |e| File.directory?(e) } + def read_posts(dir) + base = File.join(self.source, dir, '_posts') + + entries = [] + Dir.chdir(base) { entries = Dir['**/*'] } + entries = entries.reject { |e| e[-1..-1] == '~' } + entries = entries.reject { |e| File.directory?(File.join(base, e)) } + # first pass processes, but does not yet render post content entries.each do |f| - self.posts << Post.new(base, f) if Post.valid?(f) + if Post.valid?(f) + post = Post.new(self.source, dir, f) + self.posts << post + end + end + + # second pass renders each post now that full site payload is available + self.posts.each do |post| + post.render(self.layouts, site_payload) end self.posts.sort! @@ -67,14 +81,14 @@ module Jekyll # Returns nothing def write_posts self.posts.each do |post| - post.add_layout(self.layouts, site_payload) post.write(self.dest) end end # Copy all regular files from to / ignoring - # any files/directories that are hidden (start with ".") or contain - # site content (start with "_") unless they are "_posts" directories + # any files/directories that are hidden or backup files (start + # with "." or end with "~") or contain site content (start with "_") + # unless they are "_posts" directories # The +dir+ String is a relative path used to call this method # recursively as it descends through directories # @@ -82,47 +96,63 @@ module Jekyll def transform_pages(dir = '') base = File.join(self.source, dir) entries = Dir.entries(base) - entries = entries.reject { |e| - (e != '_posts') and ['.', '_'].include?(e[0..0]) - } + entries = entries.reject { |e| e[-1..-1] == '~' } + entries = entries.reject do |e| + (e != '_posts') and ['.', '_'].include?(e[0..0]) + end + + # we need to make sure to process _posts *first* otherwise they + # might not be available yet to other templates as {{ site.posts }} + if entries.include?('_posts') + entries.delete('_posts') + read_posts(dir) + end entries.each do |f| - if f == '_posts' - read_posts(File.join(base, f)) - elsif File.directory?(File.join(base, f)) + if File.directory?(File.join(base, f)) next if self.dest.sub(/\/$/, '') == File.join(base, f) transform_pages(File.join(dir, f)) else first3 = File.open(File.join(self.source, dir, f)) { |fd| fd.read(3) } - - # if the file appears to have a YAML header then process it as a page + if first3 == "---" + # file appears to have a YAML header so process it as a page page = Page.new(self.source, dir, f) - page.add_layout(self.layouts, site_payload) + page.render(self.layouts, site_payload) page.write(self.dest) - # otherwise copy the file without transforming it else + # otherwise copy the file without transforming it FileUtils.mkdir_p(File.join(self.dest, dir)) FileUtils.cp(File.join(self.source, dir, f), File.join(self.dest, dir, f)) end end end end - + + # Constructs a hash map of Posts indexed by the specified Post attribute + # + # Returns {post_attr => []} + def post_attr_hash(post_attr) + # Build a hash map based on the specified post attribute ( post attr => array of posts ) + # then sort each array in reverse order + hash = Hash.new { |hash, key| hash[key] = Array.new } + self.posts.each { |p| p.send(post_attr.to_sym).each { |t| hash[t] << p } } + hash.values.map { |sortme| sortme.sort! { |a, b| b <=> a} } + return hash + end + # The Hash payload containing site-wide data # - # Returns {"site" => {"time" =>