├── 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-03-12-hash-#1.markdown │ │ ├── 2008-10-18-foo-bar.textile │ │ ├── 2008-12-13-include.markdown │ │ ├── 2009-01-27-category.textile │ │ ├── 2009-05-18-tags.textile │ │ ├── 2009-01-27-categories.textile │ │ ├── 2008-02-02-published.textile │ │ ├── 2008-11-21-complex.textile │ │ ├── 2008-02-02-not-published.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 │ ├── z_category │ │ └── _posts │ │ │ └── 2008-9-23-categories.textile │ ├── foo │ │ └── _posts │ │ │ └── bar │ │ │ └── 2008-12-12-topical-post.textile │ ├── index.html │ ├── sitemap.xml │ └── css │ │ └── screen.css ├── suite.rb ├── helper.rb ├── test_configuration.rb ├── test_generated_site.rb ├── test_pager.rb ├── test_filters.rb ├── test_site.rb ├── test_page.rb ├── test_tags.rb └── test_post.rb ├── VERSION.yml ├── .gitignore ├── features ├── support │ └── env.rb ├── pagination.feature ├── create_sites.feature ├── embed_filters.feature ├── site_configuration.feature ├── permalinks.feature ├── step_definitions │ └── jekyll_steps.rb ├── site_data.feature └── post_data.feature ├── lib ├── jekyll │ ├── converters │ │ ├── csv.rb │ │ ├── typo.rb │ │ ├── textpattern.rb │ │ ├── wordpress.rb │ │ ├── mt.rb │ │ └── mephisto.rb │ ├── core_ext.rb │ ├── layout.rb │ ├── tags │ │ ├── include.rb │ │ └── highlight.rb │ ├── filters.rb │ ├── pager.rb │ ├── convertible.rb │ ├── page.rb │ ├── albino.rb │ ├── post.rb │ └── site.rb └── jekyll.rb ├── Rakefile ├── README.textile ├── bin └── jekyll ├── jekyll.gemspec └── History.txt /test/source/_layouts/simple.html: -------------------------------------------------------------------------------- 1 | <<< {{ content }} >>> -------------------------------------------------------------------------------- /test/source/_posts/2009-06-22-no-yaml.textile: -------------------------------------------------------------------------------- 1 | No YAML. -------------------------------------------------------------------------------- /VERSION.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :patch: 4 3 | :major: 0 4 | :minor: 5 5 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-05-18-tags.textile: -------------------------------------------------------------------------------- 1 | --- 2 | title: Some Tags 3 | tags: 4 | - food 5 | - cooking 6 | - pizza 7 | --- 8 | 9 | Awesome! 10 | -------------------------------------------------------------------------------- /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/_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/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/_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/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 -------------------------------------------------------------------------------- /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/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 'test/unit' 7 | require 'redgreen' 8 | require 'shoulda' 9 | require 'rr' 10 | 11 | include Jekyll 12 | 13 | class Test::Unit::TestCase 14 | include RR::Adapters::TestUnit 15 | 16 | def dest_dir(*subdirs) 17 | File.join(File.dirname(__FILE__), 'dest', *subdirs) 18 | end 19 | 20 | def source_dir(*subdirs) 21 | File.join(File.dirname(__FILE__), 'source', *subdirs) 22 | end 23 | 24 | def clear_dest 25 | FileUtils.rm_rf(dest_dir) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/jekyll/converters/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 -------------------------------------------------------------------------------- /test/source/sitemap.xml: -------------------------------------------------------------------------------- 1 | --- 2 | layout: nil 3 | --- 4 | 5 | 7 | 8 | 9 | http://example.com 10 | {{ site.time | date_to_xmlschema }} 11 | daily 12 | 1.0 13 | 14 | 15 | {% for post in site.posts %} 16 | 17 | http://example.com/{{ post.url }}/ 18 | {{ site.time }} 19 | monthly 20 | 0.2 21 | 22 | {% endfor %} 23 | -------------------------------------------------------------------------------- /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 | end 23 | 24 | # Thanks, ActiveSupport! 25 | class Date 26 | # Converts datetime to an appropriate format for use in XML 27 | def xmlschema 28 | strftime("%Y-%m-%dT%H:%M:%S%Z") 29 | end if RUBY_VERSION < '1.9' 30 | end 31 | -------------------------------------------------------------------------------- /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/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 | if @file !~ /^[a-zA-Z0-9_\/\.-]+$/ || @file =~ /\.\// || @file =~ /\/\./ 11 | return "Include file '#{@file}' contains invalid characters or sequences" 12 | end 13 | 14 | Dir.chdir(File.join(context.registers[:site].source, '_includes')) do 15 | choices = Dir['**/*'].reject { |x| File.symlink?(x) } 16 | if choices.include?(@file) 17 | source = File.read(@file) 18 | partial = Liquid::Template.parse(source) 19 | context.stack do 20 | partial.render(context) 21 | end 22 | else 23 | "Included file '#{@file}' not found in _includes directory" 24 | end 25 | end 26 | end 27 | end 28 | 29 | end 30 | 31 | Liquid::Template.register_tag('include', Jekyll::IncludeTag) 32 | -------------------------------------------------------------------------------- /lib/jekyll/filters.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | 3 | module Filters 4 | def textilize(input) 5 | RedCloth.new(input).to_html 6 | end 7 | 8 | def date_to_string(date) 9 | date.strftime("%d %b %Y") 10 | end 11 | 12 | def date_to_long_string(date) 13 | date.strftime("%d %B %Y") 14 | end 15 | 16 | def date_to_xmlschema(date) 17 | date.xmlschema 18 | end 19 | 20 | def xml_escape(input) 21 | CGI.escapeHTML(input) 22 | end 23 | 24 | def cgi_escape(input) 25 | CGI::escape(input) 26 | end 27 | 28 | def number_of_words(input) 29 | input.split.length 30 | end 31 | 32 | def array_to_sentence_string(array) 33 | connector = "and" 34 | case array.length 35 | when 0 36 | "" 37 | when 1 38 | array[0].to_s 39 | when 2 40 | "#{array[0]} #{connector} #{array[1]}" 41 | else 42 | "#{array[0...-1].join(', ')}, #{connector} #{array[-1]}" 43 | end 44 | end 45 | 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /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 = './_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/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 "insert site.posts into the index" do 17 | assert @index.include?("#{@site.posts.size} Posts") 18 | end 19 | 20 | should "render latest post's content" do 21 | assert @index.include?(@site.posts.last.content) 22 | end 23 | 24 | should "hide unpublished posts" do 25 | published = Dir[dest_dir('publish_test/2008/02/02/*.html')].map {|f| File.basename(f)} 26 | 27 | assert_equal 1, published.size 28 | assert_equal "published.html", published.first 29 | end 30 | 31 | should "not copy _posts directory" do 32 | assert !File.exist?(dest_dir('_posts')) 33 | end 34 | 35 | should "process other static files and generate correct permalinks" do 36 | assert File.exists?(dest_dir('/about/index.html')) 37 | assert File.exists?(dest_dir('/contacts.html')) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /test/test_pager.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestPager < Test::Unit::TestCase 4 | context "pagination enabled" do 5 | setup do 6 | stub(Jekyll).configuration do 7 | Jekyll::DEFAULTS.merge({ 8 | 'source' => source_dir, 9 | 'destination' => dest_dir, 10 | 'paginate' => 2 11 | }) 12 | end 13 | 14 | @config = Jekyll.configuration 15 | @site = Site.new(@config) 16 | @posts = @site.read_posts('') 17 | end 18 | 19 | should "calculate number of pages" do 20 | assert_equal(2, Pager.calculate_pages(@posts, @config['paginate'])) 21 | end 22 | 23 | should "create first pager" do 24 | pager = Pager.new(@config, 1, @posts) 25 | assert_equal(@config['paginate'].to_i, pager.posts.size) 26 | assert_equal(2, pager.total_pages) 27 | assert_nil(pager.previous_page) 28 | assert_equal(2, pager.next_page) 29 | end 30 | 31 | should "create second pager" do 32 | pager = Pager.new(@config, 2, @posts) 33 | assert_equal(@posts.size - @config['paginate'].to_i, pager.posts.size) 34 | assert_equal(2, pager.total_pages) 35 | assert_equal(1, pager.previous_page) 36 | assert_nil(pager.next_page) 37 | end 38 | 39 | should "not create third pager" do 40 | assert_raise(RuntimeError) { Pager.new(@config, 3, @posts) } 41 | end 42 | 43 | should "report that pagination is enabled" do 44 | assert Pager.pagination_enabled?(@config, 'index.html') 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/jekyll/pager.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | class Pager 3 | attr_reader :page, :per_page, :posts, :total_posts, :total_pages, :previous_page, :next_page 4 | 5 | def self.calculate_pages(all_posts, per_page) 6 | num_pages = all_posts.size / per_page.to_i 7 | num_pages.abs + 1 if all_posts.size % per_page.to_i != 0 8 | num_pages 9 | end 10 | 11 | def self.pagination_enabled?(config, file) 12 | file == 'index.html' && !config['paginate'].nil? 13 | end 14 | 15 | def initialize(config, page, all_posts, num_pages = nil) 16 | @page = page 17 | @per_page = config['paginate'].to_i 18 | @total_pages = num_pages || Pager.calculate_pages(all_posts, @per_page) 19 | 20 | if @page > @total_pages 21 | raise RuntimeError, "page number can't be greater than total pages: #{@page} > #{@total_pages}" 22 | end 23 | 24 | init = (@page - 1) * @per_page 25 | offset = (init + @per_page - 1) >= all_posts.size ? all_posts.size : (init + @per_page - 1) 26 | 27 | @total_posts = all_posts.size 28 | @posts = all_posts[init..offset] 29 | @previous_page = @page != 1 ? @page - 1 : nil 30 | @next_page = @page != @total_pages ? @page + 1 : nil 31 | end 32 | 33 | def to_hash 34 | { 35 | 'page' => page, 36 | 'per_page' => per_page, 37 | 'posts' => posts, 38 | 'total_posts' => total_posts, 39 | 'total_pages' => total_pages, 40 | 'previous_page' => previous_page, 41 | 'next_page' => next_page 42 | } 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /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?(:?linenos)?\s?/ 8 | 9 | def initialize(tag_name, markup, tokens) 10 | super 11 | if markup =~ SYNTAX 12 | @lang = $1 13 | if defined? $2 14 | # additional options to pass to Albino. 15 | @options = { 'O' => 'linenos=inline' } 16 | else 17 | @options = {} 18 | end 19 | else 20 | raise SyntaxError.new("Syntax Error in 'highlight' - Valid syntax: highlight [linenos]") 21 | end 22 | end 23 | 24 | def render(context) 25 | if context.registers[:site].pygments 26 | render_pygments(context, super.to_s) 27 | else 28 | render_codehighlighter(context, super.to_s) 29 | end 30 | end 31 | 32 | def render_pygments(context, code) 33 | if context["content_type"] == "markdown" 34 | return "\n" + Albino.new(code, @lang).to_s(@options) + "\n" 35 | elsif context["content_type"] == "textile" 36 | return "" + Albino.new(code, @lang).to_s(@options) + "" 37 | else 38 | return Albino.new(code, @lang).to_s(@options) 39 | end 40 | end 41 | 42 | def render_codehighlighter(context, code) 43 | #The div is required because RDiscount blows ass 44 | <<-HTML 45 |
46 |
47 |     #{h(code).strip}
48 |   
49 |
50 | HTML 51 | end 52 | end 53 | 54 | end 55 | 56 | Liquid::Template.register_tag('highlight', Jekyll::HighlightBlock) 57 | -------------------------------------------------------------------------------- /lib/jekyll/converters/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 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 | -------------------------------------------------------------------------------- /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" file that contains "Basic Site" 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 play2. | 16 | When I run jekyll 17 | Then the _site/page2 directory should exist 18 | And the _site/page2/index.html file should exist 19 | 20 | Examples: 21 | | num | 22 | | 1 | 23 | | 2 | 24 | 25 | Scenario: Correct liquid paginator replacements 26 | Given I have a configuration file with "paginate" set to "1" 27 | And I have a _layouts directory 28 | And I have an "index.html" file that contains "{{ paginator.page }}" 29 | And I have a _posts directory 30 | And I have the following post: 31 | | title | date | layout | content | 32 | | Wargames | 3/27/2009 | default | The only winning move is not to play. | 33 | | Wargames2 | 4/27/2009 | default | The only winning move is not to play2. | 34 | When I run jekyll 35 | Then the _site/index.html file should exist 36 | And I should see "1" in "_site/index.html" 37 | Then the _site/page2 directory should exist 38 | And the _site/page2/index.html file should exist 39 | And I should see "2" in "_site/page2/index.html" 40 | -------------------------------------------------------------------------------- /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 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/jekyll/converters/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) 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/converters/wordpress.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 WordPress 13 | 14 | # Reads a MySQL database via Sequel and creates a post file for each 15 | # post in wp_posts that has post_status = 'publish'. 16 | # This restriction is made because 'draft' posts are not guaranteed to 17 | # have valid dates. 18 | 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'" 19 | 20 | def self.process(dbname, user, pass, host = 'localhost') 21 | db = Sequel.mysql(dbname, :user => user, :password => pass, :host => host) 22 | 23 | FileUtils.mkdir_p "_posts" 24 | 25 | db[QUERY].each do |post| 26 | # Get required fields and construct Jekyll compatible name 27 | title = post[:post_title] 28 | slug = post[:post_name] 29 | date = post[:post_date] 30 | content = post[:post_content] 31 | name = "%02d-%02d-%02d-%s.markdown" % [date.year, date.month, date.day, 32 | slug] 33 | 34 | # Get the relevant fields as a hash, delete empty fields and convert 35 | # to YAML for the header 36 | data = { 37 | 'layout' => 'post', 38 | 'title' => title.to_s, 39 | 'excerpt' => post[:post_excerpt].to_s, 40 | 'wordpress_id' => post[:ID], 41 | 'wordpress_url' => post[:guid] 42 | }.delete_if { |k,v| v.nil? || v == ''}.to_yaml 43 | 44 | # Write out the data and content to file 45 | File.open("_posts/#{name}", "w") do |f| 46 | f.puts data 47 | f.puts "---" 48 | f.puts content 49 | end 50 | end 51 | 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /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 post: 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 include tag 41 | Given I have a _includes directory 42 | And I have an "index.html" page that contains "Basic Site with include tag: {% include about.textile %}" 43 | And I have an "_includes/about.textile" file that contains "Generated by Jekyll" 44 | When I run jekyll 45 | Then the _site directory should exist 46 | And I should see "Basic Site with include tag: Generated by Jekyll" in "_site/index.html" 47 | -------------------------------------------------------------------------------- /lib/jekyll/converters/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 | 9 | # NOTE: This converter requires Sequel and the MySQL gems. 10 | # The MySQL gem can be difficult to install on OS X. Once you have MySQL 11 | # installed, running the following commands should work: 12 | # $ sudo gem install sequel 13 | # $ sudo gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config 14 | 15 | module Jekyll 16 | module MT 17 | # This query will pull blog posts from all entries across all blogs. If 18 | # you've got unpublished, deleted or otherwise hidden posts please sift 19 | # through the created posts to make sure nothing is accidently published. 20 | QUERY = "SELECT entry_id, entry_basename, entry_text, entry_text_more, entry_created_on, entry_title FROM mt_entry" 21 | 22 | def self.process(dbname, user, pass, host = 'localhost') 23 | db = Sequel.mysql(dbname, :user => user, :password => pass, :host => host) 24 | 25 | FileUtils.mkdir_p "_posts" 26 | 27 | db[QUERY].each do |post| 28 | title = post[:entry_title] 29 | slug = post[:entry_basename] 30 | date = post[:entry_created_on] 31 | content = post[:entry_text] 32 | more_content = post[:entry_text_more] 33 | 34 | # Be sure to include the body and extended body. 35 | if more_content != nil 36 | content = content + " \n" + more_content 37 | end 38 | 39 | # Ideally, this script would determine the post format (markdown, html 40 | # , etc) and create files with proper extensions. At this point it 41 | # just assumes that markdown will be acceptable. 42 | name = [date.year, date.month, date.day, slug].join('-') + ".markdown" 43 | 44 | data = { 45 | 'layout' => 'post', 46 | 'title' => title.to_s, 47 | 'mt_id' => post[:entry_id], 48 | }.delete_if { |k,v| v.nil? || v == ''}.to_yaml 49 | 50 | File.open("_posts/#{name}", "w") do |f| 51 | f.puts data 52 | f.puts "---" 53 | f.puts content 54 | end 55 | end 56 | 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/jekyll.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.dirname(__FILE__) # For use/testing when no gem is installed 2 | 3 | # rubygems 4 | require 'rubygems' 5 | 6 | # core 7 | require 'fileutils' 8 | require 'time' 9 | require 'yaml' 10 | 11 | # stdlib 12 | 13 | # 3rd party 14 | require 'liquid' 15 | require 'redcloth' 16 | 17 | # internal requires 18 | require 'jekyll/core_ext' 19 | require 'jekyll/pager' 20 | require 'jekyll/site' 21 | require 'jekyll/convertible' 22 | require 'jekyll/layout' 23 | require 'jekyll/page' 24 | require 'jekyll/post' 25 | require 'jekyll/filters' 26 | require 'jekyll/tags/highlight' 27 | require 'jekyll/tags/include' 28 | require 'jekyll/albino' 29 | 30 | module Jekyll 31 | # Default options. Overriden by values in _config.yml or command-line opts. 32 | # (Strings rather symbols used for compatability with YAML) 33 | DEFAULTS = { 34 | 'auto' => false, 35 | 'server' => false, 36 | 'server_port' => 4000, 37 | 38 | 'source' => '.', 39 | 'destination' => File.join('.', '_site'), 40 | 41 | 'lsi' => false, 42 | 'pygments' => false, 43 | 'markdown' => 'maruku', 44 | 'permalink' => 'date', 45 | 46 | 'maruku' => { 47 | 'use_tex' => false, 48 | 'use_divs' => false, 49 | 'png_engine' => 'blahtex', 50 | 'png_dir' => 'images/latex', 51 | 'png_url' => '/images/latex' 52 | } 53 | } 54 | 55 | # Generate a Jekyll configuration Hash by merging the default options 56 | # with anything in _config.yml, and adding the given options on top 57 | # +override+ is a Hash of config directives 58 | # 59 | # Returns Hash 60 | def self.configuration(override) 61 | # _config.yml may override default source location, but until 62 | # then, we need to know where to look for _config.yml 63 | source = override['source'] || Jekyll::DEFAULTS['source'] 64 | 65 | # Get configuration from /_config.yml 66 | config_file = File.join(source, '_config.yml') 67 | begin 68 | config = YAML.load_file(config_file) 69 | raise "Invalid configuration - #{config_file}" if !config.is_a?(Hash) 70 | STDOUT.puts "Configuration from #{config_file}" 71 | rescue => err 72 | STDERR.puts "WARNING: Could not read configuration. Using defaults (and options)." 73 | STDERR.puts "\t" + err.to_s 74 | config = {} 75 | end 76 | 77 | # Merge DEFAULTS < _config.yml < override 78 | Jekyll::DEFAULTS.deep_merge(config).deep_merge(override) 79 | end 80 | 81 | def self.version 82 | yml = YAML.load(File.read(File.join(File.dirname(__FILE__), *%w[.. VERSION.yml]))) 83 | "#{yml[:major]}.#{yml[:minor]}.#{yml[:patch]}" 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /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 | module Jekyll 7 | module Convertible 8 | # Return the contents as a string 9 | def to_s 10 | self.content || '' 11 | end 12 | 13 | # Read the YAML frontmatter 14 | # +base+ is the String path to the dir containing the file 15 | # +name+ is the String filename of the file 16 | # 17 | # Returns nothing 18 | def read_yaml(base, name) 19 | self.content = File.read(File.join(base, name)) 20 | 21 | if self.content =~ /^(---\s*\n.*?\n?)(---.*?\n)/m 22 | self.content = self.content[($1.size + $2.size)..-1] 23 | 24 | self.data = YAML.load($1) 25 | end 26 | 27 | self.data ||= {} 28 | end 29 | 30 | # Transform the contents based on the file extension. 31 | # 32 | # Returns nothing 33 | def transform 34 | case self.content_type 35 | when 'textile' 36 | self.ext = ".html" 37 | self.content = self.site.textile(self.content) 38 | when 'markdown' 39 | self.ext = ".html" 40 | self.content = self.site.markdown(self.content) 41 | end 42 | end 43 | 44 | # Determine which formatting engine to use based on this convertible's 45 | # extension 46 | # 47 | # Returns one of :textile, :markdown or :unknown 48 | def content_type 49 | case self.ext[1..-1] 50 | when /textile/i 51 | return 'textile' 52 | when /markdown/i, /mkdn/i, /md/i 53 | return 'markdown' 54 | end 55 | return 'unknown' 56 | end 57 | 58 | # Add any necessary layouts to this convertible document 59 | # +layouts+ is a Hash of {"name" => "layout"} 60 | # +site_payload+ is the site payload hash 61 | # 62 | # Returns nothing 63 | def do_layout(payload, layouts) 64 | info = { :filters => [Jekyll::Filters], :registers => { :site => self.site } } 65 | 66 | # render and transform content (this becomes the final content of the object) 67 | payload["content_type"] = self.content_type 68 | self.content = Liquid::Template.parse(self.content).render(payload, info) 69 | self.transform 70 | 71 | # output keeps track of what will finally be written 72 | self.output = self.content 73 | 74 | # recursively render layouts 75 | layout = layouts[self.data["layout"]] 76 | while layout 77 | payload = payload.deep_merge({"content" => self.output, "page" => layout.data}) 78 | self.output = Liquid::Template.parse(layout.content).render(payload, info) 79 | 80 | layout = layouts[layout.data["layout"]] 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /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 | 23 | @site.process 24 | assert_equal before_posts, @site.posts.length 25 | assert_equal before_layouts, @site.layouts.length 26 | assert_equal before_categories, @site.categories.length 27 | end 28 | 29 | should "read layouts" do 30 | @site.read_layouts 31 | assert_equal ["default", "simple"].sort, @site.layouts.keys.sort 32 | end 33 | 34 | should "read posts" do 35 | @site.read_posts('') 36 | posts = Dir[source_dir('_posts', '*')] 37 | assert_equal posts.size - 1, @site.posts.size 38 | end 39 | 40 | should "deploy payload" do 41 | clear_dest 42 | @site.process 43 | 44 | posts = Dir[source_dir("**", "_posts", "*")] 45 | categories = %w(bar baz category foo z_category publish_test win).sort 46 | 47 | assert_equal posts.size - 1, @site.posts.size 48 | assert_equal categories, @site.categories.keys.sort 49 | assert_equal 4, @site.categories['foo'].size 50 | end 51 | 52 | should "filter entries" do 53 | ent1 = %w[foo.markdown bar.markdown baz.markdown #baz.markdown# 54 | .baz.markdow foo.markdown~] 55 | ent2 = %w[.htaccess _posts bla.bla] 56 | 57 | assert_equal %w[foo.markdown bar.markdown baz.markdown], @site.filter_entries(ent1) 58 | assert_equal ent2, @site.filter_entries(ent2) 59 | end 60 | 61 | should "filter entries with exclude" do 62 | excludes = %w[README TODO] 63 | includes = %w[index.html site.css] 64 | 65 | @site.exclude = excludes 66 | assert_equal includes, @site.filter_entries(excludes + includes) 67 | end 68 | 69 | context 'with an invalid markdown processor in the configuration' do 70 | 71 | should 'give a meaningful error message' do 72 | bad_processor = 'not a processor name' 73 | begin 74 | Site.new(Jekyll.configuration.merge({ 'markdown' => bad_processor })) 75 | flunk 'Invalid markdown processors should cause a failure on site creation' 76 | rescue RuntimeError => e 77 | assert e.to_s =~ /invalid|bad/i 78 | assert e.to_s =~ %r{#{bad_processor}} 79 | end 80 | end 81 | 82 | end 83 | 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | require 'rake/rdoctask' 4 | 5 | begin 6 | gem 'jeweler', '>= 0.11.0' 7 | require 'jeweler' 8 | Jeweler::Tasks.new do |s| 9 | s.name = "jekyll" 10 | s.summary = %Q{Jekyll is a simple, blog aware, static site generator.} 11 | s.email = "tom@mojombo.com" 12 | s.homepage = "http://github.com/mojombo/jekyll" 13 | s.description = "Jekyll is a simple, blog aware, static site generator." 14 | s.authors = ["Tom Preston-Werner"] 15 | s.rubyforge_project = "jekyll" 16 | s.files.exclude 'test/dest' 17 | s.test_files.exclude 'test/dest' 18 | s.add_dependency('RedCloth', '>= 4.2.1') 19 | s.add_dependency('liquid', '>= 1.9.0') 20 | s.add_dependency('classifier', '>= 1.3.1') 21 | s.add_dependency('maruku', '>= 0.5.9') 22 | s.add_dependency('directory_watcher', '>= 1.1.1') 23 | s.add_dependency('open4', '>= 0.9.6') 24 | end 25 | rescue LoadError 26 | puts "Jeweler not available. Install it with: sudo gem install jeweler --version '>= 0.11.0'" 27 | exit(1) 28 | end 29 | 30 | Rake::TestTask.new do |t| 31 | t.libs << 'lib' 32 | t.pattern = 'test/**/test_*.rb' 33 | t.verbose = false 34 | end 35 | 36 | Rake::RDocTask.new do |rdoc| 37 | rdoc.rdoc_dir = 'rdoc' 38 | rdoc.title = 'jekyll' 39 | rdoc.options << '--line-numbers' << '--inline-source' 40 | rdoc.rdoc_files.include('README*') 41 | rdoc.rdoc_files.include('lib/**/*.rb') 42 | end 43 | 44 | begin 45 | require 'rcov/rcovtask' 46 | Rcov::RcovTask.new do |t| 47 | t.libs << 'test' 48 | t.test_files = FileList['test/**/test_*.rb'] 49 | t.verbose = true 50 | end 51 | rescue LoadError 52 | end 53 | 54 | task :default => [:test, :features] 55 | 56 | # console 57 | 58 | desc "Open an irb session preloaded with this library" 59 | task :console do 60 | sh "irb -rubygems -I lib -r jekyll.rb" 61 | end 62 | 63 | # converters 64 | 65 | namespace :convert do 66 | desc "Migrate from mephisto in the current directory" 67 | task :mephisto do 68 | sh %q(ruby -r './lib/jekyll/converters/mephisto' -e 'Jekyll::Mephisto.postgres(:database => "#{ENV["DB"]}")') 69 | end 70 | desc "Migrate from Movable Type in the current directory" 71 | task :mt do 72 | sh %q(ruby -r './lib/jekyll/converters/mt' -e 'Jekyll::MT.process("#{ENV["DB"]}", "#{ENV["USER"]}", "#{ENV["PASS"]}")') 73 | end 74 | desc "Migrate from Typo in the current directory" 75 | task :typo do 76 | sh %q(ruby -r './lib/jekyll/converters/typo' -e 'Jekyll::Typo.process("#{ENV["DB"]}", "#{ENV["USER"]}", "#{ENV["PASS"]}")') 77 | end 78 | end 79 | 80 | begin 81 | require 'cucumber/rake/task' 82 | 83 | Cucumber::Rake::Task.new(:features) do |t| 84 | t.cucumber_opts = "--format progress" 85 | end 86 | rescue LoadError 87 | desc 'Cucumber rake task not available' 88 | task :features do 89 | abort 'Cucumber rake task is not available. Be sure to install cucumber as a gem or plugin' 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /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. Dependencies 24 | 25 | * RedCloth: Textile support 26 | * Liquid: Templating system 27 | * Classifier: Generating related posts 28 | * Maruku: Default markdown engine 29 | * Directory Watcher: Auto-regeneration of sites 30 | * Open4: Talking to pygments for syntax highlighting 31 | 32 | h2. License 33 | 34 | (The MIT License) 35 | 36 | Copyright (c) 2008 Tom Preston-Werner 37 | 38 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 39 | 40 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 41 | 42 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 43 | -------------------------------------------------------------------------------- /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 | context "with pretty url style" do 27 | setup do 28 | @site.permalink_style = :pretty 29 | end 30 | 31 | should "return dir correctly" do 32 | @page = setup_page('contacts.html') 33 | assert_equal '/contacts/', @page.dir 34 | end 35 | 36 | should "return dir correctly for index page" do 37 | @page = setup_page('index.html') 38 | assert_equal '/', @page.dir 39 | end 40 | end 41 | 42 | context "with any other url style" do 43 | should "return dir correctly" do 44 | @site.permalink_style = nil 45 | @page = setup_page('contacts.html') 46 | assert_equal '/', @page.dir 47 | end 48 | end 49 | 50 | should "respect permalink in yaml front matter" do 51 | file = "about.html" 52 | @page = setup_page(file) 53 | 54 | assert_equal "/about/", @page.permalink 55 | assert_equal @page.permalink, @page.url 56 | assert_equal "/about/", @page.dir 57 | end 58 | end 59 | 60 | context "rendering" do 61 | setup do 62 | clear_dest 63 | end 64 | 65 | should "write properly" do 66 | page = setup_page('contacts.html') 67 | do_render(page) 68 | page.write(dest_dir) 69 | 70 | assert File.directory?(dest_dir) 71 | assert File.exists?(File.join(dest_dir, 'contacts.html')) 72 | end 73 | 74 | should "write properly without html extension" do 75 | page = setup_page('contacts.html') 76 | page.site.permalink_style = :pretty 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', 'index.html')) 82 | end 83 | 84 | should "write properly with extension different from html" do 85 | page = setup_page("sitemap.xml") 86 | page.site.permalink_style = :pretty 87 | do_render(page) 88 | page.write(dest_dir) 89 | 90 | assert_equal("/sitemap.xml", page.url) 91 | assert_nil(page.url[/\.html$/]) 92 | assert File.directory?(dest_dir) 93 | assert File.exists?(File.join(dest_dir,'sitemap.xml')) 94 | end 95 | end 96 | 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /test/test_tags.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestTags < Test::Unit::TestCase 4 | 5 | def create_post(content, override = {}, markdown = true) 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 | 12 | if markdown 13 | payload = {"content_type" => "markdown"} 14 | else 15 | payload = {"content_type" => "textile"} 16 | end 17 | 18 | @result = Liquid::Template.parse(content).render(payload, info) 19 | 20 | if markdown 21 | @result = site.markdown(@result) 22 | else 23 | @result = site.textile(@result) 24 | end 25 | end 26 | 27 | def fill_post(code, override = {}) 28 | content = <test\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 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/jekyll/converters/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) 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 | -------------------------------------------------------------------------------- /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 "{{ site.posts.first.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 "{{ site.posts.first.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 "{{ site.posts.first.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 | -------------------------------------------------------------------------------- /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 Maruku 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 "maruku" 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: Highlight code with pygments 59 | Given I have an "index.html" file that contains "{% highlight ruby %} puts 'Hello world!' {% endhighlight %}" 60 | And I have a configuration file with "pygments" set to "true" 61 | When I run jekyll 62 | Then the _site directory should exist 63 | And I should see "puts 'Hello world!'" in "_site/index.html" 64 | -------------------------------------------------------------------------------- /lib/jekyll/page.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | 3 | class Page 4 | include Convertible 5 | 6 | attr_accessor :site 7 | attr_accessor :name, :ext, :basename 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 | end 26 | 27 | # The generated directory into which the page will be placed 28 | # upon generation. This is derived from the permalink or, if 29 | # permalink is absent, set to '/' 30 | # 31 | # Returns 32 | def dir 33 | url[-1, 1] == '/' ? url : File.dirname(url) 34 | end 35 | 36 | # The full path and filename of the post. 37 | # Defined in the YAML of the post body 38 | # (Optional) 39 | # 40 | # Returns 41 | def permalink 42 | self.data && self.data['permalink'] 43 | end 44 | 45 | def template 46 | if self.site.permalink_style == :pretty && !index? 47 | "/:name/" 48 | else 49 | "/:name.html" 50 | end 51 | end 52 | 53 | # The generated relative url of this page 54 | # e.g. /about.html 55 | # 56 | # Returns 57 | def url 58 | return permalink if permalink 59 | 60 | @url ||= (ext == '.html') ? template.gsub(':name', basename) : "/#{name}" 61 | end 62 | 63 | # Extract information from the page filename 64 | # +name+ is the String filename of the page file 65 | # 66 | # Returns nothing 67 | def process(name) 68 | self.ext = File.extname(name) 69 | self.basename = name.split('.')[0..-2].first 70 | end 71 | 72 | # Add any necessary layouts to this post 73 | # +layouts+ is a Hash of {"name" => "layout"} 74 | # +site_payload+ is the site payload hash 75 | # 76 | # Returns nothing 77 | def render(layouts, site_payload) 78 | payload = {"page" => self.data}.deep_merge(site_payload) 79 | do_layout(payload, layouts) 80 | end 81 | 82 | # Write the generated page file to the destination directory. 83 | # +dest_prefix+ is the String path to the destination dir 84 | # +dest_suffix+ is a suffix path to the destination dir 85 | # 86 | # Returns nothing 87 | def write(dest_prefix, dest_suffix = nil) 88 | dest = File.join(dest_prefix, @dir) 89 | dest = File.join(dest, dest_suffix) if dest_suffix 90 | FileUtils.mkdir_p(dest) 91 | 92 | # The url needs to be unescaped in order to preserve the correct filename 93 | path = File.join(dest, CGI.unescape(self.url)) 94 | if self.ext == '.html' && self.url[/\.html$/].nil? 95 | FileUtils.mkdir_p(path) 96 | path = File.join(path, "index.html") 97 | end 98 | 99 | File.open(path, 'w') do |f| 100 | f.write(self.output) 101 | end 102 | end 103 | 104 | private 105 | 106 | def index? 107 | basename == 'index' 108 | end 109 | 110 | end 111 | 112 | end 113 | -------------------------------------------------------------------------------- /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 | require 'open4' 45 | 46 | class Albino 47 | @@bin = Rails.development? ? 'pygmentize' : '/usr/bin/pygmentize' rescue 'pygmentize' 48 | 49 | def self.bin=(path) 50 | @@bin = path 51 | end 52 | 53 | def self.colorize(*args) 54 | new(*args).colorize 55 | end 56 | 57 | def initialize(target, lexer = :text, format = :html) 58 | @target = File.exists?(target) ? File.read(target) : target rescue target 59 | @options = { :l => lexer, :f => format, :O => 'encoding=utf-8' } 60 | end 61 | 62 | def execute(command) 63 | output = '' 64 | Open4.popen4(command) do |pid, stdin, stdout, stderr| 65 | stdin.puts @target 66 | stdin.close 67 | output = stdout.read.strip 68 | [stdout, stderr].each { |io| io.close } 69 | end 70 | output 71 | end 72 | 73 | def colorize(options = {}) 74 | html = execute(@@bin + convert_options(options)) 75 | # Work around an RDiscount bug: http://gist.github.com/97682 76 | html.to_s.sub(%r{\Z}, "\n") 77 | end 78 | alias_method :to_s, :colorize 79 | 80 | def convert_options(options = {}) 81 | @options.merge(options).inject('') do |string, (flag, value)| 82 | string + " -#{flag} #{value}" 83 | end 84 | end 85 | end 86 | 87 | if $0 == __FILE__ 88 | require 'rubygems' 89 | require 'test/spec' 90 | require 'mocha' 91 | begin require 'redgreen'; rescue LoadError; end 92 | 93 | context "Albino" do 94 | setup do 95 | @syntaxer = Albino.new(__FILE__, :ruby) 96 | end 97 | 98 | specify "defaults to text" do 99 | syntaxer = Albino.new(__FILE__) 100 | syntaxer.expects(:execute).with('pygmentize -f html -l text').returns(true) 101 | syntaxer.colorize 102 | end 103 | 104 | specify "accepts options" do 105 | @syntaxer.expects(:execute).with('pygmentize -f html -l ruby').returns(true) 106 | @syntaxer.colorize 107 | end 108 | 109 | specify "works with strings" do 110 | syntaxer = Albino.new('class New; end', :ruby) 111 | assert_match %r(highlight), syntaxer.colorize 112 | end 113 | 114 | specify "aliases to_s" do 115 | assert_equal @syntaxer.colorize, @syntaxer.to_s 116 | end 117 | 118 | specify "class method colorize" do 119 | assert_equal @syntaxer.colorize, Albino.colorize(__FILE__, :ruby) 120 | end 121 | end 122 | end -------------------------------------------------------------------------------- /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) 106 | end 107 | 108 | When /^I change "(.*)" to contain "(.*)"$/ do |file, text| 109 | File.open(file, 'a') do |f| 110 | f.write(text) 111 | end 112 | end 113 | 114 | Then /^the (.*) directory should exist$/ do |dir| 115 | assert File.directory?(dir) 116 | end 117 | 118 | Then /^the (.*) file should exist$/ do |file| 119 | assert File.file?(file) 120 | end 121 | 122 | Then /^I should see "(.*)" in "(.*)"$/ do |text, file| 123 | assert_match Regexp.new(text), File.open(file).readlines.join 124 | end 125 | 126 | Then /^the "(.*)" file should not exist$/ do |file| 127 | assert !File.exists?(file) 128 | end 129 | 130 | Then /^I should see today's time in "(.*)"$/ do |file| 131 | assert_match Regexp.new(Regexp.escape(Time.now.to_s)), File.open(file).readlines.join 132 | end 133 | 134 | Then /^I should see today's date in "(.*)"$/ do |file| 135 | assert_match Regexp.new(Date.today.to_s), File.open(file).readlines.join 136 | end 137 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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("--auto", "Auto-regenerate") do 27 | options['auto'] = true 28 | end 29 | 30 | opts.on("--no-auto", "No auto-regenerate") do 31 | options['auto'] = false 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("--lsi", "Use LSI for better related posts") do 40 | options['lsi'] = true 41 | end 42 | 43 | opts.on("--pygments", "Use pygments to highlight code") do 44 | options['pygments'] = true 45 | end 46 | 47 | opts.on("--rdiscount", "Use rdiscount gem for Markdown") do 48 | options['markdown'] = 'rdiscount' 49 | end 50 | 51 | opts.on("--permalink [TYPE]", "Use 'date' (default) for YYYY/MM/DD") do |style| 52 | options['permalink'] = style unless style.nil? 53 | end 54 | 55 | opts.on("--paginate [POSTS_PER_PAGE]", "Paginate a blog's posts") do |per_page| 56 | begin 57 | options['paginate'] = per_page.to_i 58 | raise ArgumentError if options['paginate'] == 0 59 | rescue 60 | puts 'you must specify a number of posts by page bigger than 0' 61 | exit 0 62 | end 63 | end 64 | 65 | opts.on("--version", "Display current version") do 66 | puts "Jekyll " + Jekyll.version 67 | exit 0 68 | end 69 | end 70 | 71 | # Read command line options into `options` hash 72 | opts.parse! 73 | 74 | # Get source and destintation from command line 75 | case ARGV.size 76 | when 0 77 | when 1 78 | options['destination'] = ARGV[0] 79 | when 2 80 | options['source'] = ARGV[0] 81 | options['destination'] = ARGV[1] 82 | else 83 | puts "Invalid options. Run `jekyll --help` for assistance." 84 | exit(1) 85 | end 86 | 87 | options = Jekyll.configuration(options) 88 | 89 | # Get source and destination directories (possibly set by config file) 90 | source = options['source'] 91 | destination = options['destination'] 92 | 93 | # Files to watch 94 | def globs(source) 95 | Dir.chdir(source) do 96 | dirs = Dir['*'].select { |x| File.directory?(x) } 97 | dirs -= ['_site'] 98 | dirs = dirs.map { |x| "#{x}/**/*" } 99 | dirs += ['*'] 100 | end 101 | end 102 | 103 | # Create the Site 104 | site = Jekyll::Site.new(options) 105 | 106 | # Run the directory watcher for auto-generation, if required 107 | if options['auto'] 108 | require 'directory_watcher' 109 | 110 | puts "Auto-regenerating enabled: #{source} -> #{destination}" 111 | 112 | dw = DirectoryWatcher.new(source) 113 | dw.interval = 1 114 | dw.glob = globs(source) 115 | 116 | dw.add_observer do |*args| 117 | t = Time.now.strftime("%Y-%m-%d %H:%M:%S") 118 | puts "[#{t}] regeneration: #{args.size} files changed" 119 | site.process 120 | end 121 | 122 | dw.start 123 | 124 | unless options['server'] 125 | loop { sleep 1000 } 126 | end 127 | else 128 | puts "Building site: #{source} -> #{destination}" 129 | site.process 130 | puts "Successfully generated site: #{source} -> #{destination}" 131 | end 132 | 133 | # Run the server on the specified port, if required 134 | if options['server'] 135 | require 'webrick' 136 | include WEBrick 137 | 138 | FileUtils.mkdir_p(destination) 139 | 140 | s = HTTPServer.new( 141 | :Port => options['server_port'], 142 | :DocumentRoot => destination 143 | ) 144 | t = Thread.new { 145 | s.start 146 | } 147 | 148 | trap("INT") { s.shutdown } 149 | t.join() 150 | end 151 | -------------------------------------------------------------------------------- /jekyll.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | Gem::Specification.new do |s| 4 | s.name = %q{jekyll} 5 | s.version = "0.5.4" 6 | 7 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 8 | s.authors = ["Tom Preston-Werner"] 9 | s.date = %q{2009-08-24} 10 | s.default_executable = %q{jekyll} 11 | s.description = %q{Jekyll is a simple, blog aware, static site generator.} 12 | s.email = %q{tom@mojombo.com} 13 | s.executables = ["jekyll"] 14 | s.extra_rdoc_files = [ 15 | "README.textile" 16 | ] 17 | s.files = [ 18 | ".gitignore", 19 | "History.txt", 20 | "README.textile", 21 | "Rakefile", 22 | "VERSION.yml", 23 | "bin/jekyll", 24 | "features/create_sites.feature", 25 | "features/embed_filters.feature", 26 | "features/pagination.feature", 27 | "features/permalinks.feature", 28 | "features/post_data.feature", 29 | "features/site_configuration.feature", 30 | "features/site_data.feature", 31 | "features/step_definitions/jekyll_steps.rb", 32 | "features/support/env.rb", 33 | "jekyll.gemspec", 34 | "lib/jekyll.rb", 35 | "lib/jekyll/albino.rb", 36 | "lib/jekyll/converters/csv.rb", 37 | "lib/jekyll/converters/mephisto.rb", 38 | "lib/jekyll/converters/mt.rb", 39 | "lib/jekyll/converters/textpattern.rb", 40 | "lib/jekyll/converters/typo.rb", 41 | "lib/jekyll/converters/wordpress.rb", 42 | "lib/jekyll/convertible.rb", 43 | "lib/jekyll/core_ext.rb", 44 | "lib/jekyll/filters.rb", 45 | "lib/jekyll/layout.rb", 46 | "lib/jekyll/page.rb", 47 | "lib/jekyll/pager.rb", 48 | "lib/jekyll/post.rb", 49 | "lib/jekyll/site.rb", 50 | "lib/jekyll/tags/highlight.rb", 51 | "lib/jekyll/tags/include.rb", 52 | "test/helper.rb", 53 | "test/source/_includes/sig.markdown", 54 | "test/source/_layouts/default.html", 55 | "test/source/_layouts/simple.html", 56 | "test/source/_posts/2008-02-02-not-published.textile", 57 | "test/source/_posts/2008-02-02-published.textile", 58 | "test/source/_posts/2008-10-18-foo-bar.textile", 59 | "test/source/_posts/2008-11-21-complex.textile", 60 | "test/source/_posts/2008-12-03-permalinked-post.textile", 61 | "test/source/_posts/2008-12-13-include.markdown", 62 | "test/source/_posts/2009-01-27-array-categories.textile", 63 | "test/source/_posts/2009-01-27-categories.textile", 64 | "test/source/_posts/2009-01-27-category.textile", 65 | "test/source/_posts/2009-03-12-hash-#1.markdown", 66 | "test/source/_posts/2009-05-18-tag.textile", 67 | "test/source/_posts/2009-05-18-tags.textile", 68 | "test/source/_posts/2009-06-22-empty-yaml.textile", 69 | "test/source/_posts/2009-06-22-no-yaml.textile", 70 | "test/source/about.html", 71 | "test/source/category/_posts/2008-9-23-categories.textile", 72 | "test/source/contacts.html", 73 | "test/source/css/screen.css", 74 | "test/source/foo/_posts/bar/2008-12-12-topical-post.textile", 75 | "test/source/index.html", 76 | "test/source/sitemap.xml", 77 | "test/source/win/_posts/2009-05-24-yaml-linebreak.markdown", 78 | "test/source/z_category/_posts/2008-9-23-categories.textile", 79 | "test/suite.rb", 80 | "test/test_configuration.rb", 81 | "test/test_filters.rb", 82 | "test/test_generated_site.rb", 83 | "test/test_page.rb", 84 | "test/test_pager.rb", 85 | "test/test_post.rb", 86 | "test/test_site.rb", 87 | "test/test_tags.rb" 88 | ] 89 | s.homepage = %q{http://github.com/mojombo/jekyll} 90 | s.rdoc_options = ["--charset=UTF-8"] 91 | s.require_paths = ["lib"] 92 | s.rubyforge_project = %q{jekyll} 93 | s.rubygems_version = %q{1.3.5} 94 | s.summary = %q{Jekyll is a simple, blog aware, static site generator.} 95 | s.test_files = [ 96 | "test/helper.rb", 97 | "test/suite.rb", 98 | "test/test_configuration.rb", 99 | "test/test_filters.rb", 100 | "test/test_generated_site.rb", 101 | "test/test_page.rb", 102 | "test/test_pager.rb", 103 | "test/test_post.rb", 104 | "test/test_site.rb", 105 | "test/test_tags.rb" 106 | ] 107 | 108 | if s.respond_to? :specification_version then 109 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 110 | s.specification_version = 3 111 | 112 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then 113 | s.add_runtime_dependency(%q, [">= 4.2.1"]) 114 | s.add_runtime_dependency(%q, [">= 1.9.0"]) 115 | s.add_runtime_dependency(%q, [">= 1.3.1"]) 116 | s.add_runtime_dependency(%q, [">= 0.5.9"]) 117 | s.add_runtime_dependency(%q, [">= 1.1.1"]) 118 | s.add_runtime_dependency(%q, [">= 0.9.6"]) 119 | else 120 | s.add_dependency(%q, [">= 4.2.1"]) 121 | s.add_dependency(%q, [">= 1.9.0"]) 122 | s.add_dependency(%q, [">= 1.3.1"]) 123 | s.add_dependency(%q, [">= 0.5.9"]) 124 | s.add_dependency(%q, [">= 1.1.1"]) 125 | s.add_dependency(%q, [">= 0.9.6"]) 126 | end 127 | else 128 | s.add_dependency(%q, [">= 4.2.1"]) 129 | s.add_dependency(%q, [">= 1.9.0"]) 130 | s.add_dependency(%q, [">= 1.3.1"]) 131 | s.add_dependency(%q, [">= 0.5.9"]) 132 | s.add_dependency(%q, [">= 1.1.1"]) 133 | s.add_dependency(%q, [">= 0.9.6"]) 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | == 0.5.4 / 2009-08-23 2 | * Bug Fixes 3 | * Do not allow symlinks (security vulnerability) 4 | 5 | == 0.5.3 / 2009-07-14 6 | * Bug Fixes 7 | * Solving the permalink bug where non-html files wouldn't work [github.com/jeffrydegrande] 8 | 9 | == 0.5.2 / 2009-06-24 10 | * Enhancements 11 | * Added --paginate option to the executable along with a paginator object for the payload [github.com/calavera] 12 | * Upgraded RedCloth to 4.2.1, which makes tags work once again. 13 | * Configuration options set in config.yml are now available through the site payload [github.com/vilcans] 14 | * Posts can now have an empty YAML front matter or none at all [github.com/bahuvrihi] 15 | * Bug Fixes 16 | * Fixing Ruby 1.9 issue that requires to_s on the err object [github.com/Chrononaut] 17 | * Fixes for pagination and ordering posts on the same day [github.com/ujh] 18 | * Made pages respect permalinks style and permalinks in yml front matter [github.com/eugenebolshakov] 19 | * Index.html file should always have index.html permalink [github.com/eugenebolshakov] 20 | * Added trailing slash to pretty permalink style so Apache is happy [github.com/eugenebolshakov] 21 | * Bad markdown processor in config fails sooner and with better message [github.com/gcnovus] 22 | * Allow CRLFs in yaml frontmatter [github.com/juretta] 23 | * Added Date#xmlschema for Ruby versions < 1.9 24 | 25 | == 0.5.1 / 2009-05-06 26 | * Major Enhancements 27 | * Next/previous posts in site payload [github.com/pantulis, github.com/tomo] 28 | * Permalink templating system 29 | * Moved most of the README out to the GitHub wiki 30 | * Exclude option in configuration so specified files won't be brought over with generated site [github.com/duritong] 31 | * Bug Fixes 32 | * Making sure config.yaml references are all gone, using only config.yml 33 | * Fixed syntax highlighting breaking for UTF-8 code [github.com/henrik] 34 | * Worked around RDiscount bug that prevents Markdown from getting parsed after highlight [github.com/henrik] 35 | * CGI escaped post titles [github.com/Chrononaut] 36 | 37 | == 0.5.0 / 2009-04-07 38 | * Minor Enhancements 39 | * Ability to set post categories via YAML [github.com/qrush] 40 | * Ability to set prevent a post from publishing via YAML [github.com/qrush] 41 | * Add textilize filter [github.com/willcodeforfoo] 42 | * Add 'pretty' permalink style for wordpress-like urls [github.com/dysinger] 43 | * Made it possible to enter categories from YAML as an array [github.com/Chrononaut] 44 | * Ignore Emacs autosave files [github.com/Chrononaut] 45 | * Bug Fixes 46 | * Use block syntax of popen4 to ensure that subprocesses are properly disposed [github.com/jqr] 47 | * Close open4 streams to prevent zombies [github.com/rtomayko] 48 | * Only query required fields from the WP Database [github.com/ariejan] 49 | * Prevent _posts from being copied to the destination directory [github.com/bdimcheff] 50 | * Refactors 51 | * Factored the filtering code into a method [github.com/Chrononaut] 52 | * Fix tests and convert to Shoulda [github.com/qrush, github.com/technicalpickles] 53 | * Add Cucumber acceptance test suite [github.com/qrush, github.com/technicalpickles] 54 | 55 | == 0.4.1 56 | * Minor Enhancements 57 | * Changed date format on wordpress converter (zeropadding) [github.com/dysinger] 58 | * Bug Fixes 59 | * Add jekyll binary as executable to gemspec [github.com/dysinger] 60 | 61 | == 0.4.0 / 2009-02-03 62 | * Major Enhancements 63 | * Switch to Jeweler for packaging tasks 64 | * Minor Enhancements 65 | * Type importer [github.com/codeslinger] 66 | * site.topics accessor [github.com/baz] 67 | * Add array_to_sentence_string filter [github.com/mchung] 68 | * Add a converter for textpattern [github.com/PerfectlyNormal] 69 | * Add a working Mephisto / MySQL converter [github.com/ivey] 70 | * Allowing .htaccess files to be copied over into the generated site [github.com/briandoll] 71 | * Add option to not put file date in permalink URL [github.com/mreid] 72 | * Add line number capabilities to highlight blocks [github.com/jcon] 73 | * Bug Fixes 74 | * Fix permalink behavior [github.com/cavalle] 75 | * Fixed an issue with pygments, markdown, and newlines [github.com/zpinter] 76 | * Ampersands need to be escaped [github.com/pufuwozu, github.com/ap] 77 | * Test and fix the site.categories hash [github.com/zzot] 78 | * Fix site payload available to files [github.com/matrix9180] 79 | 80 | == 0.3.0 / 2008-12-24 81 | * Major Enhancements 82 | * Added --server option to start a simple WEBrick server on destination directory [github.com/johnreilly and github.com/mchung] 83 | * Minor Enhancements 84 | * Added post categories based on directories containing _posts [github.com/mreid] 85 | * Added post topics based on directories underneath _posts 86 | * Added new date filter that shows the full month name [github.com/mreid] 87 | * Merge Post's YAML front matter into its to_liquid payload [github.com/remi] 88 | * Restrict includes to regular files underneath _includes 89 | * Bug Fixes 90 | * Change YAML delimiter matcher so as to not chew up 2nd level markdown headers [github.com/mreid] 91 | * Fix bug that meant page data (such as the date) was not available in templates [github.com/mreid] 92 | * Properly reject directories in _layouts 93 | 94 | == 0.2.1 / 2008-12-15 95 | * Major Changes 96 | * Use Maruku (pure Ruby) for Markdown by default [github.com/mreid] 97 | * Allow use of RDiscount with --rdiscount flag 98 | * Minor Enhancements 99 | * Don't load directory_watcher unless it's needed [github.com/pjhyett] 100 | 101 | == 0.2.0 / 2008-12-14 102 | * Major Changes 103 | * related_posts is now found in site.related_posts 104 | 105 | == 0.1.6 / 2008-12-13 106 | * Major Features 107 | * Include files in _includes with {% include x.textile %} 108 | 109 | == 0.1.5 / 2008-12-12 110 | * Major Features 111 | * Code highlighting with Pygments if --pygments is specified 112 | * Disable true LSI by default, enable with --lsi 113 | * Minor Enhancements 114 | * Output informative message if RDiscount is not available [github.com/JackDanger] 115 | * Bug Fixes 116 | * Prevent Jekyll from picking up the output directory as a source [github.com/JackDanger] 117 | * Skip related_posts when there is only one post [github.com/JackDanger] 118 | 119 | == 0.1.4 / 2008-12-08 120 | * Bug Fixes 121 | * DATA does not work properly with rubygems 122 | 123 | == 0.1.3 / 2008-12-06 124 | * Major Features 125 | * Markdown support [github.com/vanpelt] 126 | * Mephisto and CSV converters [github.com/vanpelt] 127 | * Code hilighting [github.com/vanpelt] 128 | * Autobuild 129 | * Bug Fixes 130 | * Accept both \r\n and \n in YAML header [github.com/vanpelt] 131 | 132 | == 0.1.2 / 2008-11-22 133 | * Major Features 134 | * Add a real "related posts" implementation using Classifier 135 | * Command Line Changes 136 | * Allow cli to be called with 0, 1, or 2 args intuiting dir paths 137 | if they are omitted 138 | 139 | == 0.1.1 / 2008-11-22 140 | * Minor Additions 141 | * Posts now support introspectional data e.g. {{ page.url }} 142 | 143 | == 0.1.0 / 2008-11-05 144 | * First release 145 | * Converts posts written in Textile 146 | * Converts regular site pages 147 | * Simple copy of binary files 148 | 149 | == 0.0.0 / 2008-10-19 150 | * Birthday! 151 | 152 | -------------------------------------------------------------------------------- /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, :date, :slug, :ext, :published, :data, :content, :output, :tags 22 | attr_writer :categories 23 | 24 | def categories 25 | @categories ||= [] 26 | end 27 | 28 | # Initialize this Post instance. 29 | # +site+ is the Site 30 | # +base+ is the String path to the dir containing the post file 31 | # +name+ is the String filename of the post file 32 | # +categories+ is an Array of Strings for the categories for this post 33 | # 34 | # Returns 35 | def initialize(site, source, dir, name) 36 | @site = site 37 | @base = File.join(source, dir, '_posts') 38 | @name = name 39 | 40 | self.categories = dir.split('/').reject { |x| x.empty? } 41 | self.process(name) 42 | self.read_yaml(@base, name) 43 | 44 | if self.data.has_key?('published') && self.data['published'] == false 45 | self.published = false 46 | else 47 | self.published = true 48 | end 49 | 50 | if self.data.has_key?("tag") 51 | self.tags = [self.data["tag"]] 52 | elsif self.data.has_key?("tags") 53 | self.tags = self.data['tags'] 54 | else 55 | self.tags = [] 56 | end 57 | 58 | if self.categories.empty? 59 | if self.data.has_key?('category') 60 | self.categories << self.data['category'] 61 | elsif self.data.has_key?('categories') 62 | # Look for categories in the YAML-header, either specified as 63 | # an array or a string. 64 | if self.data['categories'].kind_of? String 65 | self.categories = self.data['categories'].split 66 | else 67 | self.categories = self.data['categories'] 68 | end 69 | end 70 | end 71 | end 72 | 73 | # Spaceship is based on Post#date, slug 74 | # 75 | # Returns -1, 0, 1 76 | def <=>(other) 77 | cmp = self.date <=> other.date 78 | if 0 == cmp 79 | cmp = self.slug <=> other.slug 80 | end 81 | return cmp 82 | end 83 | 84 | # Extract information from the post filename 85 | # +name+ is the String filename of the post file 86 | # 87 | # Returns nothing 88 | def process(name) 89 | m, cats, date, slug, ext = *name.match(MATCHER) 90 | self.date = Time.parse(date) 91 | self.slug = slug 92 | self.ext = ext 93 | end 94 | 95 | # The generated directory into which the post will be placed 96 | # upon generation. This is derived from the permalink or, if 97 | # permalink is absent, set to the default date 98 | # e.g. "/2008/11/05/" if the permalink style is :date, otherwise nothing 99 | # 100 | # Returns 101 | def dir 102 | File.dirname(url) 103 | end 104 | 105 | # The full path and filename of the post. 106 | # Defined in the YAML of the post body 107 | # (Optional) 108 | # 109 | # Returns 110 | def permalink 111 | self.data && self.data['permalink'] 112 | end 113 | 114 | def template 115 | case self.site.permalink_style 116 | when :pretty 117 | "/:categories/:year/:month/:day/:title/" 118 | when :none 119 | "/:categories/:title.html" 120 | when :date 121 | "/:categories/:year/:month/:day/:title.html" 122 | else 123 | self.site.permalink_style.to_s 124 | end 125 | end 126 | 127 | # The generated relative url of this post 128 | # e.g. /2008/11/05/my-awesome-post.html 129 | # 130 | # Returns 131 | def url 132 | return permalink if permalink 133 | 134 | @url ||= { 135 | "year" => date.strftime("%Y"), 136 | "month" => date.strftime("%m"), 137 | "day" => date.strftime("%d"), 138 | "title" => CGI.escape(slug), 139 | "categories" => categories.sort.join('/') 140 | }.inject(template) { |result, token| 141 | result.gsub(/:#{token.first}/, token.last) 142 | }.gsub(/\/\//, "/") 143 | end 144 | 145 | # The UID for this post (useful in feeds) 146 | # e.g. /2008/11/05/my-awesome-post 147 | # 148 | # Returns 149 | def id 150 | File.join(self.dir, self.slug) 151 | end 152 | 153 | # Calculate related posts. 154 | # 155 | # Returns [] 156 | def related_posts(posts) 157 | return [] unless posts.size > 1 158 | 159 | if self.site.lsi 160 | self.class.lsi ||= begin 161 | puts "Running the classifier... this could take a while." 162 | lsi = Classifier::LSI.new 163 | posts.each { |x| $stdout.print(".");$stdout.flush;lsi.add_item(x) } 164 | puts "" 165 | lsi 166 | end 167 | 168 | related = self.class.lsi.find_related(self.content, 11) 169 | related - [self] 170 | else 171 | (posts - [self])[0..9] 172 | end 173 | end 174 | 175 | # Add any necessary layouts to this post 176 | # +layouts+ is a Hash of {"name" => "layout"} 177 | # +site_payload+ is the site payload hash 178 | # 179 | # Returns nothing 180 | def render(layouts, site_payload) 181 | # construct payload 182 | payload = 183 | { 184 | "site" => { "related_posts" => related_posts(site_payload["site"]["posts"]) }, 185 | "page" => self.to_liquid 186 | } 187 | payload = payload.deep_merge(site_payload) 188 | 189 | do_layout(payload, layouts) 190 | end 191 | 192 | # Write the generated post file to the destination directory. 193 | # +dest+ is the String path to the destination dir 194 | # 195 | # Returns nothing 196 | def write(dest) 197 | FileUtils.mkdir_p(File.join(dest, dir)) 198 | 199 | # The url needs to be unescaped in order to preserve the correct filename 200 | path = File.join(dest, CGI.unescape(self.url)) 201 | 202 | if template[/\.html$/].nil? 203 | FileUtils.mkdir_p(path) 204 | path = File.join(path, "index.html") 205 | end 206 | 207 | File.open(path, 'w') do |f| 208 | f.write(self.output) 209 | end 210 | end 211 | 212 | # Convert this post into a Hash for use in Liquid templates. 213 | # 214 | # Returns 215 | def to_liquid 216 | { "title" => self.data["title"] || self.slug.split('-').select {|w| w.capitalize! || w }.join(' '), 217 | "url" => self.url, 218 | "date" => self.date, 219 | "id" => self.id, 220 | "categories" => self.categories, 221 | "next" => self.next, 222 | "previous" => self.previous, 223 | "tags" => self.tags, 224 | "content" => self.content }.deep_merge(self.data) 225 | end 226 | 227 | def inspect 228 | "" 229 | end 230 | 231 | def next 232 | pos = self.site.posts.index(self) 233 | 234 | if pos && pos < self.site.posts.length-1 235 | self.site.posts[pos+1] 236 | else 237 | nil 238 | end 239 | end 240 | 241 | def previous 242 | pos = self.site.posts.index(self) 243 | if pos && pos > 0 244 | self.site.posts[pos-1] 245 | else 246 | nil 247 | end 248 | end 249 | end 250 | 251 | end 252 | -------------------------------------------------------------------------------- /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: {{ site.posts.first.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: {{ site.posts.first.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: {{ site.posts.first.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: {{ site.posts.first.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: {{ site.posts.first.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: {{ site.posts.first.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: {{ site.posts.first.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 movies directory 86 | And I have a movies/scifi directory 87 | And I have a movies/scifi/_posts directory 88 | And I have a _layouts directory 89 | And I have the following post in "movies/scifi": 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: {{ site.posts.first.categories | array_to_sentence_string }}" 93 | When I run jekyll 94 | Then the _site directory should exist 95 | And I should see "Post categories: movies and scifi" in "_site/movies/scifi/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: {{ site.posts.first.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 | ['movies', 'scifi'] | Luke, I am your father. | 114 | And I have a simple layout that contains "Post categories: {{ site.posts.first.categories | array_to_sentence_string }}" 115 | When I run jekyll 116 | Then the _site directory should exist 117 | And I should see "Post categories: movies and scifi" in "_site/movies/scifi/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: {{ site.posts.first.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, :categories, :exclude, 5 | :source, :dest, :lsi, :pygments, :permalink_style, :tags 6 | 7 | # Initialize the site 8 | # +config+ is a Hash containing site configurations details 9 | # 10 | # Returns 11 | def initialize(config) 12 | self.config = config.clone 13 | 14 | self.source = File.expand_path(config['source']) 15 | self.dest = config['destination'] 16 | self.lsi = config['lsi'] 17 | self.pygments = config['pygments'] 18 | self.permalink_style = config['permalink'].to_sym 19 | self.exclude = config['exclude'] || [] 20 | 21 | self.reset 22 | self.setup 23 | end 24 | 25 | def reset 26 | self.layouts = {} 27 | self.posts = [] 28 | self.categories = Hash.new { |hash, key| hash[key] = [] } 29 | self.tags = Hash.new { |hash, key| hash[key] = [] } 30 | end 31 | 32 | def setup 33 | # Check to see if LSI is enabled. 34 | require 'classifier' if self.lsi 35 | 36 | # Set the Markdown interpreter (and Maruku self.config, if necessary) 37 | case self.config['markdown'] 38 | when 'rdiscount' 39 | begin 40 | require 'rdiscount' 41 | 42 | def markdown(content) 43 | RDiscount.new(content).to_html 44 | end 45 | 46 | rescue LoadError 47 | puts 'You must have the rdiscount gem installed first' 48 | end 49 | when 'maruku' 50 | begin 51 | require 'maruku' 52 | 53 | def markdown(content) 54 | Maruku.new(content).to_html 55 | end 56 | 57 | if self.config['maruku']['use_divs'] 58 | require 'maruku/ext/div' 59 | puts 'Maruku: Using extended syntax for div elements.' 60 | end 61 | 62 | if self.config['maruku']['use_tex'] 63 | require 'maruku/ext/math' 64 | puts "Maruku: Using LaTeX extension. Images in `#{self.config['maruku']['png_dir']}`." 65 | 66 | # Switch off MathML output 67 | MaRuKu::Globals[:html_math_output_mathml] = false 68 | MaRuKu::Globals[:html_math_engine] = 'none' 69 | 70 | # Turn on math to PNG support with blahtex 71 | # Resulting PNGs stored in `images/latex` 72 | MaRuKu::Globals[:html_math_output_png] = true 73 | MaRuKu::Globals[:html_png_engine] = self.config['maruku']['png_engine'] 74 | MaRuKu::Globals[:html_png_dir] = self.config['maruku']['png_dir'] 75 | MaRuKu::Globals[:html_png_url] = self.config['maruku']['png_url'] 76 | end 77 | rescue LoadError 78 | puts "The maruku gem is required for markdown support!" 79 | end 80 | else 81 | raise "Invalid Markdown processor: '#{self.config['markdown']}' -- did you mean 'maruku' or 'rdiscount'?" 82 | end 83 | end 84 | 85 | def textile(content) 86 | RedCloth.new(content).to_html 87 | end 88 | 89 | # Do the actual work of processing the site and generating the 90 | # real deal. 91 | # 92 | # Returns nothing 93 | def process 94 | self.reset 95 | self.read_layouts 96 | self.transform_pages 97 | self.write_posts 98 | end 99 | 100 | # Read all the files in /_layouts into memory for later use. 101 | # 102 | # Returns nothing 103 | def read_layouts 104 | base = File.join(self.source, "_layouts") 105 | entries = [] 106 | Dir.chdir(base) { entries = filter_entries(Dir['*.*']) } 107 | 108 | entries.each do |f| 109 | name = f.split(".")[0..-2].join(".") 110 | self.layouts[name] = Layout.new(self, base, f) 111 | end 112 | rescue Errno::ENOENT => e 113 | # ignore missing layout dir 114 | end 115 | 116 | # Read all the files in /_posts and create a new Post object with each one. 117 | # 118 | # Returns nothing 119 | def read_posts(dir) 120 | base = File.join(self.source, dir, '_posts') 121 | entries = [] 122 | Dir.chdir(base) { entries = filter_entries(Dir['**/*']) } 123 | 124 | # first pass processes, but does not yet render post content 125 | entries.each do |f| 126 | if Post.valid?(f) 127 | post = Post.new(self, self.source, dir, f) 128 | 129 | if post.published 130 | self.posts << post 131 | post.categories.each { |c| self.categories[c] << post } 132 | post.tags.each { |c| self.tags[c] << post } 133 | end 134 | end 135 | end 136 | 137 | self.posts.sort! 138 | 139 | # second pass renders each post now that full site payload is available 140 | self.posts.each do |post| 141 | post.render(self.layouts, site_payload) 142 | end 143 | 144 | self.categories.values.map { |ps| ps.sort! { |a, b| b <=> a} } 145 | self.tags.values.map { |ps| ps.sort! { |a, b| b <=> a} } 146 | rescue Errno::ENOENT => e 147 | # ignore missing layout dir 148 | end 149 | 150 | # Write each post to //// 151 | # 152 | # Returns nothing 153 | def write_posts 154 | self.posts.each do |post| 155 | post.write(self.dest) 156 | end 157 | end 158 | 159 | # Copy all regular files from to / ignoring 160 | # any files/directories that are hidden or backup files (start 161 | # with "." or "#" or end with "~") or contain site content (start with "_") 162 | # unless they are "_posts" directories or web server files such as 163 | # '.htaccess' 164 | # The +dir+ String is a relative path used to call this method 165 | # recursively as it descends through directories 166 | # 167 | # Returns nothing 168 | def transform_pages(dir = '') 169 | base = File.join(self.source, dir) 170 | entries = filter_entries(Dir.entries(base)) 171 | directories = entries.select { |e| File.directory?(File.join(base, e)) } 172 | files = entries.reject { |e| File.directory?(File.join(base, e)) || File.symlink?(File.join(base, e)) } 173 | 174 | # we need to make sure to process _posts *first* otherwise they 175 | # might not be available yet to other templates as {{ site.posts }} 176 | if directories.include?('_posts') 177 | directories.delete('_posts') 178 | read_posts(dir) 179 | end 180 | 181 | [directories, files].each do |entries| 182 | entries.each do |f| 183 | if File.directory?(File.join(base, f)) 184 | next if self.dest.sub(/\/$/, '') == File.join(base, f) 185 | transform_pages(File.join(dir, f)) 186 | elsif Pager.pagination_enabled?(self.config, f) 187 | paginate_posts(f, dir) 188 | else 189 | first3 = File.open(File.join(self.source, dir, f)) { |fd| fd.read(3) } 190 | if first3 == "---" 191 | # file appears to have a YAML header so process it as a page 192 | page = Page.new(self, self.source, dir, f) 193 | page.render(self.layouts, site_payload) 194 | page.write(self.dest) 195 | else 196 | # otherwise copy the file without transforming it 197 | FileUtils.mkdir_p(File.join(self.dest, dir)) 198 | FileUtils.cp(File.join(self.source, dir, f), File.join(self.dest, dir, f)) 199 | end 200 | end 201 | end 202 | end 203 | end 204 | 205 | # Constructs a hash map of Posts indexed by the specified Post attribute 206 | # 207 | # Returns {post_attr => []} 208 | def post_attr_hash(post_attr) 209 | # Build a hash map based on the specified post attribute ( post attr => array of posts ) 210 | # then sort each array in reverse order 211 | hash = Hash.new { |hash, key| hash[key] = Array.new } 212 | self.posts.each { |p| p.send(post_attr.to_sym).each { |t| hash[t] << p } } 213 | hash.values.map { |sortme| sortme.sort! { |a, b| b <=> a} } 214 | return hash 215 | end 216 | 217 | # The Hash payload containing site-wide data 218 | # 219 | # Returns {"site" => {"time" =>