├── cucumber.yml ├── test ├── source │ ├── _layouts │ │ ├── simple.html │ │ └── default.html │ ├── _posts │ │ ├── 2009-06-22-no-yaml.textile │ │ ├── 2009-06-22-empty-yaml.textile │ │ ├── 2009-05-18-tag.textile │ │ ├── 2009-05-18-empty-tag.textile │ │ ├── 2009-05-18-empty-tags.textile │ │ ├── 2010-01-08-triple-dash.markdown │ │ ├── 2010-01-16-override-data.textile │ │ ├── 2009-03-12-hash-#1.markdown │ │ ├── 2008-10-18-foo-bar.textile │ │ ├── 2008-12-13-include.markdown │ │ ├── 2009-01-27-category.textile │ │ ├── 2009-01-27-empty-category.textile │ │ ├── 2009-05-18-tags.textile │ │ ├── 2009-01-27-empty-categories.textile │ │ ├── 2009-01-27-categories.textile │ │ ├── 2008-02-02-published.textile │ │ ├── 2008-11-21-complex.textile │ │ ├── 2010-01-09-date-override.textile │ │ ├── 2010-01-09-time-override.textile │ │ ├── 2008-02-02-not-published.textile │ │ ├── 2010-01-09-timezone-override.textile │ │ ├── 2009-01-27-array-categories.textile │ │ └── 2008-12-03-permalinked-post.textile │ ├── _includes │ │ └── sig.markdown │ ├── about.html │ ├── contacts.html │ ├── category │ │ └── _posts │ │ │ └── 2008-9-23-categories.textile │ ├── win │ │ └── _posts │ │ │ └── 2009-05-24-yaml-linebreak.markdown │ ├── deal.with.dots.html │ ├── z_category │ │ └── _posts │ │ │ └── 2008-9-23-categories.textile │ ├── foo │ │ └── _posts │ │ │ └── bar │ │ │ └── 2008-12-12-topical-post.textile │ ├── .htaccess │ ├── index.html │ ├── sitemap.xml │ └── css │ │ └── screen.css ├── suite.rb ├── test_rdiscount.rb ├── test_kramdown.rb ├── helper.rb ├── test_configuration.rb ├── test_filters.rb ├── test_generated_site.rb ├── test_core_ext.rb ├── test_pager.rb ├── test_tags.rb ├── test_page.rb ├── test_site.rb └── test_post.rb ├── .gitignore ├── lib ├── jekyll │ ├── generator.rb │ ├── errors.rb │ ├── converters │ │ ├── identity.rb │ │ ├── textile.rb │ │ └── markdown.rb │ ├── migrators │ │ ├── csv.rb │ │ ├── wordpress.com.rb │ │ ├── typo.rb │ │ ├── textpattern.rb │ │ ├── marley.rb │ │ ├── wordpress.rb │ │ ├── mephisto.rb │ │ ├── mt.rb │ │ └── drupal.rb │ ├── layout.rb │ ├── filters.rb │ ├── tags │ │ ├── include.rb │ │ └── highlight.rb │ ├── core_ext.rb │ ├── converter.rb │ ├── static_file.rb │ ├── plugin.rb │ ├── generators │ │ └── pagination.rb │ ├── convertible.rb │ ├── albino.rb │ ├── page.rb │ ├── post.rb │ └── site.rb └── jekyll.rb ├── features ├── support │ └── env.rb ├── pagination.feature ├── markdown.feature ├── embed_filters.feature ├── permalinks.feature ├── site_data.feature ├── step_definitions │ └── jekyll_steps.rb ├── create_sites.feature ├── site_configuration.feature └── post_data.feature ├── LICENSE ├── README.textile ├── Rakefile ├── jekyll.gemspec ├── bin └── jekyll └── History.txt /cucumber.yml: -------------------------------------------------------------------------------- 1 | default: --format progress -------------------------------------------------------------------------------- /test/source/_layouts/simple.html: -------------------------------------------------------------------------------- 1 | <<< {{ content }} >>> -------------------------------------------------------------------------------- /test/source/_posts/2009-06-22-no-yaml.textile: -------------------------------------------------------------------------------- 1 | No YAML. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test/dest 2 | *.gem 3 | pkg/ 4 | *.swp 5 | *~ 6 | _site/ 7 | -------------------------------------------------------------------------------- /test/source/_posts/2009-06-22-empty-yaml.textile: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | Empty YAML. -------------------------------------------------------------------------------- /test/source/_includes/sig.markdown: -------------------------------------------------------------------------------- 1 | -- 2 | Tom Preston-Werner 3 | github.com/mojombo -------------------------------------------------------------------------------- /lib/jekyll/generator.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | 3 | class Generator < Plugin 4 | 5 | end 6 | 7 | end -------------------------------------------------------------------------------- /test/source/_posts/2009-05-18-tag.textile: -------------------------------------------------------------------------------- 1 | --- 2 | title: A Tag 3 | tag: code 4 | --- 5 | 6 | Whoa. 7 | -------------------------------------------------------------------------------- /test/source/about.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: About 3 | permalink: /about/ 4 | --- 5 | 6 | About the site 7 | -------------------------------------------------------------------------------- /test/source/contacts.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Contact Information 3 | --- 4 | 5 | Contact Information 6 | -------------------------------------------------------------------------------- /lib/jekyll/errors.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | 3 | class FatalException < StandardError 4 | end 5 | 6 | end -------------------------------------------------------------------------------- /test/source/_posts/2009-05-18-empty-tag.textile: -------------------------------------------------------------------------------- 1 | --- 2 | title: A Tag 3 | tag: 4 | --- 5 | 6 | Whoa. 7 | -------------------------------------------------------------------------------- /test/source/_posts/2009-05-18-empty-tags.textile: -------------------------------------------------------------------------------- 1 | --- 2 | title: Some Tags 3 | tags: 4 | --- 5 | 6 | Awesome! 7 | -------------------------------------------------------------------------------- /test/source/_posts/2010-01-08-triple-dash.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | title: Foo --- Bar 3 | --- 4 | 5 | Triple the fun! -------------------------------------------------------------------------------- /test/source/_posts/2010-01-16-override-data.textile: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2010-01-10 13:07:09 3 | tags: A string 4 | --- 5 | -------------------------------------------------------------------------------- /test/source/_posts/2009-03-12-hash-#1.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Hash #1 4 | --- 5 | 6 | Hashes are nice 7 | -------------------------------------------------------------------------------- /test/source/category/_posts/2008-9-23-categories.textile: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Categories 4 | --- 5 | 6 | Categories _should_ work -------------------------------------------------------------------------------- /test/source/_posts/2008-10-18-foo-bar.textile: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Foo Bar 4 | --- 5 | 6 | h1. {{ page.title }} 7 | 8 | Best *post* ever -------------------------------------------------------------------------------- /test/source/_posts/2008-12-13-include.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Include 4 | --- 5 | 6 | {% include sig.markdown %} 7 | 8 | This _is_ cool -------------------------------------------------------------------------------- /test/source/_posts/2009-01-27-category.textile: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Category in YAML 4 | category: foo 5 | --- 6 | 7 | Best *post* ever 8 | -------------------------------------------------------------------------------- /test/source/_posts/2009-01-27-empty-category.textile: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Category in YAML 4 | category: 5 | --- 6 | 7 | Best *post* ever 8 | -------------------------------------------------------------------------------- /test/source/_posts/2009-05-18-tags.textile: -------------------------------------------------------------------------------- 1 | --- 2 | title: Some Tags 3 | tags: 4 | - food 5 | - cooking 6 | - pizza 7 | --- 8 | 9 | Awesome! 10 | -------------------------------------------------------------------------------- /test/source/_posts/2009-01-27-empty-categories.textile: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Category in YAML 4 | categories: 5 | --- 6 | 7 | Best *post* ever 8 | -------------------------------------------------------------------------------- /test/source/win/_posts/2009-05-24-yaml-linebreak.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Test title" 4 | tag: "Ruby" 5 | --- 6 | 7 | This is the content -------------------------------------------------------------------------------- /test/source/_posts/2009-01-27-categories.textile: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Categories in YAML 4 | categories: foo bar baz 5 | --- 6 | 7 | Best *post* ever 8 | -------------------------------------------------------------------------------- /test/source/deal.with.dots.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Deal with dots 3 | permalink: /deal.with.dots/ 4 | --- 5 | 6 | Let's test if jekyll deals properly with dots. 7 | 8 | -------------------------------------------------------------------------------- /test/source/_posts/2008-02-02-published.textile: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Publish 4 | category: publish_test 5 | --- 6 | 7 | This should be published. 8 | 9 | -------------------------------------------------------------------------------- /test/source/_posts/2008-11-21-complex.textile: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Complex 4 | --- 5 | 6 | url: {{ page.url }} 7 | date: {{ page.date }} 8 | id: {{ page.id }} -------------------------------------------------------------------------------- /test/source/_posts/2010-01-09-date-override.textile: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2010-01-10 3 | --- 4 | 5 | Post with a front matter date 6 | 7 | {{ page.date | date_to_string }} 8 | -------------------------------------------------------------------------------- /test/source/_posts/2010-01-09-time-override.textile: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2010-01-10 13:07:09 3 | --- 4 | 5 | Post with a front matter time 6 | 7 | {{ page.date | date_to_string }} 8 | -------------------------------------------------------------------------------- /test/source/z_category/_posts/2008-9-23-categories.textile: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Categories 4 | --- 5 | 6 | Categories _should_ work. Even if ordered after index. -------------------------------------------------------------------------------- /test/source/foo/_posts/bar/2008-12-12-topical-post.textile: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Topical Post 4 | --- 5 | 6 | h1. {{ page.title }} 7 | 8 | This post has a topic. 9 | -------------------------------------------------------------------------------- /test/source/.htaccess: -------------------------------------------------------------------------------- 1 | --- 2 | layout: nil 3 | --- 4 | ErrorDocument 404 /404.html 5 | ErrorDocument 500 /500.html 6 | {% for post in site.posts %} 7 | # {{ post.url }} 8 | {% endfor %} -------------------------------------------------------------------------------- /test/source/_posts/2008-02-02-not-published.textile: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Not published! 4 | published: false 5 | category: publish_test 6 | --- 7 | 8 | This should *not* be published! 9 | -------------------------------------------------------------------------------- /test/source/_posts/2010-01-09-timezone-override.textile: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2010-01-10 13:07:09 +00:00 3 | --- 4 | 5 | Post with a front matter time with timezone 6 | 7 | {{ page.date | date_to_string }} 8 | -------------------------------------------------------------------------------- /test/source/_posts/2009-01-27-array-categories.textile: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Array categories in YAML 4 | categories: 5 | - foo 6 | - bar 7 | - baz 8 | --- 9 | 10 | Best *post* ever 11 | -------------------------------------------------------------------------------- /test/source/_posts/2008-12-03-permalinked-post.textile: -------------------------------------------------------------------------------- 1 | --- 2 | title: Post with Permalink 3 | permalink: my_category/permalinked-post 4 | --- 5 | 6 | h1. {{ page.title }} 7 | 8 | 9 |

Best post ever

-------------------------------------------------------------------------------- /test/suite.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | 3 | # for some reason these tests fail when run via TextMate 4 | # but succeed when run on the command line. 5 | 6 | tests = Dir["#{File.dirname(__FILE__)}/test_*.rb"] 7 | tests.each do |file| 8 | require file 9 | end -------------------------------------------------------------------------------- /lib/jekyll/converters/identity.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | 3 | class IdentityConverter < Converter 4 | safe true 5 | 6 | priority :lowest 7 | 8 | def matches(ext) 9 | true 10 | end 11 | 12 | def output_ext(ext) 13 | ext 14 | end 15 | 16 | def convert(content) 17 | content 18 | end 19 | 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'rr' 3 | require 'test/unit' 4 | 5 | World do 6 | include Test::Unit::Assertions 7 | end 8 | 9 | TEST_DIR = File.join('/', 'tmp', 'jekyll') 10 | JEKYLL_PATH = File.join(ENV['PWD'], 'bin', 'jekyll') 11 | 12 | def run_jekyll(opts = {}) 13 | command = JEKYLL_PATH 14 | command << " >> /dev/null 2>&1" if opts[:debug].nil? 15 | system command 16 | end 17 | -------------------------------------------------------------------------------- /test/source/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Tom Preston-Werner 4 | --- 5 | 6 | h1. Welcome to my site 7 | 8 | h2. Please read our {{ site.posts | size }} Posts 9 | 10 | 15 | 16 | {% assign first_post = site.posts.first %} 17 |
18 |

{{ first_post.title }}

19 |
20 | {{ first_post.content }} 21 |
22 |
23 | -------------------------------------------------------------------------------- /test/test_rdiscount.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestRdiscount < Test::Unit::TestCase 4 | 5 | context "rdiscount" do 6 | setup do 7 | config = { 8 | 'rdiscount' => { 'extensions' => ['smart'] }, 9 | 'markdown' => 'rdiscount' 10 | } 11 | @markdown = MarkdownConverter.new config 12 | end 13 | 14 | should "pass rdiscount extensions" do 15 | assert_equal "

“smart”

", @markdown.convert('"smart"').strip 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/test_kramdown.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestKramdown < Test::Unit::TestCase 4 | context "kramdown" do 5 | setup do 6 | config = { 7 | 'markdown' => 'kramdown', 8 | 'kramdown' => { 9 | 'auto_ids' => false, 10 | 'footnote_nr' => 1, 11 | 'entity_output' => 'as_char', 12 | 'toc_levels' => '1..6' 13 | } 14 | } 15 | @markdown = MarkdownConverter.new config 16 | end 17 | 18 | # http://kramdown.rubyforge.org/converter/html.html#options 19 | should "pass kramdown options" do 20 | assert_equal "

Some Header

", @markdown.convert('# Some Header #').strip 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/jekyll/migrators/csv.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /lib/jekyll/converters/textile.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | 3 | class TextileConverter < Converter 4 | safe true 5 | 6 | pygments_prefix '' 7 | pygments_suffix '' 8 | 9 | def setup 10 | return if @setup 11 | require 'redcloth' 12 | @setup = true 13 | rescue LoadError 14 | STDERR.puts 'You are missing a library required for Textile. Please run:' 15 | STDERR.puts ' $ [sudo] gem install RedCloth' 16 | raise FatalException.new("Missing dependency: RedCloth") 17 | end 18 | 19 | def matches(ext) 20 | ext =~ /textile/i 21 | end 22 | 23 | def output_ext(ext) 24 | ".html" 25 | end 26 | 27 | def convert(content) 28 | setup 29 | RedCloth.new(content).to_html 30 | end 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | gem 'RedCloth', '>= 4.2.1' 3 | 4 | require File.join(File.dirname(__FILE__), *%w[.. lib jekyll]) 5 | 6 | require 'RedCloth' 7 | require 'rdiscount' 8 | require 'kramdown' 9 | 10 | require 'test/unit' 11 | require 'redgreen' 12 | require 'shoulda' 13 | require 'rr' 14 | 15 | include Jekyll 16 | 17 | # Send STDERR into the void to suppress program output messages 18 | STDERR.reopen(test(?e, '/dev/null') ? '/dev/null' : 'NUL:') 19 | 20 | class Test::Unit::TestCase 21 | include RR::Adapters::TestUnit 22 | 23 | def dest_dir(*subdirs) 24 | File.join(File.dirname(__FILE__), 'dest', *subdirs) 25 | end 26 | 27 | def source_dir(*subdirs) 28 | File.join(File.dirname(__FILE__), 'source', *subdirs) 29 | end 30 | 31 | def clear_dest 32 | FileUtils.rm_rf(dest_dir) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/source/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | {{ page.title }} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | Tom Preston-Werner 21 |
22 | 23 | {{ content }} 24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /lib/jekyll/layout.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | 3 | class Layout 4 | include Convertible 5 | 6 | attr_accessor :site 7 | attr_accessor :ext 8 | attr_accessor :data, :content 9 | 10 | # Initialize a new Layout. 11 | # +site+ is the Site 12 | # +base+ is the String path to the 13 | # +name+ is the String filename of the post file 14 | # 15 | # Returns 16 | def initialize(site, base, name) 17 | @site = site 18 | @base = base 19 | @name = name 20 | 21 | self.data = {} 22 | 23 | self.process(name) 24 | self.read_yaml(base, name) 25 | end 26 | 27 | # Extract information from the layout filename 28 | # +name+ is the String filename of the layout file 29 | # 30 | # Returns nothing 31 | def process(name) 32 | self.ext = File.extname(name) 33 | end 34 | end 35 | 36 | end -------------------------------------------------------------------------------- /lib/jekyll/migrators/wordpress.com.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'hpricot' 3 | require 'fileutils' 4 | 5 | # This importer takes a wordpress.xml file, 6 | # which can be exported from your 7 | # wordpress.com blog (/wp-admin/export.php) 8 | 9 | module Jekyll 10 | module WordpressDotCom 11 | def self.process(filename = "wordpress.xml") 12 | FileUtils.mkdir_p "_posts" 13 | posts = 0 14 | 15 | doc = Hpricot::XML(File.read(filename)) 16 | 17 | (doc/:channel/:item).each do |item| 18 | title = item.at(:title).inner_text 19 | name = "#{Date.parse((doc/:channel/:item).first.at(:pubDate).inner_text).to_s("%Y-%m-%d")}-#{title.downcase.gsub('[^a-z0-9]', '-')}.html" 20 | 21 | File.open("_posts/#{name}", "w") do |f| 22 | f.puts <<-HEADER 23 | --- 24 | layout: post 25 | title: #{title} 26 | --- 27 | 28 | HEADER 29 | f.puts item.at('content:encoded').inner_text 30 | end 31 | 32 | posts += 1 33 | end 34 | 35 | "Imported #{posts} posts" 36 | end 37 | end 38 | end -------------------------------------------------------------------------------- /lib/jekyll/filters.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | 3 | module Jekyll 4 | 5 | module Filters 6 | def textilize(input) 7 | TextileConverter.new.convert(input) 8 | end 9 | 10 | def date_to_string(date) 11 | date.strftime("%d %b %Y") 12 | end 13 | 14 | def date_to_long_string(date) 15 | date.strftime("%d %B %Y") 16 | end 17 | 18 | def date_to_xmlschema(date) 19 | date.xmlschema 20 | end 21 | 22 | def xml_escape(input) 23 | CGI.escapeHTML(input) 24 | end 25 | 26 | def cgi_escape(input) 27 | CGI::escape(input) 28 | end 29 | 30 | def uri_escape(input) 31 | URI.escape(input) 32 | end 33 | 34 | def number_of_words(input) 35 | input.split.length 36 | end 37 | 38 | def array_to_sentence_string(array) 39 | connector = "and" 40 | case array.length 41 | when 0 42 | "" 43 | when 1 44 | array[0].to_s 45 | when 2 46 | "#{array[0]} #{connector} #{array[1]}" 47 | else 48 | "#{array[0...-1].join(', ')}, #{connector} #{array[-1]}" 49 | end 50 | end 51 | 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2008 Tom Preston-Werner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the 'Software'), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /lib/jekyll/tags/include.rb: -------------------------------------------------------------------------------- 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) 38 | -------------------------------------------------------------------------------- /test/source/sitemap.xml: -------------------------------------------------------------------------------- 1 | --- 2 | layout: nil 3 | --- 4 | 5 | 7 | 8 | 9 | http://example.com 10 | {{ site.time | date: "%Y-%m-%d" }} 11 | daily 12 | 1.0 13 | 14 | 15 | {% for post in site.posts %} 16 | 17 | http://example.com{{ post.url }}/ 18 | {{ post.date | date: "%Y-%m-%d" }} 19 | monthly 20 | 0.2 21 | 22 | {% endfor %} 23 | 24 | {% for page in site.html_pages %} 25 | 26 | http://example.com{{ page.url }} 27 | {{ site.time | date: "%Y-%m-%d" }} 28 | {% if page.changefreq %}{{ page.changefreq }}{% endif %} 29 | {% if page.priority %}{{ page.priority }}{% endif %} 30 | 31 | {% endfor %} 32 | 33 | -------------------------------------------------------------------------------- /test/test_configuration.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestConfiguration < Test::Unit::TestCase 4 | context "loading configuration" do 5 | setup do 6 | @path = File.join(Dir.pwd, '_config.yml') 7 | end 8 | 9 | should "fire warning with no _config.yml" do 10 | mock(YAML).load_file(@path) { raise "No such file or directory - #{@path}" } 11 | mock($stderr).puts("WARNING: Could not read configuration. Using defaults (and options).") 12 | mock($stderr).puts("\tNo such file or directory - #{@path}") 13 | assert_equal Jekyll::DEFAULTS, Jekyll.configuration({}) 14 | end 15 | 16 | should "load configuration as hash" do 17 | mock(YAML).load_file(@path) { Hash.new } 18 | mock($stdout).puts("Configuration from #{@path}") 19 | assert_equal Jekyll::DEFAULTS, Jekyll.configuration({}) 20 | end 21 | 22 | should "fire warning with bad config" do 23 | mock(YAML).load_file(@path) { Array.new } 24 | mock($stderr).puts("WARNING: Could not read configuration. Using defaults (and options).") 25 | mock($stderr).puts("\tInvalid configuration - #{@path}") 26 | assert_equal Jekyll::DEFAULTS, Jekyll.configuration({}) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/source/css/screen.css: -------------------------------------------------------------------------------- 1 | /*****************************************************************************/ 2 | /* 3 | /* Common 4 | /* 5 | /*****************************************************************************/ 6 | 7 | /* Global Reset */ 8 | 9 | * { 10 | margin: 0; 11 | padding: 0; 12 | } 13 | 14 | html, body { 15 | height: 100%; 16 | } 17 | 18 | body { 19 | background-color: white; 20 | font: 13.34px helvetica, arial, clean, sans-serif; 21 | *font-size: small; 22 | text-align: center; 23 | } 24 | 25 | h1, h2, h3, h4, h5, h6 { 26 | font-size: 100%; 27 | } 28 | 29 | h1 { 30 | margin-bottom: 1em; 31 | } 32 | 33 | p { 34 | margin: 1em 0; 35 | } 36 | 37 | a { 38 | color: #00a; 39 | } 40 | 41 | a:hover { 42 | color: black; 43 | } 44 | 45 | a:visited { 46 | color: #a0a; 47 | } 48 | 49 | table { 50 | font-size: inherit; 51 | font: 100%; 52 | } 53 | 54 | /*****************************************************************************/ 55 | /* 56 | /* Site 57 | /* 58 | /*****************************************************************************/ 59 | 60 | .site { 61 | font-size: 110%; 62 | text-align: justify; 63 | width: 40em; 64 | margin: 3em auto 2em auto; 65 | line-height: 1.5em; 66 | } 67 | 68 | .title { 69 | color: #a00; 70 | font-weight: bold; 71 | margin-bottom: 2em; 72 | } 73 | 74 | .site .meta { 75 | color: #aaa; 76 | } -------------------------------------------------------------------------------- /features/pagination.feature: -------------------------------------------------------------------------------- 1 | Feature: Site pagination 2 | In order to paginate my blog 3 | As a blog's user 4 | I want divide the posts in several pages 5 | 6 | Scenario Outline: Paginate with N posts per page 7 | Given I have a configuration file with "paginate" set to "" 8 | And I have a _layouts directory 9 | And I have an "index.html" page that contains "{{ paginator.posts.size }}" 10 | And I have a _posts directory 11 | And I have the following post: 12 | | title | date | layout | content | 13 | | Wargames | 3/27/2009 | default | The only winning move is not to play. | 14 | | Wargames2 | 4/27/2009 | default | The only winning move is not to play2. | 15 | | Wargames3 | 5/27/2009 | default | The only winning move is not to play3. | 16 | | Wargames4 | 6/27/2009 | default | The only winning move is not to play4. | 17 | When I run jekyll 18 | Then the _site/page directory should exist 19 | And the "_site/page/index.html" file should exist 20 | And I should see "" in "_site/page/index.html" 21 | And the "_site/page/index.html" file should not exist 22 | 23 | Examples: 24 | | num | exist | posts | not_exist | 25 | | 1 | 4 | 1 | 5 | 26 | | 2 | 2 | 2 | 3 | 27 | | 3 | 2 | 1 | 3 | 28 | -------------------------------------------------------------------------------- /lib/jekyll/core_ext.rb: -------------------------------------------------------------------------------- 1 | class Hash 2 | # Merges self with another hash, recursively. 3 | # 4 | # This code was lovingly stolen from some random gem: 5 | # http://gemjack.com/gems/tartan-0.1.1/classes/Hash.html 6 | # 7 | # Thanks to whoever made it. 8 | def deep_merge(hash) 9 | target = dup 10 | 11 | hash.keys.each do |key| 12 | if hash[key].is_a? Hash and self[key].is_a? Hash 13 | target[key] = target[key].deep_merge(hash[key]) 14 | next 15 | end 16 | 17 | target[key] = hash[key] 18 | end 19 | 20 | target 21 | end 22 | 23 | # Read array from the supplied hash favouring the singular key 24 | # and then the plural key, and handling any nil entries. 25 | # +hash+ the hash to read from 26 | # +singular_key+ the singular key 27 | # +plural_key+ the singular key 28 | # 29 | # Returns an array 30 | def pluralized_array(singular_key, plural_key) 31 | hash = self 32 | if hash.has_key?(singular_key) 33 | array = [hash[singular_key]] if hash[singular_key] 34 | elsif hash.has_key?(plural_key) 35 | case hash[plural_key] 36 | when String 37 | array = hash[plural_key].split 38 | when Array 39 | array = hash[plural_key].compact 40 | end 41 | end 42 | array || [] 43 | end 44 | end 45 | 46 | # Thanks, ActiveSupport! 47 | class Date 48 | # Converts datetime to an appropriate format for use in XML 49 | def xmlschema 50 | strftime("%Y-%m-%dT%H:%M:%S%Z") 51 | end if RUBY_VERSION < '1.9' 52 | end 53 | -------------------------------------------------------------------------------- /lib/jekyll/converter.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | 3 | class Converter < Plugin 4 | # Public: Get or set the pygments prefix. When an argument is specified, 5 | # the prefix will be set. If no argument is specified, the current prefix 6 | # will be returned. 7 | # 8 | # pygments_prefix - The String prefix (default: nil). 9 | # 10 | # Returns the String prefix. 11 | def self.pygments_prefix(pygments_prefix = nil) 12 | @pygments_prefix = pygments_prefix if pygments_prefix 13 | @pygments_prefix 14 | end 15 | 16 | # Public: Get or set the pygments suffix. When an argument is specified, 17 | # the suffix will be set. If no argument is specified, the current suffix 18 | # will be returned. 19 | # 20 | # pygments_suffix - The String suffix (default: nil). 21 | # 22 | # Returns the String suffix. 23 | def self.pygments_suffix(pygments_suffix = nil) 24 | @pygments_suffix = pygments_suffix if pygments_suffix 25 | @pygments_suffix 26 | end 27 | 28 | # Initialize the converter. 29 | # 30 | # Returns an initialized Converter. 31 | def initialize(config = {}) 32 | @config = config 33 | end 34 | 35 | # Get the pygments prefix. 36 | # 37 | # Returns the String prefix. 38 | def pygments_prefix 39 | self.class.pygments_prefix 40 | end 41 | 42 | # Get the pygments suffix. 43 | # 44 | # Returns the String suffix. 45 | def pygments_suffix 46 | self.class.pygments_suffix 47 | end 48 | end 49 | 50 | end -------------------------------------------------------------------------------- /features/markdown.feature: -------------------------------------------------------------------------------- 1 | Feature: Markdown 2 | As a hacker who likes to blog 3 | I want to be able to make a static site 4 | In order to share my awesome ideas with the interwebs 5 | 6 | Scenario: Markdown in list on index 7 | Given I have a configuration file with "paginate" set to "5" 8 | And I have an "index.html" page that contains "Index - {% for post in site.posts %} {{ post.content }} {% endfor %}" 9 | And I have a _posts directory 10 | And I have the following post: 11 | | title | date | content | type | 12 | | Hackers | 3/27/2009 | # My Title | markdown | 13 | When I run jekyll 14 | Then the _site directory should exist 15 | And I should see "Index" in "_site/index.html" 16 | And I should see "

My Title

" in "_site/2009/03/27/hackers.html" 17 | And I should see "

My Title

" in "_site/index.html" 18 | 19 | Scenario: Markdown in pagination on index 20 | Given I have a configuration file with "paginate" set to "5" 21 | And I have an "index.html" page that contains "Index - {% for post in paginator.posts %} {{ post.content }} {% endfor %}" 22 | And I have a _posts directory 23 | And I have the following post: 24 | | title | date | content | type | 25 | | Hackers | 3/27/2009 | # My Title | markdown | 26 | When I run jekyll 27 | Then the _site directory should exist 28 | And I should see "Index" in "_site/index.html" 29 | And I should see "

My Title

" in "_site/index.html" 30 | -------------------------------------------------------------------------------- /lib/jekyll/migrators/typo.rb: -------------------------------------------------------------------------------- 1 | # Author: Toby DiPasquale 2 | require 'fileutils' 3 | require 'rubygems' 4 | require 'sequel' 5 | 6 | module Jekyll 7 | module Typo 8 | # this SQL *should* work for both MySQL and PostgreSQL, but I haven't 9 | # tested PostgreSQL yet (as of 2008-12-16) 10 | SQL = <<-EOS 11 | SELECT c.id id, 12 | c.title title, 13 | c.permalink slug, 14 | c.body body, 15 | c.published_at date, 16 | c.state state, 17 | COALESCE(tf.name, 'html') filter 18 | FROM contents c 19 | LEFT OUTER JOIN text_filters tf 20 | ON c.text_filter_id = tf.id 21 | EOS 22 | 23 | def self.process dbname, user, pass, host='localhost' 24 | FileUtils.mkdir_p '_posts' 25 | db = Sequel.mysql(dbname, :user => user, :password => pass, :host => host, :encoding => 'utf8') 26 | db[SQL].each do |post| 27 | next unless post[:state] =~ /Published/ 28 | 29 | name = [ sprintf("%.04d", post[:date].year), 30 | sprintf("%.02d", post[:date].month), 31 | sprintf("%.02d", post[:date].day), 32 | post[:slug].strip ].join('-') 33 | # Can have more than one text filter in this field, but we just want 34 | # the first one for this 35 | name += '.' + post[:filter].split(' ')[0] 36 | 37 | File.open("_posts/#{name}", 'w') do |f| 38 | f.puts({ 'layout' => 'post', 39 | 'title' => post[:title].to_s, 40 | 'typo_id' => post[:id] 41 | }.delete_if { |k, v| v.nil? || v == '' }.to_yaml) 42 | f.puts '---' 43 | f.puts post[:body].delete("\r") 44 | end 45 | end 46 | end 47 | 48 | end # module Typo 49 | end # module Jekyll 50 | -------------------------------------------------------------------------------- /lib/jekyll/migrators/textpattern.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'sequel' 3 | require 'fileutils' 4 | 5 | # NOTE: This converter requires Sequel and the MySQL gems. 6 | # The MySQL gem can be difficult to install on OS X. Once you have MySQL 7 | # installed, running the following commands should work: 8 | # $ sudo gem install sequel 9 | # $ sudo gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config 10 | 11 | module Jekyll 12 | module TextPattern 13 | # Reads a MySQL database via Sequel and creates a post file for each post. 14 | # The only posts selected are those with a status of 4 or 5, which means "live" 15 | # and "sticky" respectively. 16 | # Other statuses is 1 => draft, 2 => hidden and 3 => pending 17 | QUERY = "select Title, url_title, Posted, Body, Keywords from textpattern where Status = '4' or Status = '5'" 18 | 19 | def self.process(dbname, user, pass, host = 'localhost') 20 | db = Sequel.mysql(dbname, :user => user, :password => pass, :host => host, :encoding => 'utf8') 21 | 22 | FileUtils.mkdir_p "_posts" 23 | 24 | db[QUERY].each do |post| 25 | # Get required fields and construct Jekyll compatible name 26 | title = post[:Title] 27 | slug = post[:url_title] 28 | date = post[:Posted] 29 | content = post[:Body] 30 | 31 | name = [date.strftime("%Y-%m-%d"), slug].join('-') + ".textile" 32 | 33 | # Get the relevant fields as a hash, delete empty fields and convert 34 | # to YAML for the header 35 | data = { 36 | 'layout' => 'post', 37 | 'title' => title.to_s, 38 | 'tags' => post[:Keywords].split(',') 39 | }.delete_if { |k,v| v.nil? || v == ''}.to_yaml 40 | 41 | # Write out the data and content to file 42 | File.open("_posts/#{name}", "w") do |f| 43 | f.puts data 44 | f.puts "---" 45 | f.puts content 46 | end 47 | end 48 | end 49 | end 50 | end -------------------------------------------------------------------------------- /lib/jekyll/migrators/marley.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require 'fileutils' 3 | 4 | module Jekyll 5 | module Marley 6 | 7 | def self.regexp 8 | { :id => /^\d{0,4}-{0,1}(.*)$/, 9 | :title => /^#\s*(.*)\s+$/, 10 | :title_with_date => /^#\s*(.*)\s+\(([0-9\/]+)\)$/, 11 | :published_on => /.*\s+\(([0-9\/]+)\)$/, 12 | :perex => /^([^\#\n]+\n)$/, 13 | :meta => /^\{\{\n(.*)\}\}\n$/mi # Multiline Regexp 14 | } 15 | end 16 | 17 | def self.process(marley_data_dir) 18 | raise ArgumentError, "marley dir #{marley_data_dir} not found" unless File.directory?(marley_data_dir) 19 | 20 | FileUtils.mkdir_p "_posts" 21 | 22 | posts = 0 23 | Dir["#{marley_data_dir}/**/*.txt"].each do |f| 24 | next unless File.exists?(f) 25 | 26 | #copied over from marley's app/lib/post.rb 27 | file_content = File.read(f) 28 | meta_content = file_content.slice!( self.regexp[:meta] ) 29 | body = file_content.sub( self.regexp[:title], '').sub( self.regexp[:perex], '').strip 30 | 31 | title = file_content.scan( self.regexp[:title] ).first.to_s.strip 32 | prerex = file_content.scan( self.regexp[:perex] ).first.to_s.strip 33 | published_on = DateTime.parse( post[:published_on] ) rescue File.mtime( File.dirname(f) ) 34 | meta = ( meta_content ) ? YAML::load( meta_content.scan( self.regexp[:meta]).to_s ) : {} 35 | meta['title'] = title 36 | meta['layout'] = 'post' 37 | 38 | formatted_date = published_on.strftime('%Y-%m-%d') 39 | post_name = File.dirname(f).split(%r{/}).last.gsub(/\A\d+-/, '') 40 | 41 | name = "#{formatted_date}-#{post_name}" 42 | File.open("_posts/#{name}.markdown", "w") do |f| 43 | f.puts meta.to_yaml 44 | f.puts "---\n" 45 | f.puts "\n#{prerex}\n\n" if prerex 46 | f.puts body 47 | end 48 | posts += 1 49 | end 50 | "Created #{posts} posts!" 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/test_filters.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestFilters < Test::Unit::TestCase 4 | class JekyllFilter 5 | include Jekyll::Filters 6 | end 7 | 8 | context "filters" do 9 | setup do 10 | @filter = JekyllFilter.new 11 | end 12 | 13 | should "textilize with simple string" do 14 | assert_equal "

something really simple

", @filter.textilize("something *really* simple") 15 | end 16 | 17 | should "convert array to sentence string with no args" do 18 | assert_equal "", @filter.array_to_sentence_string([]) 19 | end 20 | 21 | should "convert array to sentence string with one arg" do 22 | assert_equal "1", @filter.array_to_sentence_string([1]) 23 | assert_equal "chunky", @filter.array_to_sentence_string(["chunky"]) 24 | end 25 | 26 | should "convert array to sentence string with two args" do 27 | assert_equal "1 and 2", @filter.array_to_sentence_string([1, 2]) 28 | assert_equal "chunky and bacon", @filter.array_to_sentence_string(["chunky", "bacon"]) 29 | end 30 | 31 | should "convert array to sentence string with multiple args" do 32 | assert_equal "1, 2, 3, and 4", @filter.array_to_sentence_string([1, 2, 3, 4]) 33 | assert_equal "chunky, bacon, bits, and pieces", @filter.array_to_sentence_string(["chunky", "bacon", "bits", "pieces"]) 34 | end 35 | 36 | should "escape xml with ampersands" do 37 | assert_equal "AT&T", @filter.xml_escape("AT&T") 38 | assert_equal "<code>command &lt;filename&gt;</code>", @filter.xml_escape("command <filename>") 39 | end 40 | 41 | should "escape space as plus" do 42 | assert_equal "my+things", @filter.cgi_escape("my things") 43 | end 44 | 45 | should "escape special characters" do 46 | assert_equal "hey%21", @filter.cgi_escape("hey!") 47 | end 48 | 49 | should "escape space as %20" do 50 | assert_equal "my%20things", @filter.uri_escape("my things") 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/jekyll/static_file.rb: -------------------------------------------------------------------------------- 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 9 | # +dir+ is the String path between and the file 10 | # +name+ is the String filename of the file 11 | # 12 | # Returns 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 76 | -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | h1. Jekyll 2 | 3 | By Tom Preston-Werner, Nick Quaranto, and many awesome contributors! 4 | 5 | Jekyll is a simple, blog aware, static site generator. It takes a template directory (representing the raw form of a website), runs it through Textile or Markdown and Liquid converters, and spits out a complete, static website suitable for serving with Apache or your favorite web server. This is also the engine behind "GitHub Pages":http://pages.github.com, which you can use to host your project's page or blog right here from GitHub. 6 | 7 | h2. Getting Started 8 | 9 | * "Install":http://wiki.github.com/mojombo/jekyll/install the gem 10 | * Read up about its "Usage":http://wiki.github.com/mojombo/jekyll/usage and "Configuration":http://wiki.github.com/mojombo/jekyll/configuration 11 | * Take a gander at some existing "Sites":http://wiki.github.com/mojombo/jekyll/sites 12 | * Fork and "Contribute":http://wiki.github.com/mojombo/jekyll/contribute your own modifications 13 | * Have questions? Post them on the "Mailing List":http://groups.google.com/group/jekyll-rb 14 | 15 | h2. Diving In 16 | 17 | * "Migrate":http://wiki.github.com/mojombo/jekyll/blog-migrations from your previous system 18 | * Learn how the "YAML Front Matter":http://wiki.github.com/mojombo/jekyll/yaml-front-matter works 19 | * Put information on your site with "Template Data":http://wiki.github.com/mojombo/jekyll/template-data 20 | * Customize the "Permalinks":http://wiki.github.com/mojombo/jekyll/permalinks your posts are generated with 21 | * Use the built-in "Liquid Extensions":http://wiki.github.com/mojombo/jekyll/liquid-extensions to make your life easier 22 | 23 | h2. Runtime Dependencies 24 | 25 | * RedCloth: Textile support (Ruby) 26 | * Liquid: Templating system (Ruby) 27 | * Classifier: Generating related posts (Ruby) 28 | * Maruku: Default markdown engine (Ruby) 29 | * Directory Watcher: Auto-regeneration of sites (Ruby) 30 | * Open4: Talking to pygments for syntax highlighting (Ruby) 31 | * Pygments: Syntax highlighting (Python) 32 | 33 | h2. Developer Dependencies 34 | 35 | * Shoulda: Test framework (Ruby) 36 | * RR: Mocking (Ruby) 37 | * RedGreen: Nicer test output (Ruby) 38 | 39 | h2. License 40 | 41 | See LICENSE. 42 | -------------------------------------------------------------------------------- /lib/jekyll/migrators/wordpress.rb: -------------------------------------------------------------------------------- 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 WordPress 14 | 15 | # Reads a MySQL database via Sequel and creates a post file for each 16 | # post in wp_posts that has post_status = 'publish'. 17 | # This restriction is made because 'draft' posts are not guaranteed to 18 | # have valid dates. 19 | QUERY = "select post_title, post_name, post_date, post_content, post_excerpt, ID, guid from wp_posts where post_status = 'publish' and post_type = 'post'" 20 | 21 | def self.process(dbname, user, pass, host = 'localhost') 22 | db = Sequel.mysql(dbname, :user => user, :password => pass, :host => host, :encoding => 'utf8') 23 | 24 | FileUtils.mkdir_p "_posts" 25 | 26 | db[QUERY].each do |post| 27 | # Get required fields and construct Jekyll compatible name 28 | title = post[:post_title] 29 | slug = post[:post_name] 30 | date = post[:post_date] 31 | content = post[:post_content] 32 | name = "%02d-%02d-%02d-%s.markdown" % [date.year, date.month, date.day, 33 | slug] 34 | 35 | # Get the relevant fields as a hash, delete empty fields and convert 36 | # to YAML for the header 37 | data = { 38 | 'layout' => 'post', 39 | 'title' => title.to_s, 40 | 'excerpt' => post[:post_excerpt].to_s, 41 | 'wordpress_id' => post[:ID], 42 | 'wordpress_url' => post[:guid], 43 | 'date' => date 44 | }.delete_if { |k,v| v.nil? || v == ''}.to_yaml 45 | 46 | # Write out the data and content to file 47 | File.open("_posts/#{name}", "w") do |f| 48 | f.puts data 49 | f.puts "---" 50 | f.puts content 51 | end 52 | end 53 | 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/jekyll/plugin.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | 3 | class Plugin 4 | PRIORITIES = { :lowest => -100, 5 | :low => -10, 6 | :normal => 0, 7 | :high => 10, 8 | :highest => 100 } 9 | 10 | # Install a hook so that subclasses are recorded. This method is only 11 | # ever called by Ruby itself. 12 | # 13 | # base - The Class subclass. 14 | # 15 | # Returns nothing. 16 | def self.inherited(base) 17 | subclasses << base 18 | subclasses.sort! 19 | end 20 | 21 | # The list of Classes that have been subclassed. 22 | # 23 | # Returns an Array of Class objects. 24 | def self.subclasses 25 | @subclasses ||= [] 26 | end 27 | 28 | # Get or set the priority of this plugin. When called without an 29 | # argument it returns the priority. When an argument is given, it will 30 | # set the priority. 31 | # 32 | # priority - The Symbol priority (default: nil). Valid options are: 33 | # :lowest, :low, :normal, :high, :highest 34 | # 35 | # Returns the Symbol priority. 36 | def self.priority(priority = nil) 37 | if priority && PRIORITIES.has_key?(priority) 38 | @priority = priority 39 | end 40 | @priority || :normal 41 | end 42 | 43 | # Get or set the safety of this plugin. When called without an argument 44 | # it returns the safety. When an argument is given, it will set the 45 | # safety. 46 | # 47 | # safe - The Boolean safety (default: nil). 48 | # 49 | # Returns the safety Boolean. 50 | def self.safe(safe = nil) 51 | if safe 52 | @safe = safe 53 | end 54 | @safe || false 55 | end 56 | 57 | # Spaceship is priority [higher -> lower] 58 | # 59 | # other - The class to be compared. 60 | # 61 | # Returns -1, 0, 1. 62 | def self.<=>(other) 63 | PRIORITIES[other.priority] <=> PRIORITIES[self.priority] 64 | end 65 | 66 | # Initialize a new plugin. This should be overridden by the subclass. 67 | # 68 | # config - The Hash of configuration options. 69 | # 70 | # Returns a new instance. 71 | def initialize(config = {}) 72 | # no-op for default 73 | end 74 | end 75 | 76 | end 77 | -------------------------------------------------------------------------------- /lib/jekyll/tags/highlight.rb: -------------------------------------------------------------------------------- 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 [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 |
56 |
57 |     #{h(code).strip}
58 |   
59 |
60 | HTML 61 | end 62 | 63 | def add_code_tags(code, lang) 64 | # Add nested tags to code blocks 65 | code = code.sub(/
/,'
')
66 |       code = code.sub(/<\/pre>/,"
") 67 | end 68 | 69 | end 70 | 71 | end 72 | 73 | Liquid::Template.register_tag('highlight', Jekyll::HighlightBlock) 74 | -------------------------------------------------------------------------------- /test/test_generated_site.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestGeneratedSite < Test::Unit::TestCase 4 | context "generated sites" do 5 | setup do 6 | clear_dest 7 | stub(Jekyll).configuration do 8 | Jekyll::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir}) 9 | end 10 | 11 | @site = Site.new(Jekyll.configuration) 12 | @site.process 13 | @index = File.read(dest_dir('index.html')) 14 | end 15 | 16 | should "ensure post count is as expected" do 17 | assert_equal 26, @site.posts.size 18 | end 19 | 20 | should "insert site.posts into the index" do 21 | assert @index.include?("#{@site.posts.size} Posts") 22 | end 23 | 24 | should "render latest post's content" do 25 | assert @index.include?(@site.posts.last.content) 26 | end 27 | 28 | should "hide unpublished posts" do 29 | published = Dir[dest_dir('publish_test/2008/02/02/*.html')].map {|f| File.basename(f)} 30 | 31 | assert_equal 1, published.size 32 | assert_equal "published.html", published.first 33 | end 34 | 35 | should "not copy _posts directory" do 36 | assert !File.exist?(dest_dir('_posts')) 37 | end 38 | 39 | should "process other static files and generate correct permalinks" do 40 | assert File.exists?(dest_dir('/about/index.html')) 41 | assert File.exists?(dest_dir('/contacts.html')) 42 | end 43 | end 44 | 45 | context "generating limited posts" do 46 | setup do 47 | clear_dest 48 | stub(Jekyll).configuration do 49 | Jekyll::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir, 'limit_posts' => 5}) 50 | end 51 | 52 | @site = Site.new(Jekyll.configuration) 53 | @site.process 54 | @index = File.read(dest_dir('index.html')) 55 | end 56 | 57 | should "generate only the specified number of posts" do 58 | assert_equal 5, @site.posts.size 59 | end 60 | 61 | should "ensure limit posts is 1 or more" do 62 | assert_raise ArgumentError do 63 | clear_dest 64 | stub(Jekyll).configuration do 65 | Jekyll::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir, 'limit_posts' => 0}) 66 | end 67 | 68 | @site = Site.new(Jekyll.configuration) 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /test/test_core_ext.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestCoreExt < Test::Unit::TestCase 4 | context "hash" do 5 | 6 | context "pluralized_array" do 7 | 8 | should "return empty array with no values" do 9 | data = {} 10 | assert_equal [], data.pluralized_array('tag', 'tags') 11 | end 12 | 13 | should "return empty array with no matching values" do 14 | data = { 'foo' => 'bar' } 15 | assert_equal [], data.pluralized_array('tag', 'tags') 16 | end 17 | 18 | should "return empty array with matching nil singular" do 19 | data = { 'foo' => 'bar', 'tag' => nil, 'tags' => ['dog', 'cat'] } 20 | assert_equal [], data.pluralized_array('tag', 'tags') 21 | end 22 | 23 | should "return single value array with matching singular" do 24 | data = { 'foo' => 'bar', 'tag' => 'dog', 'tags' => ['dog', 'cat'] } 25 | assert_equal ['dog'], data.pluralized_array('tag', 'tags') 26 | end 27 | 28 | should "return single value array with matching singular with spaces" do 29 | data = { 'foo' => 'bar', 'tag' => 'dog cat', 'tags' => ['dog', 'cat'] } 30 | assert_equal ['dog cat'], data.pluralized_array('tag', 'tags') 31 | end 32 | 33 | should "return empty array with matching nil plural" do 34 | data = { 'foo' => 'bar', 'tags' => nil } 35 | assert_equal [], data.pluralized_array('tag', 'tags') 36 | end 37 | 38 | should "return empty array with matching empty array" do 39 | data = { 'foo' => 'bar', 'tags' => [] } 40 | assert_equal [], data.pluralized_array('tag', 'tags') 41 | end 42 | 43 | should "return single value array with matching plural with single string value" do 44 | data = { 'foo' => 'bar', 'tags' => 'dog' } 45 | assert_equal ['dog'], data.pluralized_array('tag', 'tags') 46 | end 47 | 48 | should "return multiple value array with matching plural with single string value with spaces" do 49 | data = { 'foo' => 'bar', 'tags' => 'dog cat' } 50 | assert_equal ['dog', 'cat'], data.pluralized_array('tag', 'tags') 51 | end 52 | 53 | should "return single value array with matching plural with single value array" do 54 | data = { 'foo' => 'bar', 'tags' => ['dog'] } 55 | assert_equal ['dog'], data.pluralized_array('tag', 'tags') 56 | end 57 | 58 | should "return multiple value array with matching plural with multiple value array" do 59 | data = { 'foo' => 'bar', 'tags' => ['dog', 'cat'] } 60 | assert_equal ['dog', 'cat'], data.pluralized_array('tag', 'tags') 61 | end 62 | 63 | end 64 | 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/jekyll/generators/pagination.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | 3 | class Pagination < Generator 4 | safe true 5 | 6 | def generate(site) 7 | site.pages.dup.each do |page| 8 | paginate(site, page) if Pager.pagination_enabled?(site.config, page.name) 9 | end 10 | end 11 | 12 | # Paginates the blog's posts. Renders the index.html file into paginated 13 | # directories, ie: page2/index.html, page3/index.html, etc and adds more 14 | # site-wide data. 15 | # +page+ is the index.html Page that requires pagination 16 | # 17 | # {"paginator" => { "page" => , 18 | # "per_page" => , 19 | # "posts" => [], 20 | # "total_posts" => , 21 | # "total_pages" => , 22 | # "previous_page" => , 23 | # "next_page" => }} 24 | def paginate(site, page) 25 | all_posts = site.site_payload['site']['posts'] 26 | pages = Pager.calculate_pages(all_posts, site.config['paginate'].to_i) 27 | (1..pages).each do |num_page| 28 | pager = Pager.new(site.config, num_page, all_posts, pages) 29 | if num_page > 1 30 | newpage = Page.new(site, site.source, page.dir, page.name) 31 | newpage.pager = pager 32 | newpage.dir = File.join(page.dir, "page#{num_page}") 33 | site.pages << newpage 34 | else 35 | page.pager = pager 36 | end 37 | end 38 | end 39 | 40 | end 41 | 42 | class Pager 43 | attr_reader :page, :per_page, :posts, :total_posts, :total_pages, :previous_page, :next_page 44 | 45 | def self.calculate_pages(all_posts, per_page) 46 | num_pages = all_posts.size / per_page.to_i 47 | num_pages = num_pages + 1 if all_posts.size % per_page.to_i != 0 48 | num_pages 49 | end 50 | 51 | def self.pagination_enabled?(config, file) 52 | file == 'index.html' && !config['paginate'].nil? 53 | end 54 | 55 | def initialize(config, page, all_posts, num_pages = nil) 56 | @page = page 57 | @per_page = config['paginate'].to_i 58 | @total_pages = num_pages || Pager.calculate_pages(all_posts, @per_page) 59 | 60 | if @page > @total_pages 61 | raise RuntimeError, "page number can't be greater than total pages: #{@page} > #{@total_pages}" 62 | end 63 | 64 | init = (@page - 1) * @per_page 65 | offset = (init + @per_page - 1) >= all_posts.size ? all_posts.size : (init + @per_page - 1) 66 | 67 | @total_posts = all_posts.size 68 | @posts = all_posts[init..offset] 69 | @previous_page = @page != 1 ? @page - 1 : nil 70 | @next_page = @page != @total_pages ? @page + 1 : nil 71 | end 72 | 73 | def to_liquid 74 | { 75 | 'page' => page, 76 | 'per_page' => per_page, 77 | 'posts' => posts, 78 | 'total_posts' => total_posts, 79 | 'total_pages' => total_pages, 80 | 'previous_page' => previous_page, 81 | 'next_page' => next_page 82 | } 83 | end 84 | end 85 | 86 | 87 | end 88 | -------------------------------------------------------------------------------- /lib/jekyll/convertible.rb: -------------------------------------------------------------------------------- 1 | # Convertible provides methods for converting a pagelike item 2 | # from a certain type of markup into actual content 3 | # 4 | # Requires 5 | # self.site -> Jekyll::Site 6 | # self.content 7 | # self.content= 8 | # self.data= 9 | # self.ext= 10 | # self.output= 11 | module Jekyll 12 | module Convertible 13 | # Return the contents as a string 14 | def to_s 15 | self.content || '' 16 | end 17 | 18 | # Read the YAML frontmatter 19 | # +base+ is the String path to the dir containing the file 20 | # +name+ is the String filename of the file 21 | # 22 | # Returns nothing 23 | def read_yaml(base, name) 24 | self.content = File.read(File.join(base, name)) 25 | 26 | if self.content =~ /^(---\s*\n.*?\n?)^(---\s*$\n?)/m 27 | self.content = self.content[($1.size + $2.size)..-1] 28 | 29 | begin 30 | self.data = YAML.load($1) 31 | rescue => e 32 | puts "YAML Exception: #{e.message}" 33 | end 34 | end 35 | 36 | self.data ||= {} 37 | end 38 | 39 | # Transform the contents based on the content type. 40 | # 41 | # Returns nothing 42 | def transform 43 | self.content = converter.convert(self.content) 44 | end 45 | 46 | # Determine the extension depending on content_type 47 | # 48 | # Returns the extensions for the output file 49 | def output_ext 50 | converter.output_ext(self.ext) 51 | end 52 | 53 | # Determine which converter to use based on this convertible's 54 | # extension 55 | def converter 56 | @converter ||= self.site.converters.find { |c| c.matches(self.ext) } 57 | end 58 | 59 | # Add any necessary layouts to this convertible document 60 | # +layouts+ is a Hash of {"name" => "layout"} 61 | # +site_payload+ is the site payload hash 62 | # 63 | # Returns nothing 64 | def do_layout(payload, layouts) 65 | info = { :filters => [Jekyll::Filters], :registers => { :site => self.site } } 66 | 67 | # render and transform content (this becomes the final content of the object) 68 | payload["pygments_prefix"] = converter.pygments_prefix 69 | payload["pygments_suffix"] = converter.pygments_suffix 70 | 71 | begin 72 | self.content = Liquid::Template.parse(self.content).render(payload, info) 73 | rescue => e 74 | puts "Liquid Exception: #{e.message} in #{self.data["layout"]}" 75 | end 76 | 77 | self.transform 78 | 79 | # output keeps track of what will finally be written 80 | self.output = self.content 81 | 82 | # recursively render layouts 83 | layout = layouts[self.data["layout"]] 84 | while layout 85 | payload = payload.deep_merge({"content" => self.output, "page" => layout.data}) 86 | 87 | begin 88 | self.output = Liquid::Template.parse(layout.content).render(payload, info) 89 | rescue => e 90 | puts "Liquid Exception: #{e.message} in #{self.data["layout"]}" 91 | end 92 | 93 | layout = layouts[layout.data["layout"]] 94 | end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /features/embed_filters.feature: -------------------------------------------------------------------------------- 1 | Feature: Embed filters 2 | As a hacker who likes to blog 3 | I want to be able to transform text inside a post or page 4 | In order to perform cool stuff in my posts 5 | 6 | Scenario: Convert date to XML schema 7 | Given I have a _posts directory 8 | And I have a _layouts directory 9 | And I have the following post: 10 | | title | date | layout | content | 11 | | Star Wars | 3/27/2009 | default | These aren't the droids you're looking for. | 12 | And I have a default layout that contains "{{ site.time | date_to_xmlschema }}" 13 | When I run jekyll 14 | Then the _site directory should exist 15 | And I should see today's date in "_site/2009/03/27/star-wars.html" 16 | 17 | Scenario: Escape text for XML 18 | Given I have a _posts directory 19 | And I have a _layouts directory 20 | And I have the following post: 21 | | title | date | layout | content | 22 | | Star & Wars | 3/27/2009 | default | These aren't the droids you're looking for. | 23 | And I have a default layout that contains "{{ page.title | xml_escape }}" 24 | When I run jekyll 25 | Then the _site directory should exist 26 | And I should see "Star & Wars" in "_site/2009/03/27/star-wars.html" 27 | 28 | Scenario: Calculate number of words 29 | Given I have a _posts directory 30 | And I have a _layouts directory 31 | And I have the following post: 32 | | title | date | layout | content | 33 | | Star Wars | 3/27/2009 | default | These aren't the droids you're looking for. | 34 | And I have a default layout that contains "{{ content | xml_escape }}" 35 | When I run jekyll 36 | Then the _site directory should exist 37 | And I should see "7" in "_site/2009/03/27/star-wars.html" 38 | 39 | Scenario: Convert an array into a sentence 40 | Given I have a _posts directory 41 | And I have a _layouts directory 42 | And I have the following post: 43 | | title | date | layout | tags | content | 44 | | Star Wars | 3/27/2009 | default | [scifi, movies, force] | These aren't the droids you're looking for. | 45 | And I have a default layout that contains "{{ page.tags | array_to_sentence_string }}" 46 | When I run jekyll 47 | Then the _site directory should exist 48 | And I should see "scifi, movies, and force" in "_site/2009/03/27/star-wars.html" 49 | 50 | Scenario: Textilize a given string 51 | Given I have a _posts directory 52 | And I have a _layouts directory 53 | And I have the following post: 54 | | title | date | layout | content | 55 | | Star Wars | 3/27/2009 | default | These aren't the droids you're looking for. | 56 | And I have a default layout that contains "By {{ '_Obi-wan_' | textilize }}" 57 | When I run jekyll 58 | Then the _site directory should exist 59 | And I should see "By

Obi-wan

" in "_site/2009/03/27/star-wars.html" 60 | 61 | -------------------------------------------------------------------------------- /lib/jekyll/migrators/mephisto.rb: -------------------------------------------------------------------------------- 1 | # Quickly hacked together my Michael Ivey 2 | # Based on mt.rb by Nick Gerakines, open source and publically 3 | # available under the MIT license. Use this module at your own risk. 4 | 5 | require 'rubygems' 6 | require 'sequel' 7 | require 'fastercsv' 8 | require 'fileutils' 9 | require File.join(File.dirname(__FILE__),"csv.rb") 10 | 11 | # NOTE: This converter requires Sequel and the MySQL gems. 12 | # The MySQL gem can be difficult to install on OS X. Once you have MySQL 13 | # installed, running the following commands should work: 14 | # $ sudo gem install sequel 15 | # $ sudo gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config 16 | 17 | module Jekyll 18 | module Mephisto 19 | #Accepts a hash with database config variables, exports mephisto posts into a csv 20 | #export PGPASSWORD if you must 21 | def self.postgres(c) 22 | sql = <<-SQL 23 | BEGIN; 24 | CREATE TEMP TABLE jekyll AS 25 | SELECT title, permalink, body, published_at, filter FROM contents 26 | WHERE user_id = 1 AND type = 'Article' ORDER BY published_at; 27 | COPY jekyll TO STDOUT WITH CSV HEADER; 28 | ROLLBACK; 29 | SQL 30 | command = %Q(psql -h #{c[:host] || "localhost"} -c "#{sql.strip}" #{c[:database]} #{c[:username]} -o #{c[:filename] || "posts.csv"}) 31 | puts command 32 | `#{command}` 33 | CSV.process 34 | end 35 | 36 | # This query will pull blog posts from all entries across all blogs. If 37 | # you've got unpublished, deleted or otherwise hidden posts please sift 38 | # through the created posts to make sure nothing is accidently published. 39 | 40 | QUERY = "SELECT id, permalink, body, published_at, title FROM contents WHERE user_id = 1 AND type = 'Article' AND published_at IS NOT NULL ORDER BY published_at" 41 | 42 | def self.process(dbname, user, pass, host = 'localhost') 43 | db = Sequel.mysql(dbname, :user => user, :password => pass, :host => host, :encoding => 'utf8') 44 | 45 | FileUtils.mkdir_p "_posts" 46 | 47 | db[QUERY].each do |post| 48 | title = post[:title] 49 | slug = post[:permalink] 50 | date = post[:published_at] 51 | content = post[:body] 52 | # more_content = '' 53 | 54 | # Be sure to include the body and extended body. 55 | # if more_content != nil 56 | # content = content + " \n" + more_content 57 | # end 58 | 59 | # Ideally, this script would determine the post format (markdown, html 60 | # , etc) and create files with proper extensions. At this point it 61 | # just assumes that markdown will be acceptable. 62 | name = [date.year, date.month, date.day, slug].join('-') + ".markdown" 63 | 64 | data = { 65 | 'layout' => 'post', 66 | 'title' => title.to_s, 67 | 'mt_id' => post[:entry_id], 68 | }.delete_if { |k,v| v.nil? || v == ''}.to_yaml 69 | 70 | File.open("_posts/#{name}", "w") do |f| 71 | f.puts data 72 | f.puts "---" 73 | f.puts content 74 | end 75 | end 76 | 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/jekyll/migrators/mt.rb: -------------------------------------------------------------------------------- 1 | # Created by Nick Gerakines, open source and publically available under the 2 | # MIT license. Use this module at your own risk. 3 | # I'm an Erlang/Perl/C++ guy so please forgive my dirty ruby. 4 | 5 | require 'rubygems' 6 | require 'sequel' 7 | require 'fileutils' 8 | require 'yaml' 9 | 10 | # NOTE: This converter requires Sequel and the MySQL gems. 11 | # The MySQL gem can be difficult to install on OS X. Once you have MySQL 12 | # installed, running the following commands should work: 13 | # $ sudo gem install sequel 14 | # $ sudo gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config 15 | 16 | module Jekyll 17 | module MT 18 | # This query will pull blog posts from all entries across all blogs. If 19 | # you've got unpublished, deleted or otherwise hidden posts please sift 20 | # through the created posts to make sure nothing is accidently published. 21 | QUERY = "SELECT entry_id, entry_basename, entry_text, entry_text_more, entry_authored_on, entry_title, entry_convert_breaks FROM mt_entry" 22 | 23 | def self.process(dbname, user, pass, host = 'localhost') 24 | db = Sequel.mysql(dbname, :user => user, :password => pass, :host => host, :encoding => 'utf8') 25 | 26 | FileUtils.mkdir_p "_posts" 27 | 28 | db[QUERY].each do |post| 29 | title = post[:entry_title] 30 | slug = post[:entry_basename].gsub(/_/, '-') 31 | date = post[:entry_authored_on] 32 | content = post[:entry_text] 33 | more_content = post[:entry_text_more] 34 | entry_convert_breaks = post[:entry_convert_breaks] 35 | 36 | # Be sure to include the body and extended body. 37 | if more_content != nil 38 | content = content + " \n" + more_content 39 | end 40 | 41 | # Ideally, this script would determine the post format (markdown, html 42 | # , etc) and create files with proper extensions. At this point it 43 | # just assumes that markdown will be acceptable. 44 | name = [date.year, date.month, date.day, slug].join('-') + '.' + self.suffix(entry_convert_breaks) 45 | 46 | data = { 47 | 'layout' => 'post', 48 | 'title' => title.to_s, 49 | 'mt_id' => post[:entry_id], 50 | 'date' => date 51 | }.delete_if { |k,v| v.nil? || v == ''}.to_yaml 52 | 53 | File.open("_posts/#{name}", "w") do |f| 54 | f.puts data 55 | f.puts "---" 56 | f.puts content 57 | end 58 | end 59 | end 60 | 61 | def self.suffix(entry_type) 62 | if entry_type.nil? || entry_type.include?("markdown") 63 | # The markdown plugin I have saves this as "markdown_with_smarty_pants", so I just look for "markdown". 64 | "markdown" 65 | elsif entry_type.include?("textile") 66 | # This is saved as "textile_2" on my installation of MT 5.1. 67 | "textile" 68 | elsif entry_type == "0" || entry_type.include?("richtext") 69 | # richtext looks to me like it's saved as HTML, so I include it here. 70 | "html" 71 | else 72 | # Other values might need custom work. 73 | entry_type 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/jekyll/migrators/drupal.rb: -------------------------------------------------------------------------------- 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 | 15 | # Reads a MySQL database via Sequel and creates a post file for each 16 | # post in wp_posts that has post_status = 'publish'. 17 | # This restriction is made because 'draft' posts are not guaranteed to 18 | # have valid dates. 19 | QUERY = "SELECT node.nid, node.title, node_revisions.body, node.created, node.status FROM node, node_revisions WHERE (node.type = 'blog' OR node.type = 'story') AND node.vid = node_revisions.vid" 20 | 21 | def self.process(dbname, user, pass, host = 'localhost') 22 | db = Sequel.mysql(dbname, :user => user, :password => pass, :host => host, :encoding => 'utf8') 23 | 24 | FileUtils.mkdir_p "_posts" 25 | FileUtils.mkdir_p "_drafts" 26 | 27 | # Create the refresh layout 28 | # Change the refresh url if you customized your permalink config 29 | File.open("_layouts/refresh.html", "w") do |f| 30 | f.puts < 32 | 33 | 34 | 35 | 36 | 37 | 38 | EOF 39 | end 40 | 41 | db[QUERY].each do |post| 42 | # Get required fields and construct Jekyll compatible name 43 | node_id = post[:nid] 44 | title = post[:title] 45 | content = post[:body] 46 | created = post[:created] 47 | time = Time.at(created) 48 | is_published = post[:status] == 1 49 | dir = is_published ? "_posts" : "_drafts" 50 | slug = title.strip.downcase.gsub(/(&|&)/, ' and ').gsub(/[\s\.\/\\]/, '-').gsub(/[^\w-]/, '').gsub(/[-_]{2,}/, '-').gsub(/^[-_]/, '').gsub(/[-_]$/, '') 51 | name = time.strftime("%Y-%m-%d-") + slug + '.md' 52 | 53 | # Get the relevant fields as a hash, delete empty fields and convert 54 | # to YAML for the header 55 | data = { 56 | 'layout' => 'post', 57 | 'title' => title.to_s, 58 | 'created' => created, 59 | }.delete_if { |k,v| v.nil? || v == ''}.to_yaml 60 | 61 | # Write out the data and content to file 62 | File.open("#{dir}/#{name}", "w") do |f| 63 | f.puts data 64 | f.puts "---" 65 | f.puts content 66 | end 67 | 68 | # Make a file to redirect from the old Drupal URL 69 | if is_published 70 | FileUtils.mkdir_p "node/#{node_id}" 71 | File.open("node/#{node_id}/index.md", "w") do |f| 72 | f.puts "---" 73 | f.puts "layout: refresh" 74 | f.puts "refresh_to_post_id: /#{time.strftime("%Y/%m/%d/") + slug}" 75 | f.puts "---" 76 | end 77 | end 78 | end 79 | 80 | # TODO: Make dirs & files for nodes of type 'page' 81 | # Make refresh pages for these as well 82 | 83 | # TODO: Make refresh dirs & files according to entries in url_alias table 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /features/permalinks.feature: -------------------------------------------------------------------------------- 1 | Feature: Fancy permalinks 2 | As a hacker who likes to blog 3 | I want to be able to set permalinks 4 | In order to make my blog URLs awesome 5 | 6 | Scenario: Use none permalink schema 7 | Given I have a _posts directory 8 | And I have the following post: 9 | | title | date | content | 10 | | None Permalink Schema | 3/27/2009 | Totally nothing. | 11 | And I have a configuration file with "permalink" set to "none" 12 | When I run jekyll 13 | Then the _site directory should exist 14 | And I should see "Totally nothing." in "_site/none-permalink-schema.html" 15 | 16 | Scenario: Use pretty permalink schema 17 | Given I have a _posts directory 18 | And I have the following post: 19 | | title | date | content | 20 | | Pretty Permalink Schema | 3/27/2009 | Totally wordpress. | 21 | And I have a configuration file with "permalink" set to "pretty" 22 | When I run jekyll 23 | Then the _site directory should exist 24 | And I should see "Totally wordpress." in "_site/2009/03/27/pretty-permalink-schema/index.html" 25 | 26 | Scenario: Use pretty permalink schema for pages 27 | Given I have an "index.html" page that contains "Totally index" 28 | And I have an "awesome.html" page that contains "Totally awesome" 29 | And I have an "sitemap.xml" page that contains "Totally uhm, sitemap" 30 | And I have a configuration file with "permalink" set to "pretty" 31 | When I run jekyll 32 | Then the _site directory should exist 33 | And I should see "Totally index" in "_site/index.html" 34 | And I should see "Totally awesome" in "_site/awesome/index.html" 35 | And I should see "Totally uhm, sitemap" in "_site/sitemap.xml" 36 | 37 | Scenario: Use custom permalink schema with prefix 38 | Given I have a _posts directory 39 | And I have the following post: 40 | | title | category | date | content | 41 | | Custom Permalink Schema | stuff | 3/27/2009 | Totally custom. | 42 | And I have a configuration file with "permalink" set to "/blog/:year/:month/:day/:title" 43 | When I run jekyll 44 | Then the _site directory should exist 45 | And I should see "Totally custom." in "_site/blog/2009/03/27/custom-permalink-schema/index.html" 46 | 47 | Scenario: Use custom permalink schema with category 48 | Given I have a _posts directory 49 | And I have the following post: 50 | | title | category | date | content | 51 | | Custom Permalink Schema | stuff | 3/27/2009 | Totally custom. | 52 | And I have a configuration file with "permalink" set to "/:categories/:title.html" 53 | When I run jekyll 54 | Then the _site directory should exist 55 | And I should see "Totally custom." in "_site/stuff/custom-permalink-schema.html" 56 | 57 | Scenario: Use custom permalink schema with squished date 58 | Given I have a _posts directory 59 | And I have the following post: 60 | | title | category | date | content | 61 | | Custom Permalink Schema | stuff | 3/27/2009 | Totally custom. | 62 | And I have a configuration file with "permalink" set to "/:month-:day-:year/:title.html" 63 | When I run jekyll 64 | Then the _site directory should exist 65 | And I should see "Totally custom." in "_site/03-27-2009/custom-permalink-schema.html" 66 | -------------------------------------------------------------------------------- /lib/jekyll/albino.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Wrapper for the Pygments command line tool, pygmentize. 3 | # 4 | # Pygments: http://pygments.org/ 5 | # 6 | # Assumes pygmentize is in the path. If not, set its location 7 | # with Albino.bin = '/path/to/pygmentize' 8 | # 9 | # Use like so: 10 | # 11 | # @syntaxer = Albino.new('/some/file.rb', :ruby) 12 | # puts @syntaxer.colorize 13 | # 14 | # This'll print out an HTMLized, Ruby-highlighted version 15 | # of '/some/file.rb'. 16 | # 17 | # To use another formatter, pass it as the third argument: 18 | # 19 | # @syntaxer = Albino.new('/some/file.rb', :ruby, :bbcode) 20 | # puts @syntaxer.colorize 21 | # 22 | # You can also use the #colorize class method: 23 | # 24 | # puts Albino.colorize('/some/file.rb', :ruby) 25 | # 26 | # Another also: you get a #to_s, for somewhat nicer use in Rails views. 27 | # 28 | # ... helper file ... 29 | # def highlight(text) 30 | # Albino.new(text, :ruby) 31 | # end 32 | # 33 | # ... view file ... 34 | # <%= highlight text %> 35 | # 36 | # The default lexer is 'text'. You need to specify a lexer yourself; 37 | # because we are using STDIN there is no auto-detect. 38 | # 39 | # To see all lexers and formatters available, run `pygmentize -L`. 40 | # 41 | # Chris Wanstrath // chris@ozmm.org 42 | # GitHub // http://github.com 43 | # 44 | 45 | class Albino 46 | @@bin = Rails.development? ? 'pygmentize' : '/usr/bin/pygmentize' rescue 'pygmentize' 47 | 48 | def self.bin=(path) 49 | @@bin = path 50 | end 51 | 52 | def self.colorize(*args) 53 | new(*args).colorize 54 | end 55 | 56 | def initialize(target, lexer = :text, format = :html) 57 | @target = target 58 | @options = { :l => lexer, :f => format, :O => 'encoding=utf-8' } 59 | end 60 | 61 | def execute(command) 62 | output = '' 63 | IO.popen(command, mode='r+') do |p| 64 | p.write @target 65 | p.close_write 66 | output = p.read.strip 67 | end 68 | output 69 | end 70 | 71 | def colorize(options = {}) 72 | html = execute(@@bin + convert_options(options)) 73 | # Work around an RDiscount bug: http://gist.github.com/97682 74 | html.to_s.sub(%r{
\Z}, "\n") 75 | end 76 | alias_method :to_s, :colorize 77 | 78 | def convert_options(options = {}) 79 | @options.merge(options).inject('') do |string, (flag, value)| 80 | string + " -#{flag} #{value}" 81 | end 82 | end 83 | end 84 | 85 | if $0 == __FILE__ 86 | require 'rubygems' 87 | require 'test/spec' 88 | require 'mocha' 89 | begin require 'redgreen'; rescue LoadError; end 90 | 91 | context "Albino" do 92 | setup do 93 | @syntaxer = Albino.new(__FILE__, :ruby) 94 | end 95 | 96 | specify "defaults to text" do 97 | syntaxer = Albino.new(__FILE__) 98 | syntaxer.expects(:execute).with('pygmentize -f html -l text').returns(true) 99 | syntaxer.colorize 100 | end 101 | 102 | specify "accepts options" do 103 | @syntaxer.expects(:execute).with('pygmentize -f html -l ruby').returns(true) 104 | @syntaxer.colorize 105 | end 106 | 107 | specify "works with strings" do 108 | syntaxer = Albino.new('class New; end', :ruby) 109 | assert_match %r(highlight), syntaxer.colorize 110 | end 111 | 112 | specify "aliases to_s" do 113 | assert_equal @syntaxer.colorize, @syntaxer.to_s 114 | end 115 | 116 | specify "class method colorize" do 117 | assert_equal @syntaxer.colorize, Albino.colorize(__FILE__, :ruby) 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /test/test_pager.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestPager < Test::Unit::TestCase 4 | 5 | should "calculate number of pages" do 6 | assert_equal(0, Pager.calculate_pages([], '2')) 7 | assert_equal(1, Pager.calculate_pages([1], '2')) 8 | assert_equal(1, Pager.calculate_pages([1,2], '2')) 9 | assert_equal(2, Pager.calculate_pages([1,2,3], '2')) 10 | assert_equal(2, Pager.calculate_pages([1,2,3,4], '2')) 11 | assert_equal(3, Pager.calculate_pages([1,2,3,4,5], '2')) 12 | end 13 | 14 | context "pagination disabled" do 15 | setup do 16 | stub(Jekyll).configuration do 17 | Jekyll::DEFAULTS.merge({ 18 | 'source' => source_dir, 19 | 'destination' => dest_dir 20 | }) 21 | end 22 | @config = Jekyll.configuration 23 | end 24 | 25 | should "report that pagination is disabled" do 26 | assert !Pager.pagination_enabled?(@config, 'index.html') 27 | end 28 | 29 | end 30 | 31 | context "pagination enabled for 2" do 32 | setup do 33 | stub(Jekyll).configuration do 34 | Jekyll::DEFAULTS.merge({ 35 | 'source' => source_dir, 36 | 'destination' => dest_dir, 37 | 'paginate' => 2 38 | }) 39 | end 40 | 41 | @config = Jekyll.configuration 42 | @site = Site.new(@config) 43 | @site.process 44 | @posts = @site.posts 45 | end 46 | 47 | should "report that pagination is enabled" do 48 | assert Pager.pagination_enabled?(@config, 'index.html') 49 | end 50 | 51 | context "with 4 posts" do 52 | setup do 53 | @posts = @site.posts[1..4] # limit to 4 54 | end 55 | 56 | should "create first pager" do 57 | pager = Pager.new(@config, 1, @posts) 58 | assert_equal(2, pager.posts.size) 59 | assert_equal(2, pager.total_pages) 60 | assert_nil(pager.previous_page) 61 | assert_equal(2, pager.next_page) 62 | end 63 | 64 | should "create second pager" do 65 | pager = Pager.new(@config, 2, @posts) 66 | assert_equal(2, pager.posts.size) 67 | assert_equal(2, pager.total_pages) 68 | assert_equal(1, pager.previous_page) 69 | assert_nil(pager.next_page) 70 | end 71 | 72 | should "not create third pager" do 73 | assert_raise(RuntimeError) { Pager.new(@config, 3, @posts) } 74 | end 75 | 76 | end 77 | 78 | context "with 5 posts" do 79 | setup do 80 | @posts = @site.posts[1..5] # limit to 5 81 | end 82 | 83 | should "create first pager" do 84 | pager = Pager.new(@config, 1, @posts) 85 | assert_equal(2, pager.posts.size) 86 | assert_equal(3, pager.total_pages) 87 | assert_nil(pager.previous_page) 88 | assert_equal(2, pager.next_page) 89 | end 90 | 91 | should "create second pager" do 92 | pager = Pager.new(@config, 2, @posts) 93 | assert_equal(2, pager.posts.size) 94 | assert_equal(3, pager.total_pages) 95 | assert_equal(1, pager.previous_page) 96 | assert_equal(3, pager.next_page) 97 | end 98 | 99 | should "create third pager" do 100 | pager = Pager.new(@config, 3, @posts) 101 | assert_equal(1, pager.posts.size) 102 | assert_equal(3, pager.total_pages) 103 | assert_equal(2, pager.previous_page) 104 | assert_nil(pager.next_page) 105 | end 106 | 107 | should "not create fourth pager" do 108 | assert_raise(RuntimeError) { Pager.new(@config, 4, @posts) } 109 | end 110 | 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /test/test_tags.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestTags < Test::Unit::TestCase 4 | 5 | def create_post(content, override = {}, converter_class = Jekyll::MarkdownConverter) 6 | stub(Jekyll).configuration do 7 | Jekyll::DEFAULTS.merge({'pygments' => true}).merge(override) 8 | end 9 | site = Site.new(Jekyll.configuration) 10 | info = { :filters => [Jekyll::Filters], :registers => { :site => site } } 11 | @converter = site.converters.find { |c| c.class == converter_class } 12 | payload = { "pygments_prefix" => @converter.pygments_prefix, 13 | "pygments_suffix" => @converter.pygments_suffix } 14 | 15 | @result = Liquid::Template.parse(content).render(payload, info) 16 | @result = @converter.convert(@result) 17 | end 18 | 19 | def fill_post(code, override = {}) 20 | content = <test\n}, @result 43 | end 44 | end 45 | 46 | context "post content has highlight with file reference" do 47 | setup do 48 | fill_post("./jekyll.gemspec") 49 | end 50 | 51 | should "not embed the file" do 52 | assert_match %{
./jekyll.gemspec\n
}, @result 53 | end 54 | end 55 | 56 | context "post content has highlight tag with UTF character" do 57 | setup do 58 | fill_post("Æ") 59 | end 60 | 61 | should "render markdown with pygments line handling" do 62 | assert_match %{
Æ\n
}, @result 63 | end 64 | end 65 | 66 | context "simple post with markdown and pre tags" do 67 | setup do 68 | @content = <
}, @result 91 | end 92 | end 93 | 94 | context "using Maruku" do 95 | setup do 96 | create_post(@content) 97 | end 98 | 99 | should "parse correctly" do 100 | assert_match %r{FIGHT!}, @result 101 | assert_match %r{FINISH HIM}, @result 102 | end 103 | end 104 | 105 | context "using RDiscount" do 106 | setup do 107 | create_post(@content, 'markdown' => 'rdiscount') 108 | end 109 | 110 | should "parse correctly" do 111 | assert_match %r{FIGHT!}, @result 112 | assert_match %r{FINISH HIM}, @result 113 | end 114 | end 115 | 116 | context "using Kramdown" do 117 | setup do 118 | create_post(@content, 'markdown' => 'kramdown') 119 | end 120 | 121 | should "parse correctly" do 122 | assert_match %r{FIGHT!}, @result 123 | assert_match %r{FINISH HIM}, @result 124 | end 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /test/test_page.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestPage < Test::Unit::TestCase 4 | def setup_page(file) 5 | @page = Page.new(@site, source_dir, '', file) 6 | end 7 | 8 | def do_render(page) 9 | layouts = { "default" => Layout.new(@site, source_dir('_layouts'), "simple.html")} 10 | page.render(layouts, {"site" => {"posts" => []}}) 11 | end 12 | 13 | context "A Page" do 14 | setup do 15 | clear_dest 16 | stub(Jekyll).configuration { Jekyll::DEFAULTS } 17 | @site = Site.new(Jekyll.configuration) 18 | end 19 | 20 | context "processing pages" do 21 | should "create url based on filename" do 22 | @page = setup_page('contacts.html') 23 | assert_equal "/contacts.html", @page.url 24 | end 25 | 26 | should "deal properly with extensions" do 27 | @page = setup_page('deal.with.dots.html') 28 | assert_equal ".html", @page.ext 29 | end 30 | 31 | should "deal properly with dots" do 32 | @page = setup_page('deal.with.dots.html') 33 | assert_equal "deal.with.dots", @page.basename 34 | end 35 | 36 | context "with pretty url style" do 37 | setup do 38 | @site.permalink_style = :pretty 39 | end 40 | 41 | should "return dir correctly" do 42 | @page = setup_page('contacts.html') 43 | assert_equal '/contacts/', @page.dir 44 | end 45 | 46 | should "return dir correctly for index page" do 47 | @page = setup_page('index.html') 48 | assert_equal '/', @page.dir 49 | end 50 | end 51 | 52 | context "with any other url style" do 53 | should "return dir correctly" do 54 | @site.permalink_style = nil 55 | @page = setup_page('contacts.html') 56 | assert_equal '/', @page.dir 57 | end 58 | end 59 | 60 | should "respect permalink in yaml front matter" do 61 | file = "about.html" 62 | @page = setup_page(file) 63 | 64 | assert_equal "/about/", @page.permalink 65 | assert_equal @page.permalink, @page.url 66 | assert_equal "/about/", @page.dir 67 | end 68 | end 69 | 70 | context "rendering" do 71 | setup do 72 | clear_dest 73 | end 74 | 75 | should "write properly" do 76 | page = setup_page('contacts.html') 77 | do_render(page) 78 | page.write(dest_dir) 79 | 80 | assert File.directory?(dest_dir) 81 | assert File.exists?(File.join(dest_dir, 'contacts.html')) 82 | end 83 | 84 | should "write properly without html extension" do 85 | page = setup_page('contacts.html') 86 | page.site.permalink_style = :pretty 87 | do_render(page) 88 | page.write(dest_dir) 89 | 90 | assert File.directory?(dest_dir) 91 | assert File.exists?(File.join(dest_dir, 'contacts', 'index.html')) 92 | end 93 | 94 | should "write properly with extension different from html" do 95 | page = setup_page("sitemap.xml") 96 | page.site.permalink_style = :pretty 97 | do_render(page) 98 | page.write(dest_dir) 99 | 100 | assert_equal("/sitemap.xml", page.url) 101 | assert_nil(page.url[/\.html$/]) 102 | assert File.directory?(dest_dir) 103 | assert File.exists?(File.join(dest_dir,'sitemap.xml')) 104 | end 105 | 106 | should "write dotfiles properly" do 107 | page = setup_page('.htaccess') 108 | do_render(page) 109 | page.write(dest_dir) 110 | 111 | assert File.directory?(dest_dir) 112 | assert File.exists?(File.join(dest_dir, '.htaccess')) 113 | end 114 | end 115 | 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/jekyll/page.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | 3 | class Page 4 | include Convertible 5 | 6 | attr_accessor :site, :pager 7 | attr_accessor :name, :ext, :basename, :dir 8 | attr_accessor :data, :content, :output 9 | 10 | # Initialize a new Page. 11 | # +site+ is the Site 12 | # +base+ is the String path to the 13 | # +dir+ is the String path between and the file 14 | # +name+ is the String filename of the file 15 | # 16 | # Returns 17 | def initialize(site, base, dir, name) 18 | @site = site 19 | @base = base 20 | @dir = dir 21 | @name = name 22 | 23 | self.process(name) 24 | self.read_yaml(File.join(base, dir), name) 25 | self.data['layout'] ||= 'page' 26 | end 27 | 28 | # The generated directory into which the page will be placed 29 | # upon generation. This is derived from the permalink or, if 30 | # permalink is absent, set to '/' 31 | # 32 | # Returns 33 | def dir 34 | url[-1, 1] == '/' ? url : File.dirname(url) 35 | end 36 | 37 | # The full path and filename of the post. 38 | # Defined in the YAML of the post body 39 | # (Optional) 40 | # 41 | # Returns 42 | def permalink 43 | self.data && self.data['permalink'] 44 | end 45 | 46 | def template 47 | if self.site.permalink_style == :pretty && !index? && html? 48 | "/:basename/" 49 | else 50 | "/:basename:output_ext" 51 | end 52 | end 53 | 54 | # The generated relative url of this page 55 | # e.g. /about.html 56 | # 57 | # Returns 58 | def url 59 | return permalink if permalink 60 | 61 | @url ||= { 62 | "basename" => self.basename, 63 | "output_ext" => self.output_ext, 64 | }.inject(template) { |result, token| 65 | result.gsub(/:#{token.first}/, token.last) 66 | }.gsub(/\/\//, "/") 67 | end 68 | 69 | # Extract information from the page filename 70 | # +name+ is the String filename of the page file 71 | # 72 | # Returns nothing 73 | def process(name) 74 | self.ext = File.extname(name) 75 | self.basename = name[0 .. -self.ext.length-1] 76 | end 77 | 78 | # Add any necessary layouts to this post 79 | # +layouts+ is a Hash of {"name" => "layout"} 80 | # +site_payload+ is the site payload hash 81 | # 82 | # Returns nothing 83 | def render(layouts, site_payload) 84 | payload = { 85 | "page" => self.to_liquid, 86 | 'paginator' => pager.to_liquid 87 | }.deep_merge(site_payload) 88 | 89 | do_layout(payload, layouts) 90 | end 91 | 92 | def to_liquid 93 | self.data.deep_merge({ 94 | "url" => File.join(@dir, self.url), 95 | "content" => self.content }) 96 | end 97 | 98 | # Obtain destination path. 99 | # +dest+ is the String path to the destination dir 100 | # 101 | # Returns destination file path. 102 | def destination(dest) 103 | # The url needs to be unescaped in order to preserve the correct filename 104 | path = File.join(dest, @dir, CGI.unescape(self.url)) 105 | path = File.join(path, "index.html") if self.url =~ /\/$/ 106 | path 107 | end 108 | 109 | # Write the generated page file to the destination directory. 110 | # +dest+ is the String path to the destination dir 111 | # 112 | # Returns nothing 113 | def write(dest) 114 | path = destination(dest) 115 | FileUtils.mkdir_p(File.dirname(path)) 116 | File.open(path, 'w') do |f| 117 | f.write(self.output) 118 | end 119 | end 120 | 121 | def inspect 122 | "#" 123 | end 124 | 125 | def html? 126 | output_ext == '.html' 127 | end 128 | 129 | def index? 130 | basename == 'index' 131 | end 132 | 133 | end 134 | 135 | end 136 | -------------------------------------------------------------------------------- /features/site_data.feature: -------------------------------------------------------------------------------- 1 | Feature: Site data 2 | As a hacker who likes to blog 3 | I want to be able to embed data into my site 4 | In order to make the site slightly dynamic 5 | 6 | Scenario: Use page variable in a page 7 | Given I have an "contact.html" page with title "Contact" that contains "{{ page.title }}: email@me.com" 8 | When I run jekyll 9 | Then the _site directory should exist 10 | And I should see "Contact: email@me.com" in "_site/contact.html" 11 | 12 | Scenario: Use site.time variable 13 | Given I have an "index.html" page that contains "{{ site.time }}" 14 | When I run jekyll 15 | Then the _site directory should exist 16 | And I should see today's time in "_site/index.html" 17 | 18 | Scenario: Use site.posts variable for latest post 19 | Given I have a _posts directory 20 | And I have an "index.html" page that contains "{{ site.posts.first.title }}: {{ site.posts.first.url }}" 21 | And I have the following posts: 22 | | title | date | content | 23 | | First Post | 3/25/2009 | My First Post | 24 | | Second Post | 3/26/2009 | My Second Post | 25 | | Third Post | 3/27/2009 | My Third Post | 26 | When I run jekyll 27 | Then the _site directory should exist 28 | And I should see "Third Post: /2009/03/27/third-post.html" in "_site/index.html" 29 | 30 | Scenario: Use site.posts variable in a loop 31 | Given I have a _posts directory 32 | And I have an "index.html" page that contains "{% for post in site.posts %} {{ post.title }} {% endfor %}" 33 | And I have the following posts: 34 | | title | date | content | 35 | | First Post | 3/25/2009 | My First Post | 36 | | Second Post | 3/26/2009 | My Second Post | 37 | | Third Post | 3/27/2009 | My Third Post | 38 | When I run jekyll 39 | Then the _site directory should exist 40 | And I should see "Third Post Second Post First Post" in "_site/index.html" 41 | 42 | Scenario: Use site.categories.code variable 43 | Given I have a _posts directory 44 | And I have an "index.html" page that contains "{% for post in site.categories.code %} {{ post.title }} {% endfor %}" 45 | And I have the following posts: 46 | | title | date | category | content | 47 | | Awesome Hack | 3/26/2009 | code | puts 'Hello World' | 48 | | Delicious Beer | 3/26/2009 | food | 1) Yuengling | 49 | When I run jekyll 50 | Then the _site directory should exist 51 | And I should see "Awesome Hack" in "_site/index.html" 52 | 53 | Scenario: Use site.tags variable 54 | Given I have a _posts directory 55 | And I have an "index.html" page that contains "{% for post in site.tags.beer %} {{ post.content }} {% endfor %}" 56 | And I have the following posts: 57 | | title | date | tag | content | 58 | | Delicious Beer | 3/26/2009 | beer | 1) Yuengling | 59 | When I run jekyll 60 | Then the _site directory should exist 61 | And I should see "Yuengling" in "_site/index.html" 62 | 63 | Scenario: Order Posts by name when on the same date 64 | Given I have a _posts directory 65 | And I have an "index.html" page that contains "{% for post in site.posts %}{{ post.title }}:{{ post.previous.title}},{{ post.next.title}} {% endfor %}" 66 | And I have the following posts: 67 | | title | date | content | 68 | | first | 2/26/2009 | first | 69 | | A | 3/26/2009 | A | 70 | | B | 3/26/2009 | B | 71 | | C | 3/26/2009 | C | 72 | | last | 4/26/2009 | last | 73 | When I run jekyll 74 | Then the _site directory should exist 75 | And I should see "last:C, C:B,last B:A,C A:first,B first:,A" in "_site/index.html" 76 | 77 | Scenario: Use configuration date in site payload 78 | Given I have an "index.html" page that contains "{{ site.url }}" 79 | And I have a configuration file with "url" set to "http://mysite.com" 80 | When I run jekyll 81 | Then the _site directory should exist 82 | And I should see "http://mysite.com" in "_site/index.html" 83 | -------------------------------------------------------------------------------- /lib/jekyll.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.dirname(__FILE__) # For use/testing when no gem is installed 2 | 3 | # Require all of the Ruby files in the given directory. 4 | # 5 | # path - The String relative path from here to the directory. 6 | # 7 | # Returns nothing. 8 | def require_all(path) 9 | glob = File.join(File.dirname(__FILE__), path, '*.rb') 10 | Dir[glob].each do |f| 11 | require f 12 | end 13 | end 14 | 15 | # rubygems 16 | require 'rubygems' 17 | 18 | # stdlib 19 | require 'fileutils' 20 | require 'time' 21 | require 'yaml' 22 | 23 | # 3rd party 24 | require 'liquid' 25 | require 'maruku' 26 | 27 | # internal requires 28 | require 'jekyll/core_ext' 29 | require 'jekyll/site' 30 | require 'jekyll/convertible' 31 | require 'jekyll/layout' 32 | require 'jekyll/page' 33 | require 'jekyll/post' 34 | require 'jekyll/filters' 35 | require 'jekyll/albino' 36 | require 'jekyll/static_file' 37 | require 'jekyll/errors' 38 | 39 | # extensions 40 | require 'jekyll/plugin' 41 | require 'jekyll/converter' 42 | require 'jekyll/generator' 43 | require_all 'jekyll/converters' 44 | require_all 'jekyll/generators' 45 | require_all 'jekyll/tags' 46 | 47 | module Jekyll 48 | VERSION = '0.10.0' 49 | 50 | # Default options. Overriden by values in _config.yml or command-line opts. 51 | # (Strings rather symbols used for compatability with YAML). 52 | DEFAULTS = { 53 | 'safe' => false, 54 | 'auto' => false, 55 | 'server' => false, 56 | 'server_port' => 4000, 57 | 58 | 'source' => Dir.pwd, 59 | 'destination' => File.join(Dir.pwd, '_site'), 60 | 'plugins' => File.join(Dir.pwd, '_plugins'), 61 | 62 | 'future' => true, 63 | 'lsi' => false, 64 | 'pygments' => false, 65 | 'markdown' => 'maruku', 66 | 'permalink' => 'date', 67 | 68 | 'maruku' => { 69 | 'use_tex' => false, 70 | 'use_divs' => false, 71 | 'png_engine' => 'blahtex', 72 | 'png_dir' => 'images/latex', 73 | 'png_url' => '/images/latex' 74 | }, 75 | 'rdiscount' => { 76 | 'extensions' => [] 77 | }, 78 | 'kramdown' => { 79 | 'auto_ids' => true, 80 | 'footnote_nr' => 1, 81 | 'entity_output' => 'as_char', 82 | 'toc_levels' => '1..6', 83 | 'use_coderay' => false, 84 | 85 | 'coderay' => { 86 | 'coderay_wrap' => 'div', 87 | 'coderay_line_numbers' => 'inline', 88 | 'coderay_line_number_start' => 1, 89 | 'coderay_tab_width' => 4, 90 | 'coderay_bold_every' => 10, 91 | 'coderay_css' => 'style' 92 | } 93 | } 94 | } 95 | 96 | # Generate a Jekyll configuration Hash by merging the default options 97 | # with anything in _config.yml, and adding the given options on top. 98 | # 99 | # override - A Hash of config directives that override any options in both 100 | # the defaults and the config file. See Jekyll::DEFAULTS for a 101 | # list of option names and their defaults. 102 | # 103 | # Returns the final configuration Hash. 104 | def self.configuration(override) 105 | # _config.yml may override default source location, but until 106 | # then, we need to know where to look for _config.yml 107 | source = override['source'] || Jekyll::DEFAULTS['source'] 108 | 109 | # Get configuration from /_config.yml 110 | config_file = File.join(source, '_config.yml') 111 | begin 112 | config = YAML.load_file(config_file) 113 | raise "Invalid configuration - #{config_file}" if !config.is_a?(Hash) 114 | $stdout.puts "Configuration from #{config_file}" 115 | rescue => err 116 | $stderr.puts "WARNING: Could not read configuration. " + 117 | "Using defaults (and options)." 118 | $stderr.puts "\t" + err.to_s 119 | config = {} 120 | end 121 | 122 | # Merge DEFAULTS < _config.yml < override 123 | Jekyll::DEFAULTS.deep_merge(config).deep_merge(override) 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /features/step_definitions/jekyll_steps.rb: -------------------------------------------------------------------------------- 1 | Before do 2 | FileUtils.mkdir(TEST_DIR) 3 | Dir.chdir(TEST_DIR) 4 | end 5 | 6 | After do 7 | Dir.chdir(TEST_DIR) 8 | FileUtils.rm_rf(TEST_DIR) 9 | end 10 | 11 | Given /^I have a blank site in "(.*)"$/ do |path| 12 | FileUtils.mkdir(path) 13 | end 14 | 15 | # Like "I have a foo file" but gives a yaml front matter so jekyll actually processes it 16 | Given /^I have an? "(.*)" page(?: with (.*) "(.*)")? that contains "(.*)"$/ do |file, key, value, text| 17 | File.open(file, 'w') do |f| 18 | f.write < true) 115 | end 116 | 117 | When /^I change "(.*)" to contain "(.*)"$/ do |file, text| 118 | File.open(file, 'a') do |f| 119 | f.write(text) 120 | end 121 | end 122 | 123 | Then /^the (.*) directory should exist$/ do |dir| 124 | assert File.directory?(dir) 125 | end 126 | 127 | Then /^I should see "(.*)" in "(.*)"$/ do |text, file| 128 | assert_match Regexp.new(text), File.open(file).readlines.join 129 | end 130 | 131 | Then /^the "(.*)" file should exist$/ do |file| 132 | assert File.file?(file) 133 | end 134 | 135 | Then /^the "(.*)" file should not exist$/ do |file| 136 | assert !File.exists?(file) 137 | end 138 | 139 | Then /^I should see today's time in "(.*)"$/ do |file| 140 | assert_match Regexp.new(Regexp.escape(Time.now.to_s)), File.open(file).readlines.join 141 | end 142 | 143 | Then /^I should see today's date in "(.*)"$/ do |file| 144 | assert_match Regexp.new(Date.today.to_s), File.open(file).readlines.join 145 | end 146 | -------------------------------------------------------------------------------- /lib/jekyll/converters/markdown.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | 3 | class MarkdownConverter < Converter 4 | safe true 5 | 6 | pygments_prefix "\n" 7 | pygments_suffix "\n" 8 | 9 | def setup 10 | return if @setup 11 | # Set the Markdown interpreter (and Maruku self.config, if necessary) 12 | case @config['markdown'] 13 | when 'kramdown' 14 | begin 15 | require 'kramdown' 16 | rescue LoadError 17 | STDERR.puts 'You are missing a library required for Markdown. Please run:' 18 | STDERR.puts ' $ [sudo] gem install kramdown' 19 | raise FatalException.new("Missing dependency: kramdown") 20 | end 21 | when 'rdiscount' 22 | begin 23 | require 'rdiscount' 24 | 25 | # Load rdiscount extensions 26 | @rdiscount_extensions = @config['rdiscount']['extensions'].map { |e| e.to_sym } 27 | rescue LoadError 28 | STDERR.puts 'You are missing a library required for Markdown. Please run:' 29 | STDERR.puts ' $ [sudo] gem install rdiscount' 30 | raise FatalException.new("Missing dependency: rdiscount") 31 | end 32 | when 'maruku' 33 | begin 34 | require 'maruku' 35 | 36 | if @config['maruku']['use_divs'] 37 | require 'maruku/ext/div' 38 | STDERR.puts 'Maruku: Using extended syntax for div elements.' 39 | end 40 | 41 | if @config['maruku']['use_tex'] 42 | require 'maruku/ext/math' 43 | STDERR.puts "Maruku: Using LaTeX extension. Images in `#{@config['maruku']['png_dir']}`." 44 | 45 | # Switch off MathML output 46 | MaRuKu::Globals[:html_math_output_mathml] = false 47 | MaRuKu::Globals[:html_math_engine] = 'none' 48 | 49 | # Turn on math to PNG support with blahtex 50 | # Resulting PNGs stored in `images/latex` 51 | MaRuKu::Globals[:html_math_output_png] = true 52 | MaRuKu::Globals[:html_png_engine] = @config['maruku']['png_engine'] 53 | MaRuKu::Globals[:html_png_dir] = @config['maruku']['png_dir'] 54 | MaRuKu::Globals[:html_png_url] = @config['maruku']['png_url'] 55 | end 56 | rescue LoadError 57 | STDERR.puts 'You are missing a library required for Markdown. Please run:' 58 | STDERR.puts ' $ [sudo] gem install maruku' 59 | raise FatalException.new("Missing dependency: maruku") 60 | end 61 | else 62 | STDERR.puts "Invalid Markdown processor: #{@config['markdown']}" 63 | STDERR.puts " Valid options are [ maruku | rdiscount | kramdown ]" 64 | raise FatalException.new("Invalid Markdown process: #{@config['markdown']}") 65 | end 66 | @setup = true 67 | end 68 | 69 | def matches(ext) 70 | ext =~ /(markdown|mkdn?|md)/i 71 | end 72 | 73 | def output_ext(ext) 74 | ".html" 75 | end 76 | 77 | def convert(content) 78 | setup 79 | case @config['markdown'] 80 | when 'kramdown' 81 | # Check for use of coderay 82 | if @config['kramdown']['use_coderay'] 83 | Kramdown::Document.new(content, { 84 | :auto_ids => @config['kramdown']['auto_ids'], 85 | :footnote_nr => @config['kramdown']['footnote_nr'], 86 | :entity_output => @config['kramdown']['entity_output'], 87 | :toc_levels => @config['kramdown']['toc_levels'], 88 | 89 | :coderay_wrap => @config['kramdown']['coderay']['coderay_wrap'], 90 | :coderay_line_numbers => @config['kramdown']['coderay']['coderay_line_numbers'], 91 | :coderay_line_number_start => @config['kramdown']['coderay']['coderay_line_number_start'], 92 | :coderay_tab_width => @config['kramdown']['coderay']['coderay_tab_width'], 93 | :coderay_bold_every => @config['kramdown']['coderay']['coderay_bold_every'], 94 | :coderay_css => @config['kramdown']['coderay']['coderay_css'] 95 | }).to_html 96 | else 97 | # not using coderay 98 | Kramdown::Document.new(content, { 99 | :auto_ids => @config['kramdown']['auto_ids'], 100 | :footnote_nr => @config['kramdown']['footnote_nr'], 101 | :entity_output => @config['kramdown']['entity_output'], 102 | :toc_levels => @config['kramdown']['toc_levels'] 103 | }).to_html 104 | end 105 | when 'rdiscount' 106 | RDiscount.new(content, *@rdiscount_extensions).to_html 107 | when 'maruku' 108 | Maruku.new(content).to_html 109 | end 110 | end 111 | end 112 | 113 | end 114 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | require 'date' 4 | 5 | ############################################################################# 6 | # 7 | # Helper functions 8 | # 9 | ############################################################################# 10 | 11 | def name 12 | @name ||= Dir['*.gemspec'].first.split('.').first 13 | end 14 | 15 | def version 16 | line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/] 17 | line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1] 18 | end 19 | 20 | def date 21 | Date.today.to_s 22 | end 23 | 24 | def rubyforge_project 25 | name 26 | end 27 | 28 | def gemspec_file 29 | "#{name}.gemspec" 30 | end 31 | 32 | def gem_file 33 | "#{name}-#{version}.gem" 34 | end 35 | 36 | def replace_header(head, header_name) 37 | head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"} 38 | end 39 | 40 | ############################################################################# 41 | # 42 | # Standard tasks 43 | # 44 | ############################################################################# 45 | 46 | task :default => [:test, :features] 47 | 48 | require 'rake/testtask' 49 | Rake::TestTask.new(:test) do |test| 50 | test.libs << 'lib' << 'test' 51 | test.pattern = 'test/**/test_*.rb' 52 | test.verbose = true 53 | end 54 | 55 | desc "Generate RCov test coverage and open in your browser" 56 | task :coverage do 57 | require 'rcov' 58 | sh "rm -fr coverage" 59 | sh "rcov test/test_*.rb" 60 | sh "open coverage/index.html" 61 | end 62 | 63 | require 'rake/rdoctask' 64 | Rake::RDocTask.new do |rdoc| 65 | rdoc.rdoc_dir = 'rdoc' 66 | rdoc.title = "#{name} #{version}" 67 | rdoc.rdoc_files.include('README*') 68 | rdoc.rdoc_files.include('lib/**/*.rb') 69 | end 70 | 71 | desc "Open an irb session preloaded with this library" 72 | task :console do 73 | sh "irb -rubygems -r ./lib/#{name}.rb" 74 | end 75 | 76 | ############################################################################# 77 | # 78 | # Custom tasks (add your own tasks here) 79 | # 80 | ############################################################################# 81 | 82 | namespace :migrate do 83 | desc "Migrate from mephisto in the current directory" 84 | task :mephisto do 85 | sh %q(ruby -r './lib/jekyll/migrators/mephisto' -e 'Jekyll::Mephisto.postgres(:database => "#{ENV["DB"]}")') 86 | end 87 | desc "Migrate from Movable Type in the current directory" 88 | task :mt do 89 | sh %q(ruby -r './lib/jekyll/migrators/mt' -e 'Jekyll::MT.process("#{ENV["DB"]}", "#{ENV["USER"]}", "#{ENV["PASS"]}")') 90 | end 91 | desc "Migrate from Typo in the current directory" 92 | task :typo do 93 | sh %q(ruby -r './lib/jekyll/migrators/typo' -e 'Jekyll::Typo.process("#{ENV["DB"]}", "#{ENV["USER"]}", "#{ENV["PASS"]}")') 94 | end 95 | end 96 | 97 | begin 98 | require 'cucumber/rake/task' 99 | Cucumber::Rake::Task.new(:features) do |t| 100 | t.cucumber_opts = "--format progress" 101 | end 102 | rescue LoadError 103 | desc 'Cucumber rake task not available' 104 | task :features do 105 | abort 'Cucumber rake task is not available. Be sure to install cucumber as a gem or plugin' 106 | end 107 | end 108 | 109 | ############################################################################# 110 | # 111 | # Packaging tasks 112 | # 113 | ############################################################################# 114 | 115 | task :release => :build do 116 | unless `git branch` =~ /^\* master$/ 117 | puts "You must be on the master branch to release!" 118 | exit! 119 | end 120 | sh "git commit --allow-empty -a -m 'Release #{version}'" 121 | sh "git tag v#{version}" 122 | sh "git push origin master" 123 | sh "git push origin v#{version}" 124 | sh "gem push pkg/#{name}-#{version}.gem" 125 | end 126 | 127 | task :build => :gemspec do 128 | sh "mkdir -p pkg" 129 | sh "gem build #{gemspec_file}" 130 | sh "mv #{gem_file} pkg" 131 | end 132 | 133 | task :gemspec do 134 | # read spec file and split out manifest section 135 | spec = File.read(gemspec_file) 136 | head, manifest, tail = spec.split(" # = MANIFEST =\n") 137 | 138 | # replace name version and date 139 | replace_header(head, :name) 140 | replace_header(head, :version) 141 | replace_header(head, :date) 142 | #comment this out if your rubyforge_project has a different name 143 | replace_header(head, :rubyforge_project) 144 | 145 | # determine file list from git ls-files 146 | files = `git ls-files`. 147 | split("\n"). 148 | sort. 149 | reject { |file| file =~ /^\./ }. 150 | reject { |file| file =~ /^(rdoc|pkg|coverage)/ }. 151 | map { |file| " #{file}" }. 152 | join("\n") 153 | 154 | # piece file back together and write 155 | manifest = " s.files = %w[\n#{files}\n ]\n" 156 | spec = [head, manifest, tail].join(" # = MANIFEST =\n") 157 | File.open(gemspec_file, 'w') { |io| io.write(spec) } 158 | puts "Updated #{gemspec_file}" 159 | end -------------------------------------------------------------------------------- /features/create_sites.feature: -------------------------------------------------------------------------------- 1 | Feature: Create sites 2 | As a hacker who likes to blog 3 | I want to be able to make a static site 4 | In order to share my awesome ideas with the interwebs 5 | 6 | Scenario: Basic site 7 | Given I have an "index.html" file that contains "Basic Site" 8 | When I run jekyll 9 | Then the _site directory should exist 10 | And I should see "Basic Site" in "_site/index.html" 11 | 12 | Scenario: Basic site with a post 13 | Given I have a _posts directory 14 | And I have the following post: 15 | | title | date | content | 16 | | Hackers | 3/27/2009 | My First Exploit | 17 | When I run jekyll 18 | Then the _site directory should exist 19 | And I should see "My First Exploit" in "_site/2009/03/27/hackers.html" 20 | 21 | Scenario: Basic site with layout and a page 22 | Given I have a _layouts directory 23 | And I have an "index.html" page with layout "default" that contains "Basic Site with Layout" 24 | And I have a default layout that contains "Page Layout: {{ content }}" 25 | When I run jekyll 26 | Then the _site directory should exist 27 | And I should see "Page Layout: Basic Site with Layout" in "_site/index.html" 28 | 29 | Scenario: Basic site with layout and a post 30 | Given I have a _layouts directory 31 | And I have a _posts directory 32 | And I have the following posts: 33 | | title | date | layout | content | 34 | | Wargames | 3/27/2009 | default | The only winning move is not to play. | 35 | And I have a default layout that contains "Post Layout: {{ content }}" 36 | When I run jekyll 37 | Then the _site directory should exist 38 | And I should see "Post Layout:

The only winning move is not to play.

" in "_site/2009/03/27/wargames.html" 39 | 40 | Scenario: Basic site with layouts, pages, posts and files 41 | Given I have a _layouts directory 42 | And I have a page layout that contains "Page {{ page.title }}: {{ content }}" 43 | And I have a post layout that contains "Post {{ page.title }}: {{ content }}" 44 | And I have an "index.html" page with layout "page" that contains "Site contains {{ site.pages.size }} pages and {{ site.posts.size }} posts" 45 | And I have a blog directory 46 | And I have a "blog/index.html" page with layout "page" that contains "blog category index page" 47 | And I have an "about.html" file that contains "No replacement {{ site.posts.size }}" 48 | And I have an "another_file" file that contains "" 49 | And I have a _posts directory 50 | And I have the following posts: 51 | | title | date | layout | content | 52 | | entry1 | 3/27/2009 | post | content for entry1. | 53 | | entry2 | 4/27/2009 | post | content for entry2. | 54 | And I have a category/_posts directory 55 | And I have the following posts in "category": 56 | | title | date | layout | content | 57 | | entry3 | 5/27/2009 | post | content for entry3. | 58 | | entry4 | 6/27/2009 | post | content for entry4. | 59 | When I run jekyll 60 | Then the _site directory should exist 61 | And I should see "Page : Site contains 2 pages and 4 posts" in "_site/index.html" 62 | And I should see "No replacement \{\{ site.posts.size \}\}" in "_site/about.html" 63 | And I should see "" in "_site/another_file" 64 | And I should see "Page : blog category index page" in "_site/blog/index.html" 65 | And I should see "Post entry1:

content for entry1.

" in "_site/2009/03/27/entry1.html" 66 | And I should see "Post entry2:

content for entry2.

" in "_site/2009/04/27/entry2.html" 67 | And I should see "Post entry3:

content for entry3.

" in "_site/category/2009/05/27/entry3.html" 68 | And I should see "Post entry4:

content for entry4.

" in "_site/category/2009/06/27/entry4.html" 69 | 70 | Scenario: Basic site with include tag 71 | Given I have a _includes directory 72 | And I have an "index.html" page that contains "Basic Site with include tag: {% include about.textile %}" 73 | And I have an "_includes/about.textile" file that contains "Generated by Jekyll" 74 | When I run jekyll 75 | Then the _site directory should exist 76 | And I should see "Basic Site with include tag: Generated by Jekyll" in "_site/index.html" 77 | 78 | Scenario: Basic site with subdir include tag 79 | Given I have a _includes directory 80 | And I have an "_includes/about.textile" file that contains "Generated by Jekyll" 81 | And I have an info directory 82 | And I have an "info/index.html" page that contains "Basic Site with subdir include tag: {% include about.textile %}" 83 | When I run jekyll 84 | Then the _site directory should exist 85 | And I should see "Basic Site with subdir include tag: Generated by Jekyll" in "_site/info/index.html" 86 | 87 | Scenario: Basic site with nested include tag 88 | Given I have a _includes directory 89 | And I have an "_includes/about.textile" file that contains "Generated by {% include jekyll.textile %}" 90 | And I have an "_includes/jekyll.textile" file that contains "Jekyll" 91 | And I have an "index.html" page that contains "Basic Site with include tag: {% include about.textile %}" 92 | When I debug jekyll 93 | Then the _site directory should exist 94 | And I should see "Basic Site with include tag: Generated by Jekyll" in "_site/index.html" 95 | -------------------------------------------------------------------------------- /jekyll.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.specification_version = 2 if s.respond_to? :specification_version= 3 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 4 | s.rubygems_version = '1.3.5' 5 | 6 | s.name = 'jekyll' 7 | s.version = '0.10.0' 8 | s.date = '2010-12-16' 9 | s.rubyforge_project = 'jekyll' 10 | 11 | s.summary = "A simple, blog aware, static site generator." 12 | s.description = "Jekyll is a simple, blog aware, static site generator." 13 | 14 | s.authors = ["Tom Preston-Werner"] 15 | s.email = 'tom@mojombo.com' 16 | s.homepage = 'http://github.com/mojombo/jekyll' 17 | 18 | s.require_paths = %w[lib] 19 | 20 | s.executables = ["jekyll"] 21 | s.default_executable = 'jekyll' 22 | 23 | s.rdoc_options = ["--charset=UTF-8"] 24 | s.extra_rdoc_files = %w[README.textile LICENSE] 25 | 26 | s.add_runtime_dependency('liquid', [">= 1.9.0"]) 27 | s.add_runtime_dependency('classifier', [">= 1.3.1"]) 28 | s.add_runtime_dependency('directory_watcher', [">= 1.1.1"]) 29 | s.add_runtime_dependency('maruku', [">= 0.5.9"]) 30 | 31 | s.add_development_dependency('redgreen', [">= 4.2.1"]) 32 | s.add_development_dependency('shoulda', [">= 4.2.1"]) 33 | s.add_development_dependency('rr', [">= 4.2.1"]) 34 | s.add_development_dependency('cucumber', [">= 4.2.1"]) 35 | s.add_development_dependency('RedCloth', [">= 4.2.1"]) 36 | s.add_development_dependency('kramdown', [">= 0.12.0"]) 37 | 38 | # = MANIFEST = 39 | s.files = %w[ 40 | History.txt 41 | LICENSE 42 | README.textile 43 | Rakefile 44 | bin/jekyll 45 | cucumber.yml 46 | features/create_sites.feature 47 | features/embed_filters.feature 48 | features/markdown.feature 49 | features/pagination.feature 50 | features/permalinks.feature 51 | features/post_data.feature 52 | features/site_configuration.feature 53 | features/site_data.feature 54 | features/step_definitions/jekyll_steps.rb 55 | features/support/env.rb 56 | jekyll.gemspec 57 | lib/jekyll.rb 58 | lib/jekyll/albino.rb 59 | lib/jekyll/converter.rb 60 | lib/jekyll/converters/identity.rb 61 | lib/jekyll/converters/markdown.rb 62 | lib/jekyll/converters/textile.rb 63 | lib/jekyll/convertible.rb 64 | lib/jekyll/core_ext.rb 65 | lib/jekyll/errors.rb 66 | lib/jekyll/filters.rb 67 | lib/jekyll/generator.rb 68 | lib/jekyll/generators/pagination.rb 69 | lib/jekyll/layout.rb 70 | lib/jekyll/migrators/csv.rb 71 | lib/jekyll/migrators/drupal.rb 72 | lib/jekyll/migrators/marley.rb 73 | lib/jekyll/migrators/mephisto.rb 74 | lib/jekyll/migrators/mt.rb 75 | lib/jekyll/migrators/textpattern.rb 76 | lib/jekyll/migrators/typo.rb 77 | lib/jekyll/migrators/wordpress.com.rb 78 | lib/jekyll/migrators/wordpress.rb 79 | lib/jekyll/page.rb 80 | lib/jekyll/plugin.rb 81 | lib/jekyll/post.rb 82 | lib/jekyll/site.rb 83 | lib/jekyll/static_file.rb 84 | lib/jekyll/tags/highlight.rb 85 | lib/jekyll/tags/include.rb 86 | test/helper.rb 87 | test/source/.htaccess 88 | test/source/_includes/sig.markdown 89 | test/source/_layouts/default.html 90 | test/source/_layouts/simple.html 91 | test/source/_posts/2008-02-02-not-published.textile 92 | test/source/_posts/2008-02-02-published.textile 93 | test/source/_posts/2008-10-18-foo-bar.textile 94 | test/source/_posts/2008-11-21-complex.textile 95 | test/source/_posts/2008-12-03-permalinked-post.textile 96 | test/source/_posts/2008-12-13-include.markdown 97 | test/source/_posts/2009-01-27-array-categories.textile 98 | test/source/_posts/2009-01-27-categories.textile 99 | test/source/_posts/2009-01-27-category.textile 100 | test/source/_posts/2009-01-27-empty-categories.textile 101 | test/source/_posts/2009-01-27-empty-category.textile 102 | test/source/_posts/2009-03-12-hash-#1.markdown 103 | test/source/_posts/2009-05-18-empty-tag.textile 104 | test/source/_posts/2009-05-18-empty-tags.textile 105 | test/source/_posts/2009-05-18-tag.textile 106 | test/source/_posts/2009-05-18-tags.textile 107 | test/source/_posts/2009-06-22-empty-yaml.textile 108 | test/source/_posts/2009-06-22-no-yaml.textile 109 | test/source/_posts/2010-01-08-triple-dash.markdown 110 | test/source/_posts/2010-01-09-date-override.textile 111 | test/source/_posts/2010-01-09-time-override.textile 112 | test/source/_posts/2010-01-09-timezone-override.textile 113 | test/source/_posts/2010-01-16-override-data.textile 114 | test/source/about.html 115 | test/source/category/_posts/2008-9-23-categories.textile 116 | test/source/contacts.html 117 | test/source/css/screen.css 118 | test/source/deal.with.dots.html 119 | test/source/foo/_posts/bar/2008-12-12-topical-post.textile 120 | test/source/index.html 121 | test/source/sitemap.xml 122 | test/source/win/_posts/2009-05-24-yaml-linebreak.markdown 123 | test/source/z_category/_posts/2008-9-23-categories.textile 124 | test/suite.rb 125 | test/test_configuration.rb 126 | test/test_core_ext.rb 127 | test/test_filters.rb 128 | test/test_generated_site.rb 129 | test/test_kramdown.rb 130 | test/test_page.rb 131 | test/test_pager.rb 132 | test/test_post.rb 133 | test/test_rdiscount.rb 134 | test/test_site.rb 135 | test/test_tags.rb 136 | ] 137 | # = MANIFEST = 138 | 139 | s.test_files = s.files.select { |path| path =~ /^test\/test_.*\.rb/ } 140 | end 141 | -------------------------------------------------------------------------------- /bin/jekyll: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib]) 4 | 5 | help = < ./_site 10 | jekyll # . -> 11 | jekyll # -> 12 | 13 | Configuration is read from '/_config.yml' but can be overriden 14 | using the following options: 15 | 16 | HELP 17 | 18 | require 'optparse' 19 | require 'jekyll' 20 | 21 | exec = {} 22 | options = {} 23 | opts = OptionParser.new do |opts| 24 | opts.banner = help 25 | 26 | opts.on("--[no-]safe", "Safe mode (default unsafe)") do |safe| 27 | options['safe'] = safe 28 | end 29 | 30 | opts.on("--[no-]auto", "Auto-regenerate") do |auto| 31 | options['auto'] = auto 32 | end 33 | 34 | opts.on("--server [PORT]", "Start web server (default port 4000)") do |port| 35 | options['server'] = true 36 | options['server_port'] = port unless port.nil? 37 | end 38 | 39 | opts.on("--no-server", "Do not start a web server") do |part| 40 | options['server'] = false 41 | end 42 | 43 | opts.on("--base-url [BASE_URL]", "Serve website from a given base URL (default '/'") do |baseurl| 44 | options['baseurl'] = baseurl 45 | end 46 | 47 | opts.on("--[no-]lsi", "Use LSI for better related posts") do |lsi| 48 | options['lsi'] = lsi 49 | end 50 | 51 | opts.on("--[no-]pygments", "Use pygments to highlight code") do |pygments| 52 | options['pygments'] = pygments 53 | end 54 | 55 | opts.on("--rdiscount", "Use rdiscount gem for Markdown") do 56 | options['markdown'] = 'rdiscount' 57 | end 58 | 59 | opts.on("--kramdown", "Use kramdown gem for Markdown") do 60 | options['markdown'] = 'kramdown' 61 | end 62 | 63 | opts.on("--time [TIME]", "Time to generate the site for") do |time| 64 | options['time'] = Time.parse(time) 65 | end 66 | 67 | opts.on("--[no-]future", "Render future dated posts") do |future| 68 | options['future'] = future 69 | end 70 | 71 | opts.on("--permalink [TYPE]", "Use 'date' (default) for YYYY/MM/DD") do |style| 72 | options['permalink'] = style unless style.nil? 73 | end 74 | 75 | opts.on("--paginate [POSTS_PER_PAGE]", "Paginate a blog's posts") do |per_page| 76 | begin 77 | options['paginate'] = per_page.to_i 78 | raise ArgumentError if options['paginate'] == 0 79 | rescue 80 | puts 'you must specify a number of posts by page bigger than 0' 81 | exit 0 82 | end 83 | end 84 | 85 | opts.on("--limit_posts [MAX_POSTS]", "Limit the number of posts to publish") do |limit_posts| 86 | begin 87 | options['limit_posts'] = limit_posts.to_i 88 | raise ArgumentError if options['limit_posts'] < 1 89 | rescue 90 | puts 'you must specify a number of posts by page bigger than 0' 91 | exit 0 92 | end 93 | end 94 | 95 | opts.on("--url [URL]", "Set custom site.url") do |url| 96 | options['url'] = url 97 | end 98 | 99 | opts.on("--version", "Display current version") do 100 | puts "Jekyll " + Jekyll::VERSION 101 | exit 0 102 | end 103 | end 104 | 105 | # Read command line options into `options` hash 106 | opts.parse! 107 | 108 | # Get source and destintation from command line 109 | case ARGV.size 110 | when 0 111 | when 1 112 | options['destination'] = ARGV[0] 113 | when 2 114 | options['source'] = ARGV[0] 115 | options['destination'] = ARGV[1] 116 | else 117 | puts "Invalid options. Run `jekyll --help` for assistance." 118 | exit(1) 119 | end 120 | 121 | options = Jekyll.configuration(options) 122 | 123 | # Get source and destination directories (possibly set by config file) 124 | source = options['source'] 125 | destination = options['destination'] 126 | 127 | # Files to watch 128 | def globs(source) 129 | Dir.chdir(source) do 130 | dirs = Dir['*'].select { |x| File.directory?(x) } 131 | dirs -= ['_site'] 132 | dirs = dirs.map { |x| "#{x}/**/*" } 133 | dirs += ['*'] 134 | end 135 | end 136 | 137 | # Create the Site 138 | site = Jekyll::Site.new(options) 139 | 140 | # Run the directory watcher for auto-generation, if required 141 | if options['auto'] 142 | require 'directory_watcher' 143 | 144 | puts "Auto-regenerating enabled: #{source} -> #{destination}" 145 | 146 | dw = DirectoryWatcher.new(source) 147 | dw.interval = 1 148 | dw.glob = globs(source) 149 | 150 | dw.add_observer do |*args| 151 | t = Time.now.strftime("%Y-%m-%d %H:%M:%S") 152 | puts "[#{t}] regeneration: #{args.size} files changed" 153 | site.process 154 | end 155 | 156 | dw.start 157 | 158 | unless options['server'] 159 | loop { sleep 1000 } 160 | end 161 | else 162 | puts "Building site: #{source} -> #{destination}" 163 | begin 164 | site.process 165 | rescue Jekyll::FatalException 166 | exit(1) 167 | end 168 | puts "Successfully generated site: #{source} -> #{destination}" 169 | end 170 | 171 | # Run the server on the specified port, if required 172 | if options['server'] 173 | require 'webrick' 174 | include WEBrick 175 | 176 | FileUtils.mkdir_p(destination) 177 | 178 | mime_types = WEBrick::HTTPUtils::DefaultMimeTypes 179 | mime_types.store 'js', 'application/javascript' 180 | 181 | s = HTTPServer.new( 182 | :Port => options['server_port'], 183 | :MimeTypes => mime_types 184 | ) 185 | s.mount(options['baseurl'], HTTPServlet::FileHandler, destination) 186 | t = Thread.new { 187 | s.start 188 | } 189 | 190 | trap("INT") { s.shutdown } 191 | t.join() 192 | end 193 | -------------------------------------------------------------------------------- /test/test_site.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestSite < Test::Unit::TestCase 4 | context "creating sites" do 5 | setup do 6 | stub(Jekyll).configuration do 7 | Jekyll::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir}) 8 | end 9 | @site = Site.new(Jekyll.configuration) 10 | end 11 | 12 | should "have an empty tag hash by default" do 13 | assert_equal Hash.new, @site.tags 14 | end 15 | 16 | should "reset data before processing" do 17 | clear_dest 18 | @site.process 19 | before_posts = @site.posts.length 20 | before_layouts = @site.layouts.length 21 | before_categories = @site.categories.length 22 | before_tags = @site.tags.length 23 | before_pages = @site.pages.length 24 | before_static_files = @site.static_files.length 25 | before_time = @site.time 26 | 27 | @site.process 28 | assert_equal before_posts, @site.posts.length 29 | assert_equal before_layouts, @site.layouts.length 30 | assert_equal before_categories, @site.categories.length 31 | assert_equal before_tags, @site.tags.length 32 | assert_equal before_pages, @site.pages.length 33 | assert_equal before_static_files, @site.static_files.length 34 | assert before_time <= @site.time 35 | end 36 | 37 | should "write only modified static files" do 38 | clear_dest 39 | StaticFile.reset_cache 40 | 41 | @site.process 42 | some_static_file = @site.static_files[0].path 43 | dest = File.expand_path(@site.static_files[0].destination(@site.dest)) 44 | mtime1 = File.stat(dest).mtime.to_i # first run must generate dest file 45 | 46 | # need to sleep because filesystem timestamps have best resolution in seconds 47 | sleep 1 48 | @site.process 49 | mtime2 = File.stat(dest).mtime.to_i 50 | assert_equal mtime1, mtime2 51 | 52 | # simulate file modification by user 53 | FileUtils.touch some_static_file 54 | 55 | sleep 1 56 | @site.process 57 | mtime3 = File.stat(dest).mtime.to_i 58 | assert_not_equal mtime2, mtime3 # must be regenerated! 59 | 60 | sleep 1 61 | @site.process 62 | mtime4 = File.stat(dest).mtime.to_i 63 | assert_equal mtime3, mtime4 # no modifications, so must be the same 64 | end 65 | 66 | should "write static files if not modified but missing in destination" do 67 | clear_dest 68 | StaticFile.reset_cache 69 | 70 | @site.process 71 | some_static_file = @site.static_files[0].path 72 | dest = File.expand_path(@site.static_files[0].destination(@site.dest)) 73 | mtime1 = File.stat(dest).mtime.to_i # first run must generate dest file 74 | 75 | # need to sleep because filesystem timestamps have best resolution in seconds 76 | sleep 1 77 | @site.process 78 | mtime2 = File.stat(dest).mtime.to_i 79 | assert_equal mtime1, mtime2 80 | 81 | # simulate destination file deletion 82 | File.unlink dest 83 | 84 | sleep 1 85 | @site.process 86 | mtime3 = File.stat(dest).mtime.to_i 87 | assert_not_equal mtime2, mtime3 # must be regenerated and differ! 88 | 89 | sleep 1 90 | @site.process 91 | mtime4 = File.stat(dest).mtime.to_i 92 | assert_equal mtime3, mtime4 # no modifications, so must be the same 93 | end 94 | 95 | should "read layouts" do 96 | @site.read_layouts 97 | assert_equal ["default", "simple"].sort, @site.layouts.keys.sort 98 | end 99 | 100 | should "read posts" do 101 | @site.read_posts('') 102 | posts = Dir[source_dir('_posts', '*')] 103 | assert_equal posts.size - 1, @site.posts.size 104 | end 105 | 106 | should "deploy payload" do 107 | clear_dest 108 | @site.process 109 | 110 | posts = Dir[source_dir("**", "_posts", "*")] 111 | categories = %w(bar baz category foo z_category publish_test win).sort 112 | 113 | assert_equal posts.size - 1, @site.posts.size 114 | assert_equal categories, @site.categories.keys.sort 115 | assert_equal 4, @site.categories['foo'].size 116 | end 117 | 118 | should "filter entries" do 119 | ent1 = %w[foo.markdown bar.markdown baz.markdown #baz.markdown# 120 | .baz.markdow foo.markdown~] 121 | ent2 = %w[.htaccess _posts _pages bla.bla] 122 | 123 | assert_equal %w[foo.markdown bar.markdown baz.markdown], @site.filter_entries(ent1) 124 | assert_equal %w[.htaccess bla.bla], @site.filter_entries(ent2) 125 | end 126 | 127 | should "filter entries with exclude" do 128 | excludes = %w[README TODO] 129 | includes = %w[index.html site.css] 130 | 131 | @site.exclude = excludes 132 | assert_equal includes, @site.filter_entries(excludes + includes) 133 | end 134 | 135 | context 'with orphaned files in destination' do 136 | setup do 137 | clear_dest 138 | @site.process 139 | # generate some orphaned files: 140 | # hidden file 141 | File.open(dest_dir('.htpasswd'), 'w') 142 | # single file 143 | File.open(dest_dir('obsolete.html'), 'w') 144 | # single file in sub directory 145 | FileUtils.mkdir(dest_dir('qux')) 146 | File.open(dest_dir('qux/obsolete.html'), 'w') 147 | # empty directory 148 | FileUtils.mkdir(dest_dir('quux')) 149 | end 150 | 151 | teardown do 152 | FileUtils.rm_f(dest_dir('.htpasswd')) 153 | FileUtils.rm_f(dest_dir('obsolete.html')) 154 | FileUtils.rm_rf(dest_dir('qux')) 155 | FileUtils.rm_f(dest_dir('quux')) 156 | end 157 | 158 | should 'remove orphaned files in destination' do 159 | @site.process 160 | assert !File.exist?(dest_dir('.htpasswd')) 161 | assert !File.exist?(dest_dir('obsolete.html')) 162 | assert !File.exist?(dest_dir('qux')) 163 | assert !File.exist?(dest_dir('quux')) 164 | end 165 | 166 | end 167 | 168 | context 'with an invalid markdown processor in the configuration' do 169 | should 'not throw an error at initialization time' do 170 | bad_processor = 'not a processor name' 171 | assert_nothing_raised do 172 | Site.new(Jekyll.configuration.merge({ 'markdown' => bad_processor })) 173 | end 174 | end 175 | 176 | should 'throw FatalException at process time' do 177 | bad_processor = 'not a processor name' 178 | s = Site.new(Jekyll.configuration.merge({ 'markdown' => bad_processor })) 179 | assert_raise Jekyll::FatalException do 180 | s.process 181 | end 182 | end 183 | end 184 | 185 | end 186 | end 187 | -------------------------------------------------------------------------------- /features/site_configuration.feature: -------------------------------------------------------------------------------- 1 | Feature: Site configuration 2 | As a hacker who likes to blog 3 | I want to be able to configure jekyll 4 | In order to make setting up a site easier 5 | 6 | Scenario: Change destination directory 7 | Given I have a blank site in "_sourcedir" 8 | And I have an "_sourcedir/index.html" file that contains "Changing source directory" 9 | And I have a configuration file with "source" set to "_sourcedir" 10 | When I run jekyll 11 | Then the _site directory should exist 12 | And I should see "Changing source directory" in "_site/index.html" 13 | 14 | Scenario: Change destination directory 15 | Given I have an "index.html" file that contains "Changing destination directory" 16 | And I have a configuration file with "destination" set to "_mysite" 17 | When I run jekyll 18 | Then the _mysite directory should exist 19 | And I should see "Changing destination directory" in "_mysite/index.html" 20 | 21 | Scenario: Exclude files inline 22 | Given I have an "Rakefile" file that contains "I want to be excluded" 23 | And I have an "README" file that contains "I want to be excluded" 24 | And I have an "index.html" file that contains "I want to be included" 25 | And I have a configuration file with "exclude" set to "Rakefile", "README" 26 | When I run jekyll 27 | Then I should see "I want to be included" in "_site/index.html" 28 | And the "_site/Rakefile" file should not exist 29 | And the "_site/README" file should not exist 30 | 31 | Scenario: Exclude files with YAML array 32 | Given I have an "Rakefile" file that contains "I want to be excluded" 33 | And I have an "README" file that contains "I want to be excluded" 34 | And I have an "index.html" file that contains "I want to be included" 35 | And I have a configuration file with "exclude" set to: 36 | | value | 37 | | README | 38 | | Rakefile | 39 | When I run jekyll 40 | Then I should see "I want to be included" in "_site/index.html" 41 | And the "_site/Rakefile" file should not exist 42 | And the "_site/README" file should not exist 43 | 44 | Scenario: Use RDiscount for markup 45 | Given I have an "index.markdown" page that contains "[Google](http://google.com)" 46 | And I have a configuration file with "markdown" set to "rdiscount" 47 | When I run jekyll 48 | Then the _site directory should exist 49 | And I should see "Google" in "_site/index.html" 50 | 51 | Scenario: Use Kramdown for markup 52 | Given I have an "index.markdown" page that contains "[Google](http://google.com)" 53 | And I have a configuration file with "markdown" set to "kramdown" 54 | When I run jekyll 55 | Then the _site directory should exist 56 | And I should see "Google" in "_site/index.html" 57 | 58 | Scenario: Use Maruku for markup 59 | Given I have an "index.markdown" page that contains "[Google](http://google.com)" 60 | And I have a configuration file with "markdown" set to "maruku" 61 | When I run jekyll 62 | Then the _site directory should exist 63 | And I should see "Google" in "_site/index.html" 64 | 65 | Scenario: Highlight code with pygments 66 | Given I have an "index.html" file that contains "{% highlight ruby %} puts 'Hello world!' {% endhighlight %}" 67 | And I have a configuration file with "pygments" set to "true" 68 | When I run jekyll 69 | Then the _site directory should exist 70 | And I should see "puts 'Hello world!'" in "_site/index.html" 71 | 72 | Scenario: Set time and no future dated posts 73 | Given I have a _layouts directory 74 | And I have a page layout that contains "Page Layout: {{ site.posts.size }} on {{ site.time | date: "%Y-%m-%d" }}" 75 | And I have a post layout that contains "Post Layout: {{ content }}" 76 | And I have an "index.html" page with layout "page" that contains "site index page" 77 | And I have a configuration file with: 78 | | key | value | 79 | | time | 2010-01-01 | 80 | | future | false | 81 | And I have a _posts directory 82 | And I have the following posts: 83 | | title | date | layout | content | 84 | | entry1 | 12/31/2007 | post | content for entry1. | 85 | | entry2 | 01/31/2020 | post | content for entry2. | 86 | When I run jekyll 87 | Then the _site directory should exist 88 | And I should see "Page Layout: 1 on 2010-01-01" in "_site/index.html" 89 | And I should see "Post Layout:

content for entry1.

" in "_site/2007/12/31/entry1.html" 90 | And the "_site/2020/01/31/entry2.html" file should not exist 91 | 92 | Scenario: Set time and future dated posts allowed 93 | Given I have a _layouts directory 94 | And I have a page layout that contains "Page Layout: {{ site.posts.size }} on {{ site.time | date: "%Y-%m-%d" }}" 95 | And I have a post layout that contains "Post Layout: {{ content }}" 96 | And I have an "index.html" page with layout "page" that contains "site index page" 97 | And I have a configuration file with: 98 | | key | value | 99 | | time | 2010-01-01 | 100 | | future | true | 101 | And I have a _posts directory 102 | And I have the following posts: 103 | | title | date | layout | content | 104 | | entry1 | 12/31/2007 | post | content for entry1. | 105 | | entry2 | 01/31/2020 | post | content for entry2. | 106 | When I run jekyll 107 | Then the _site directory should exist 108 | And I should see "Page Layout: 2 on 2010-01-01" in "_site/index.html" 109 | And I should see "Post Layout:

content for entry1.

" in "_site/2007/12/31/entry1.html" 110 | And I should see "Post Layout:

content for entry2.

" in "_site/2020/01/31/entry2.html" 111 | 112 | Scenario: Limit the number of posts generated by most recent date 113 | Given I have a _posts directory 114 | And I have a configuration file with: 115 | | key | value | 116 | | limit_posts | 2 | 117 | And I have the following posts: 118 | | title | date | content | 119 | | Apples | 3/27/2009 | An article about apples | 120 | | Oranges | 4/1/2009 | An article about oranges | 121 | | Bananas | 4/5/2009 | An article about bananas | 122 | When I run jekyll 123 | Then the _site directory should exist 124 | And the "_site/2009/04/05/bananas.html" file should exist 125 | And the "_site/2009/04/01/oranges.html" file should exist 126 | And the "_site/2009/03/27/apples.html" file should not exist 127 | -------------------------------------------------------------------------------- /lib/jekyll/post.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | 3 | class Post 4 | include Comparable 5 | include Convertible 6 | 7 | class << self 8 | attr_accessor :lsi 9 | end 10 | 11 | MATCHER = /^(.+\/)*(\d+-\d+-\d+-)?(.*)(\.[^.]+)$/ 12 | 13 | # Post name validator. Post filenames must be like: 14 | # 2008-11-05-my-awesome-post.textile 15 | # 16 | # Returns 17 | def self.valid?(name) 18 | name =~ MATCHER 19 | end 20 | 21 | attr_accessor :site 22 | attr_accessor :data, :content, :output, :ext 23 | attr_accessor :date, :slug, :published, :tags, :categories 24 | 25 | # Initialize this Post instance. 26 | # +site+ is the Site 27 | # +base+ is the String path to the dir containing the post file 28 | # +name+ is the String filename of the post file 29 | # +categories+ is an Array of Strings for the categories for this post 30 | # 31 | # Returns 32 | def initialize(site, source, dir, name) 33 | @site = site 34 | @base = File.join(source, dir, '_posts') 35 | @name = name 36 | 37 | self.categories = @name.split('/').tap{|o| o.pop}.reject { |x| x.empty? } 38 | self.process(name) 39 | self.read_yaml(@base, name) 40 | self.data['layout'] ||= 'post' 41 | 42 | #If we've added a date and time to the yaml, use that instead of the filename date 43 | #Means we'll sort correctly. 44 | if self.data.has_key?('date') 45 | # ensure Time via to_s and reparse 46 | self.date = Time.parse(self.data["date"].to_s.gsub(/-$/,"")) 47 | end 48 | 49 | if self.data.has_key?('published') && self.data['published'] == false 50 | self.published = false 51 | else 52 | self.published = true 53 | end 54 | 55 | self.tags = self.data.pluralized_array("tag", "tags") 56 | 57 | if self.categories.empty? 58 | self.categories = self.data.pluralized_array('category', 'categories') 59 | end 60 | end 61 | 62 | # Spaceship is based on Post#date, slug 63 | # 64 | # Returns -1, 0, 1 65 | def <=>(other) 66 | if self.date && other.date 67 | cmp = self.date <=> other.date 68 | else 69 | cmp = self.slug <=> other.slug 70 | end 71 | if 0 == cmp 72 | cmp = self.slug <=> other.slug 73 | end 74 | return cmp 75 | end 76 | 77 | # Extract information from the post filename 78 | # +name+ is the String filename of the post file 79 | # 80 | # Returns nothing 81 | def process(name) 82 | m, cats, date, slug, ext = *name.match(MATCHER) 83 | self.date = Time.parse(date) if date 84 | self.slug = slug 85 | self.ext = ext 86 | end 87 | 88 | # The generated directory into which the post will be placed 89 | # upon generation. This is derived from the permalink or, if 90 | # permalink is absent, set to the default date 91 | # e.g. "/2008/11/05/" if the permalink style is :date, otherwise nothing 92 | # 93 | # Returns 94 | def dir 95 | File.dirname(url) 96 | end 97 | 98 | # The full path and filename of the post. 99 | # Defined in the YAML of the post body 100 | # (Optional) 101 | # 102 | # Returns 103 | def permalink 104 | self.data && self.data['permalink'] 105 | end 106 | 107 | def template 108 | case self.site.permalink_style 109 | when :pretty 110 | "/:categories/:year/:month/:day/:title/" 111 | when :none 112 | "/:categories/:title.html" 113 | when :date 114 | "/:categories/:year/:month/:day/:title.html" 115 | else 116 | self.site.permalink_style.to_s 117 | end 118 | end 119 | 120 | # The generated relative url of this post 121 | # e.g. /2008/11/05/my-awesome-post.html 122 | # 123 | # Returns 124 | def url 125 | return permalink if permalink 126 | 127 | @url ||= { 128 | "year" => (date ? date.strftime("%Y") : nil).to_s, 129 | "month" => (date ? date.strftime("%m") : nil).to_s, 130 | "day" => (date ? date.strftime("%d") : nil).to_s, 131 | "title" => CGI.escape(slug), 132 | "i_day" => (date ? date.strftime("%d").to_i.to_s : nil).to_s, 133 | "i_month" => (date ? date.strftime("%m").to_i.to_s : nil).to_s, 134 | "categories" => categories.join('/'), 135 | "output_ext" => self.output_ext 136 | }.inject(template) { |result, token| 137 | result.gsub(/:#{Regexp.escape token.first}/, token.last) 138 | }.gsub(/\/\//, "/") 139 | end 140 | 141 | # The UID for this post (useful in feeds) 142 | # e.g. /2008/11/05/my-awesome-post 143 | # 144 | # Returns 145 | def id 146 | File.join(self.dir, self.slug) 147 | end 148 | 149 | # Calculate related posts. 150 | # 151 | # Returns [] 152 | def related_posts(posts) 153 | return [] unless posts.size > 1 154 | 155 | if self.site.lsi 156 | self.class.lsi ||= begin 157 | puts "Running the classifier... this could take a while." 158 | lsi = Classifier::LSI.new 159 | posts.each { |x| $stdout.print(".");$stdout.flush;lsi.add_item(x) } 160 | puts "" 161 | lsi 162 | end 163 | 164 | related = self.class.lsi.find_related(self.content, 11) 165 | related - [self] 166 | else 167 | (posts - [self])[0..9] 168 | end 169 | end 170 | 171 | # Add any necessary layouts to this post 172 | # +layouts+ is a Hash of {"name" => "layout"} 173 | # +site_payload+ is the site payload hash 174 | # 175 | # Returns nothing 176 | def render(layouts, site_payload) 177 | # construct payload 178 | payload = { 179 | "site" => { "related_posts" => related_posts(site_payload["site"]["posts"]) }, 180 | "page" => self.to_liquid 181 | }.deep_merge(site_payload) 182 | 183 | do_layout(payload, layouts) 184 | end 185 | 186 | # Obtain destination path. 187 | # +dest+ is the String path to the destination dir 188 | # 189 | # Returns destination file path. 190 | def destination(dest) 191 | # The url needs to be unescaped in order to preserve the correct filename 192 | path = File.join(dest, CGI.unescape(self.url)) 193 | path = File.join(path, "index.html") if template[/\.html$/].nil? 194 | path 195 | end 196 | 197 | # Write the generated post file to the destination directory. 198 | # +dest+ is the String path to the destination dir 199 | # 200 | # Returns nothing 201 | def write(dest) 202 | path = destination(dest) 203 | FileUtils.mkdir_p(File.dirname(path)) 204 | File.open(path, 'w') do |f| 205 | f.write(self.output) 206 | end 207 | end 208 | 209 | # Convert this post into a Hash for use in Liquid templates. 210 | # 211 | # Returns 212 | def to_liquid 213 | self.data.deep_merge({ 214 | "title" => self.data["title"] || self.slug.split('-').select {|w| w.capitalize! || w }.join(' '), 215 | "url" => self.url, 216 | "date" => self.date, 217 | "id" => self.id, 218 | "categories" => self.categories, 219 | "next" => self.next, 220 | "previous" => self.previous, 221 | "tags" => self.tags, 222 | "content" => self.content }) 223 | end 224 | 225 | def inspect 226 | "" 227 | end 228 | 229 | def next 230 | pos = self.site.posts.index(self) 231 | 232 | if pos && pos < self.site.posts.length-1 233 | self.site.posts[pos+1] 234 | else 235 | nil 236 | end 237 | end 238 | 239 | def previous 240 | pos = self.site.posts.index(self) 241 | if pos && pos > 0 242 | self.site.posts[pos-1] 243 | else 244 | nil 245 | end 246 | end 247 | end 248 | 249 | end 250 | -------------------------------------------------------------------------------- /features/post_data.feature: -------------------------------------------------------------------------------- 1 | Feature: Post data 2 | As a hacker who likes to blog 3 | I want to be able to embed data into my posts 4 | In order to make the posts slightly dynamic 5 | 6 | Scenario: Use post.title variable 7 | Given I have a _posts directory 8 | And I have a _layouts directory 9 | And I have the following post: 10 | | title | date | layout | content | 11 | | Star Wars | 3/27/2009 | simple | Luke, I am your father. | 12 | And I have a simple layout that contains "Post title: {{ page.title }}" 13 | When I run jekyll 14 | Then the _site directory should exist 15 | And I should see "Post title: Star Wars" in "_site/2009/03/27/star-wars.html" 16 | 17 | Scenario: Use post.url variable 18 | Given I have a _posts directory 19 | And I have a _layouts directory 20 | And I have the following post: 21 | | title | date | layout | content | 22 | | Star Wars | 3/27/2009 | simple | Luke, I am your father. | 23 | And I have a simple layout that contains "Post url: {{ page.url }}" 24 | When I run jekyll 25 | Then the _site directory should exist 26 | And I should see "Post url: /2009/03/27/star-wars.html" in "_site/2009/03/27/star-wars.html" 27 | 28 | Scenario: Use post.date variable 29 | Given I have a _posts directory 30 | And I have a _layouts directory 31 | And I have the following post: 32 | | title | date | layout | content | 33 | | Star Wars | 3/27/2009 | simple | Luke, I am your father. | 34 | And I have a simple layout that contains "Post date: {{ page.date }}" 35 | When I run jekyll 36 | Then the _site directory should exist 37 | And I should see "Post date: Fri Mar 27" in "_site/2009/03/27/star-wars.html" 38 | 39 | Scenario: Use post.id variable 40 | Given I have a _posts directory 41 | And I have a _layouts directory 42 | And I have the following post: 43 | | title | date | layout | content | 44 | | Star Wars | 3/27/2009 | simple | Luke, I am your father. | 45 | And I have a simple layout that contains "Post id: {{ page.id }}" 46 | When I run jekyll 47 | Then the _site directory should exist 48 | And I should see "Post id: /2009/03/27/star-wars" in "_site/2009/03/27/star-wars.html" 49 | 50 | Scenario: Use post.content variable 51 | Given I have a _posts directory 52 | And I have a _layouts directory 53 | And I have the following post: 54 | | title | date | layout | content | 55 | | Star Wars | 3/27/2009 | simple | Luke, I am your father. | 56 | And I have a simple layout that contains "Post content: {{ content }}" 57 | When I run jekyll 58 | Then the _site directory should exist 59 | And I should see "Post content:

Luke, I am your father.

" in "_site/2009/03/27/star-wars.html" 60 | 61 | Scenario: Use post.categories variable when category is in a folder 62 | Given I have a movies directory 63 | And I have a movies/_posts directory 64 | And I have a _layouts directory 65 | And I have the following post in "movies": 66 | | title | date | layout | content | 67 | | Star Wars | 3/27/2009 | simple | Luke, I am your father. | 68 | And I have a simple layout that contains "Post category: {{ page.categories }}" 69 | When I run jekyll 70 | Then the _site directory should exist 71 | And I should see "Post category: movies" in "_site/movies/2009/03/27/star-wars.html" 72 | 73 | Scenario: Use post.tags variable 74 | Given I have a _posts directory 75 | And I have a _layouts directory 76 | And I have the following post: 77 | | title | date | layout | tag | content | 78 | | Star Wars | 5/18/2009 | simple | twist | Luke, I am your father. | 79 | And I have a simple layout that contains "Post tags: {{ page.tags }}" 80 | When I run jekyll 81 | Then the _site directory should exist 82 | And I should see "Post tags: twist" in "_site/2009/05/18/star-wars.html" 83 | 84 | Scenario: Use post.categories variable when categories are in folders 85 | Given I have a scifi directory 86 | And I have a scifi/movies directory 87 | And I have a scifi/movies/_posts directory 88 | And I have a _layouts directory 89 | And I have the following post in "scifi/movies": 90 | | title | date | layout | content | 91 | | Star Wars | 3/27/2009 | simple | Luke, I am your father. | 92 | And I have a simple layout that contains "Post categories: {{ page.categories | array_to_sentence_string }}" 93 | When I run jekyll 94 | Then the _site directory should exist 95 | And I should see "Post categories: scifi and movies" in "_site/scifi/movies/2009/03/27/star-wars.html" 96 | 97 | Scenario: Use post.categories variable when category is in YAML 98 | Given I have a _posts directory 99 | And I have a _layouts directory 100 | And I have the following post: 101 | | title | date | layout | category | content | 102 | | Star Wars | 3/27/2009 | simple | movies | Luke, I am your father. | 103 | And I have a simple layout that contains "Post category: {{ page.categories }}" 104 | When I run jekyll 105 | Then the _site directory should exist 106 | And I should see "Post category: movies" in "_site/movies/2009/03/27/star-wars.html" 107 | 108 | Scenario: Use post.categories variable when categories are in YAML 109 | Given I have a _posts directory 110 | And I have a _layouts directory 111 | And I have the following post: 112 | | title | date | layout | categories | content | 113 | | Star Wars | 3/27/2009 | simple | ['scifi', 'movies'] | Luke, I am your father. | 114 | And I have a simple layout that contains "Post categories: {{ page.categories | array_to_sentence_string }}" 115 | When I run jekyll 116 | Then the _site directory should exist 117 | And I should see "Post categories: scifi and movies" in "_site/scifi/movies/2009/03/27/star-wars.html" 118 | 119 | Scenario: Disable a post from being published 120 | Given I have a _posts directory 121 | And I have an "index.html" file that contains "Published!" 122 | And I have the following post: 123 | | title | date | layout | published | content | 124 | | Star Wars | 3/27/2009 | simple | false | Luke, I am your father. | 125 | When I run jekyll 126 | Then the _site directory should exist 127 | And the "_site/2009/03/27/star-wars.html" file should not exist 128 | And I should see "Published!" in "_site/index.html" 129 | 130 | Scenario: Use a custom variable 131 | Given I have a _posts directory 132 | And I have a _layouts directory 133 | And I have the following post: 134 | | title | date | layout | author | content | 135 | | Star Wars | 3/27/2009 | simple | Darth Vader | Luke, I am your father. | 136 | And I have a simple layout that contains "Post author: {{ page.author }}" 137 | When I run jekyll 138 | Then the _site directory should exist 139 | And I should see "Post author: Darth Vader" in "_site/2009/03/27/star-wars.html" 140 | 141 | Scenario: Previous and next posts title 142 | Given I have a _posts directory 143 | And I have a _layouts directory 144 | And I have the following posts: 145 | | title | date | layout | author | content | 146 | | Star Wars | 3/27/2009 | ordered | Darth Vader | Luke, I am your father. | 147 | | Some like it hot | 4/27/2009 | ordered | Osgood | Nobody is perfect. | 148 | | Terminator | 5/27/2009 | ordered | Arnold | Sayonara, baby | 149 | And I have a ordered layout that contains "Previous post: {{ page.previous.title }} and next post: {{ page.next.title }}" 150 | When I run jekyll 151 | Then the _site directory should exist 152 | And I should see "next post: Some like it hot" in "_site/2009/03/27/star-wars.html" 153 | And I should see "Previous post: Some like it hot" in "_site/2009/05/27/terminator.html" 154 | -------------------------------------------------------------------------------- /lib/jekyll/site.rb: -------------------------------------------------------------------------------- 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 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 //_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 //_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 => []} 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" =>