25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/lib/jekyll/layout.rb:
--------------------------------------------------------------------------------
1 | module Jekyll
2 |
3 | class Layout
4 | include Convertible
5 |
6 | attr_accessor :site
7 | attr_accessor :ext
8 | attr_accessor :data, :content
9 |
10 | # Initialize a new Layout.
11 | # +site+ is the Site
12 | # +base+ is the String path to the
13 | # +name+ is the String filename of the post file
14 | #
15 | # Returns
16 | def initialize(site, base, name)
17 | @site = site
18 | @base = base
19 | @name = name
20 |
21 | self.data = {}
22 |
23 | self.process(name)
24 | self.read_yaml(base, name)
25 | end
26 |
27 | # Extract information from the layout filename
28 | # +name+ is the String filename of the layout file
29 | #
30 | # Returns nothing
31 | def process(name)
32 | self.ext = File.extname(name)
33 | end
34 | end
35 |
36 | end
--------------------------------------------------------------------------------
/lib/jekyll/migrators/wordpress.com.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'hpricot'
3 | require 'fileutils'
4 |
5 | # This importer takes a wordpress.xml file,
6 | # which can be exported from your
7 | # wordpress.com blog (/wp-admin/export.php)
8 |
9 | module Jekyll
10 | module WordpressDotCom
11 | def self.process(filename = "wordpress.xml")
12 | FileUtils.mkdir_p "_posts"
13 | posts = 0
14 |
15 | doc = Hpricot::XML(File.read(filename))
16 |
17 | (doc/:channel/:item).each do |item|
18 | title = item.at(:title).inner_text
19 | name = "#{Date.parse((doc/:channel/:item).first.at(:pubDate).inner_text).to_s("%Y-%m-%d")}-#{title.downcase.gsub('[^a-z0-9]', '-')}.html"
20 |
21 | File.open("_posts/#{name}", "w") do |f|
22 | f.puts <<-HEADER
23 | ---
24 | layout: post
25 | title: #{title}
26 | ---
27 |
28 | HEADER
29 | f.puts item.at('content:encoded').inner_text
30 | end
31 |
32 | posts += 1
33 | end
34 |
35 | "Imported #{posts} posts"
36 | end
37 | end
38 | end
--------------------------------------------------------------------------------
/lib/jekyll/filters.rb:
--------------------------------------------------------------------------------
1 | require 'uri'
2 |
3 | module Jekyll
4 |
5 | module Filters
6 | def textilize(input)
7 | TextileConverter.new.convert(input)
8 | end
9 |
10 | def date_to_string(date)
11 | date.strftime("%d %b %Y")
12 | end
13 |
14 | def date_to_long_string(date)
15 | date.strftime("%d %B %Y")
16 | end
17 |
18 | def date_to_xmlschema(date)
19 | date.xmlschema
20 | end
21 |
22 | def xml_escape(input)
23 | CGI.escapeHTML(input)
24 | end
25 |
26 | def cgi_escape(input)
27 | CGI::escape(input)
28 | end
29 |
30 | def uri_escape(input)
31 | URI.escape(input)
32 | end
33 |
34 | def number_of_words(input)
35 | input.split.length
36 | end
37 |
38 | def array_to_sentence_string(array)
39 | connector = "and"
40 | case array.length
41 | when 0
42 | ""
43 | when 1
44 | array[0].to_s
45 | when 2
46 | "#{array[0]} #{connector} #{array[1]}"
47 | else
48 | "#{array[0...-1].join(', ')}, #{connector} #{array[-1]}"
49 | end
50 | end
51 |
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | (The MIT License)
2 |
3 | Copyright (c) 2008 Tom Preston-Werner
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the 'Software'), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/lib/jekyll/tags/include.rb:
--------------------------------------------------------------------------------
1 | module Jekyll
2 |
3 | class IncludeTag < Liquid::Tag
4 | def initialize(tag_name, file, tokens)
5 | super
6 | @file = file.strip
7 | end
8 |
9 | def render(context)
10 | includes_dir = File.join(context.registers[:site].source, '_includes')
11 |
12 | if File.symlink?(includes_dir)
13 | return "Includes directory '#{includes_dir}' cannot be a symlink"
14 | end
15 |
16 | if @file !~ /^[a-zA-Z0-9_\/\.-]+$/ || @file =~ /\.\// || @file =~ /\/\./
17 | return "Include file '#{@file}' contains invalid characters or sequences"
18 | end
19 |
20 | Dir.chdir(includes_dir) do
21 | choices = Dir['**/*'].reject { |x| File.symlink?(x) }
22 | if choices.include?(@file)
23 | source = File.read(@file)
24 | partial = Liquid::Template.parse(source)
25 | context.stack do
26 | partial.render(context)
27 | end
28 | else
29 | "Included file '#{@file}' not found in _includes directory"
30 | end
31 | end
32 | end
33 | end
34 |
35 | end
36 |
37 | Liquid::Template.register_tag('include', Jekyll::IncludeTag)
38 |
--------------------------------------------------------------------------------
/test/source/sitemap.xml:
--------------------------------------------------------------------------------
1 | ---
2 | layout: nil
3 | ---
4 |
5 |
7 |
8 |
9 | http://example.com
10 | {{ site.time | date: "%Y-%m-%d" }}
11 | daily
12 | 1.0
13 |
14 |
15 | {% for post in site.posts %}
16 |
17 | http://example.com{{ post.url }}/
18 | {{ post.date | date: "%Y-%m-%d" }}
19 | monthly
20 | 0.2
21 |
22 | {% endfor %}
23 |
24 | {% for page in site.html_pages %}
25 |
26 | http://example.com{{ page.url }}
27 | {{ site.time | date: "%Y-%m-%d" }}
28 | {% if page.changefreq %}{{ page.changefreq }}{% endif %}
29 | {% if page.priority %}{{ page.priority }}{% endif %}
30 |
31 | {% endfor %}
32 |
33 |
--------------------------------------------------------------------------------
/test/test_configuration.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/helper'
2 |
3 | class TestConfiguration < Test::Unit::TestCase
4 | context "loading configuration" do
5 | setup do
6 | @path = File.join(Dir.pwd, '_config.yml')
7 | end
8 |
9 | should "fire warning with no _config.yml" do
10 | mock(YAML).load_file(@path) { raise "No such file or directory - #{@path}" }
11 | mock($stderr).puts("WARNING: Could not read configuration. Using defaults (and options).")
12 | mock($stderr).puts("\tNo such file or directory - #{@path}")
13 | assert_equal Jekyll::DEFAULTS, Jekyll.configuration({})
14 | end
15 |
16 | should "load configuration as hash" do
17 | mock(YAML).load_file(@path) { Hash.new }
18 | mock($stdout).puts("Configuration from #{@path}")
19 | assert_equal Jekyll::DEFAULTS, Jekyll.configuration({})
20 | end
21 |
22 | should "fire warning with bad config" do
23 | mock(YAML).load_file(@path) { Array.new }
24 | mock($stderr).puts("WARNING: Could not read configuration. Using defaults (and options).")
25 | mock($stderr).puts("\tInvalid configuration - #{@path}")
26 | assert_equal Jekyll::DEFAULTS, Jekyll.configuration({})
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/test/source/css/screen.css:
--------------------------------------------------------------------------------
1 | /*****************************************************************************/
2 | /*
3 | /* Common
4 | /*
5 | /*****************************************************************************/
6 |
7 | /* Global Reset */
8 |
9 | * {
10 | margin: 0;
11 | padding: 0;
12 | }
13 |
14 | html, body {
15 | height: 100%;
16 | }
17 |
18 | body {
19 | background-color: white;
20 | font: 13.34px helvetica, arial, clean, sans-serif;
21 | *font-size: small;
22 | text-align: center;
23 | }
24 |
25 | h1, h2, h3, h4, h5, h6 {
26 | font-size: 100%;
27 | }
28 |
29 | h1 {
30 | margin-bottom: 1em;
31 | }
32 |
33 | p {
34 | margin: 1em 0;
35 | }
36 |
37 | a {
38 | color: #00a;
39 | }
40 |
41 | a:hover {
42 | color: black;
43 | }
44 |
45 | a:visited {
46 | color: #a0a;
47 | }
48 |
49 | table {
50 | font-size: inherit;
51 | font: 100%;
52 | }
53 |
54 | /*****************************************************************************/
55 | /*
56 | /* Site
57 | /*
58 | /*****************************************************************************/
59 |
60 | .site {
61 | font-size: 110%;
62 | text-align: justify;
63 | width: 40em;
64 | margin: 3em auto 2em auto;
65 | line-height: 1.5em;
66 | }
67 |
68 | .title {
69 | color: #a00;
70 | font-weight: bold;
71 | margin-bottom: 2em;
72 | }
73 |
74 | .site .meta {
75 | color: #aaa;
76 | }
--------------------------------------------------------------------------------
/features/pagination.feature:
--------------------------------------------------------------------------------
1 | Feature: Site pagination
2 | In order to paginate my blog
3 | As a blog's user
4 | I want divide the posts in several pages
5 |
6 | Scenario Outline: Paginate with N posts per page
7 | Given I have a configuration file with "paginate" set to ""
8 | And I have a _layouts directory
9 | And I have an "index.html" page that contains "{{ paginator.posts.size }}"
10 | And I have a _posts directory
11 | And I have the following post:
12 | | title | date | layout | content |
13 | | Wargames | 3/27/2009 | default | The only winning move is not to play. |
14 | | Wargames2 | 4/27/2009 | default | The only winning move is not to play2. |
15 | | Wargames3 | 5/27/2009 | default | The only winning move is not to play3. |
16 | | Wargames4 | 6/27/2009 | default | The only winning move is not to play4. |
17 | When I run jekyll
18 | Then the _site/page directory should exist
19 | And the "_site/page/index.html" file should exist
20 | And I should see "" in "_site/page/index.html"
21 | And the "_site/page/index.html" file should not exist
22 |
23 | Examples:
24 | | num | exist | posts | not_exist |
25 | | 1 | 4 | 1 | 5 |
26 | | 2 | 2 | 2 | 3 |
27 | | 3 | 2 | 1 | 3 |
28 |
--------------------------------------------------------------------------------
/lib/jekyll/core_ext.rb:
--------------------------------------------------------------------------------
1 | class Hash
2 | # Merges self with another hash, recursively.
3 | #
4 | # This code was lovingly stolen from some random gem:
5 | # http://gemjack.com/gems/tartan-0.1.1/classes/Hash.html
6 | #
7 | # Thanks to whoever made it.
8 | def deep_merge(hash)
9 | target = dup
10 |
11 | hash.keys.each do |key|
12 | if hash[key].is_a? Hash and self[key].is_a? Hash
13 | target[key] = target[key].deep_merge(hash[key])
14 | next
15 | end
16 |
17 | target[key] = hash[key]
18 | end
19 |
20 | target
21 | end
22 |
23 | # Read array from the supplied hash favouring the singular key
24 | # and then the plural key, and handling any nil entries.
25 | # +hash+ the hash to read from
26 | # +singular_key+ the singular key
27 | # +plural_key+ the singular key
28 | #
29 | # Returns an array
30 | def pluralized_array(singular_key, plural_key)
31 | hash = self
32 | if hash.has_key?(singular_key)
33 | array = [hash[singular_key]] if hash[singular_key]
34 | elsif hash.has_key?(plural_key)
35 | case hash[plural_key]
36 | when String
37 | array = hash[plural_key].split
38 | when Array
39 | array = hash[plural_key].compact
40 | end
41 | end
42 | array || []
43 | end
44 | end
45 |
46 | # Thanks, ActiveSupport!
47 | class Date
48 | # Converts datetime to an appropriate format for use in XML
49 | def xmlschema
50 | strftime("%Y-%m-%dT%H:%M:%S%Z")
51 | end if RUBY_VERSION < '1.9'
52 | end
53 |
--------------------------------------------------------------------------------
/lib/jekyll/converter.rb:
--------------------------------------------------------------------------------
1 | module Jekyll
2 |
3 | class Converter < Plugin
4 | # Public: Get or set the pygments prefix. When an argument is specified,
5 | # the prefix will be set. If no argument is specified, the current prefix
6 | # will be returned.
7 | #
8 | # pygments_prefix - The String prefix (default: nil).
9 | #
10 | # Returns the String prefix.
11 | def self.pygments_prefix(pygments_prefix = nil)
12 | @pygments_prefix = pygments_prefix if pygments_prefix
13 | @pygments_prefix
14 | end
15 |
16 | # Public: Get or set the pygments suffix. When an argument is specified,
17 | # the suffix will be set. If no argument is specified, the current suffix
18 | # will be returned.
19 | #
20 | # pygments_suffix - The String suffix (default: nil).
21 | #
22 | # Returns the String suffix.
23 | def self.pygments_suffix(pygments_suffix = nil)
24 | @pygments_suffix = pygments_suffix if pygments_suffix
25 | @pygments_suffix
26 | end
27 |
28 | # Initialize the converter.
29 | #
30 | # Returns an initialized Converter.
31 | def initialize(config = {})
32 | @config = config
33 | end
34 |
35 | # Get the pygments prefix.
36 | #
37 | # Returns the String prefix.
38 | def pygments_prefix
39 | self.class.pygments_prefix
40 | end
41 |
42 | # Get the pygments suffix.
43 | #
44 | # Returns the String suffix.
45 | def pygments_suffix
46 | self.class.pygments_suffix
47 | end
48 | end
49 |
50 | end
--------------------------------------------------------------------------------
/features/markdown.feature:
--------------------------------------------------------------------------------
1 | Feature: Markdown
2 | As a hacker who likes to blog
3 | I want to be able to make a static site
4 | In order to share my awesome ideas with the interwebs
5 |
6 | Scenario: Markdown in list on index
7 | Given I have a configuration file with "paginate" set to "5"
8 | And I have an "index.html" page that contains "Index - {% for post in site.posts %} {{ post.content }} {% endfor %}"
9 | And I have a _posts directory
10 | And I have the following post:
11 | | title | date | content | type |
12 | | Hackers | 3/27/2009 | # My Title | markdown |
13 | When I run jekyll
14 | Then the _site directory should exist
15 | And I should see "Index" in "_site/index.html"
16 | And I should see "
My Title
" in "_site/2009/03/27/hackers.html"
17 | And I should see "
My Title
" in "_site/index.html"
18 |
19 | Scenario: Markdown in pagination on index
20 | Given I have a configuration file with "paginate" set to "5"
21 | And I have an "index.html" page that contains "Index - {% for post in paginator.posts %} {{ post.content }} {% endfor %}"
22 | And I have a _posts directory
23 | And I have the following post:
24 | | title | date | content | type |
25 | | Hackers | 3/27/2009 | # My Title | markdown |
26 | When I run jekyll
27 | Then the _site directory should exist
28 | And I should see "Index" in "_site/index.html"
29 | And I should see "
My Title
" in "_site/index.html"
30 |
--------------------------------------------------------------------------------
/lib/jekyll/migrators/typo.rb:
--------------------------------------------------------------------------------
1 | # Author: Toby DiPasquale
2 | require 'fileutils'
3 | require 'rubygems'
4 | require 'sequel'
5 |
6 | module Jekyll
7 | module Typo
8 | # this SQL *should* work for both MySQL and PostgreSQL, but I haven't
9 | # tested PostgreSQL yet (as of 2008-12-16)
10 | SQL = <<-EOS
11 | SELECT c.id id,
12 | c.title title,
13 | c.permalink slug,
14 | c.body body,
15 | c.published_at date,
16 | c.state state,
17 | COALESCE(tf.name, 'html') filter
18 | FROM contents c
19 | LEFT OUTER JOIN text_filters tf
20 | ON c.text_filter_id = tf.id
21 | EOS
22 |
23 | def self.process dbname, user, pass, host='localhost'
24 | FileUtils.mkdir_p '_posts'
25 | db = Sequel.mysql(dbname, :user => user, :password => pass, :host => host, :encoding => 'utf8')
26 | db[SQL].each do |post|
27 | next unless post[:state] =~ /Published/
28 |
29 | name = [ sprintf("%.04d", post[:date].year),
30 | sprintf("%.02d", post[:date].month),
31 | sprintf("%.02d", post[:date].day),
32 | post[:slug].strip ].join('-')
33 | # Can have more than one text filter in this field, but we just want
34 | # the first one for this
35 | name += '.' + post[:filter].split(' ')[0]
36 |
37 | File.open("_posts/#{name}", 'w') do |f|
38 | f.puts({ 'layout' => 'post',
39 | 'title' => post[:title].to_s,
40 | 'typo_id' => post[:id]
41 | }.delete_if { |k, v| v.nil? || v == '' }.to_yaml)
42 | f.puts '---'
43 | f.puts post[:body].delete("\r")
44 | end
45 | end
46 | end
47 |
48 | end # module Typo
49 | end # module Jekyll
50 |
--------------------------------------------------------------------------------
/lib/jekyll/migrators/textpattern.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'sequel'
3 | require 'fileutils'
4 |
5 | # NOTE: This converter requires Sequel and the MySQL gems.
6 | # The MySQL gem can be difficult to install on OS X. Once you have MySQL
7 | # installed, running the following commands should work:
8 | # $ sudo gem install sequel
9 | # $ sudo gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config
10 |
11 | module Jekyll
12 | module TextPattern
13 | # Reads a MySQL database via Sequel and creates a post file for each post.
14 | # The only posts selected are those with a status of 4 or 5, which means "live"
15 | # and "sticky" respectively.
16 | # Other statuses is 1 => draft, 2 => hidden and 3 => pending
17 | QUERY = "select Title, url_title, Posted, Body, Keywords from textpattern where Status = '4' or Status = '5'"
18 |
19 | def self.process(dbname, user, pass, host = 'localhost')
20 | db = Sequel.mysql(dbname, :user => user, :password => pass, :host => host, :encoding => 'utf8')
21 |
22 | FileUtils.mkdir_p "_posts"
23 |
24 | db[QUERY].each do |post|
25 | # Get required fields and construct Jekyll compatible name
26 | title = post[:Title]
27 | slug = post[:url_title]
28 | date = post[:Posted]
29 | content = post[:Body]
30 |
31 | name = [date.strftime("%Y-%m-%d"), slug].join('-') + ".textile"
32 |
33 | # Get the relevant fields as a hash, delete empty fields and convert
34 | # to YAML for the header
35 | data = {
36 | 'layout' => 'post',
37 | 'title' => title.to_s,
38 | 'tags' => post[:Keywords].split(',')
39 | }.delete_if { |k,v| v.nil? || v == ''}.to_yaml
40 |
41 | # Write out the data and content to file
42 | File.open("_posts/#{name}", "w") do |f|
43 | f.puts data
44 | f.puts "---"
45 | f.puts content
46 | end
47 | end
48 | end
49 | end
50 | end
--------------------------------------------------------------------------------
/lib/jekyll/migrators/marley.rb:
--------------------------------------------------------------------------------
1 | require 'yaml'
2 | require 'fileutils'
3 |
4 | module Jekyll
5 | module Marley
6 |
7 | def self.regexp
8 | { :id => /^\d{0,4}-{0,1}(.*)$/,
9 | :title => /^#\s*(.*)\s+$/,
10 | :title_with_date => /^#\s*(.*)\s+\(([0-9\/]+)\)$/,
11 | :published_on => /.*\s+\(([0-9\/]+)\)$/,
12 | :perex => /^([^\#\n]+\n)$/,
13 | :meta => /^\{\{\n(.*)\}\}\n$/mi # Multiline Regexp
14 | }
15 | end
16 |
17 | def self.process(marley_data_dir)
18 | raise ArgumentError, "marley dir #{marley_data_dir} not found" unless File.directory?(marley_data_dir)
19 |
20 | FileUtils.mkdir_p "_posts"
21 |
22 | posts = 0
23 | Dir["#{marley_data_dir}/**/*.txt"].each do |f|
24 | next unless File.exists?(f)
25 |
26 | #copied over from marley's app/lib/post.rb
27 | file_content = File.read(f)
28 | meta_content = file_content.slice!( self.regexp[:meta] )
29 | body = file_content.sub( self.regexp[:title], '').sub( self.regexp[:perex], '').strip
30 |
31 | title = file_content.scan( self.regexp[:title] ).first.to_s.strip
32 | prerex = file_content.scan( self.regexp[:perex] ).first.to_s.strip
33 | published_on = DateTime.parse( post[:published_on] ) rescue File.mtime( File.dirname(f) )
34 | meta = ( meta_content ) ? YAML::load( meta_content.scan( self.regexp[:meta]).to_s ) : {}
35 | meta['title'] = title
36 | meta['layout'] = 'post'
37 |
38 | formatted_date = published_on.strftime('%Y-%m-%d')
39 | post_name = File.dirname(f).split(%r{/}).last.gsub(/\A\d+-/, '')
40 |
41 | name = "#{formatted_date}-#{post_name}"
42 | File.open("_posts/#{name}.markdown", "w") do |f|
43 | f.puts meta.to_yaml
44 | f.puts "---\n"
45 | f.puts "\n#{prerex}\n\n" if prerex
46 | f.puts body
47 | end
48 | posts += 1
49 | end
50 | "Created #{posts} posts!"
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/test/test_filters.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/helper'
2 |
3 | class TestFilters < Test::Unit::TestCase
4 | class JekyllFilter
5 | include Jekyll::Filters
6 | end
7 |
8 | context "filters" do
9 | setup do
10 | @filter = JekyllFilter.new
11 | end
12 |
13 | should "textilize with simple string" do
14 | assert_equal "
something really simple
", @filter.textilize("something *really* simple")
15 | end
16 |
17 | should "convert array to sentence string with no args" do
18 | assert_equal "", @filter.array_to_sentence_string([])
19 | end
20 |
21 | should "convert array to sentence string with one arg" do
22 | assert_equal "1", @filter.array_to_sentence_string([1])
23 | assert_equal "chunky", @filter.array_to_sentence_string(["chunky"])
24 | end
25 |
26 | should "convert array to sentence string with two args" do
27 | assert_equal "1 and 2", @filter.array_to_sentence_string([1, 2])
28 | assert_equal "chunky and bacon", @filter.array_to_sentence_string(["chunky", "bacon"])
29 | end
30 |
31 | should "convert array to sentence string with multiple args" do
32 | assert_equal "1, 2, 3, and 4", @filter.array_to_sentence_string([1, 2, 3, 4])
33 | assert_equal "chunky, bacon, bits, and pieces", @filter.array_to_sentence_string(["chunky", "bacon", "bits", "pieces"])
34 | end
35 |
36 | should "escape xml with ampersands" do
37 | assert_equal "AT&T", @filter.xml_escape("AT&T")
38 | assert_equal "<code>command <filename></code>", @filter.xml_escape("command <filename>")
39 | end
40 |
41 | should "escape space as plus" do
42 | assert_equal "my+things", @filter.cgi_escape("my things")
43 | end
44 |
45 | should "escape special characters" do
46 | assert_equal "hey%21", @filter.cgi_escape("hey!")
47 | end
48 |
49 | should "escape space as %20" do
50 | assert_equal "my%20things", @filter.uri_escape("my things")
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/lib/jekyll/static_file.rb:
--------------------------------------------------------------------------------
1 | module Jekyll
2 |
3 | class StaticFile
4 | @@mtimes = Hash.new # the cache of last modification times [path] -> mtime
5 |
6 | # Initialize a new StaticFile.
7 | # +site+ is the Site
8 | # +base+ is the String path to the
9 | # +dir+ is the String path between and the file
10 | # +name+ is the String filename of the file
11 | #
12 | # Returns
13 | def initialize(site, base, dir, name)
14 | @site = site
15 | @base = base
16 | @dir = dir
17 | @name = name
18 | end
19 |
20 | # Obtains source file path.
21 | #
22 | # Returns source file path.
23 | def path
24 | File.join(@base, @dir, @name)
25 | end
26 |
27 | # Obtain destination path.
28 | # +dest+ is the String path to the destination dir
29 | #
30 | # Returns destination file path.
31 | def destination(dest)
32 | File.join(dest, @dir, @name)
33 | end
34 |
35 | # Obtain mtime of the source path.
36 | #
37 | # Returns last modifiaction time for this file.
38 | def mtime
39 | File.stat(path).mtime.to_i
40 | end
41 |
42 | # Is source path modified?
43 | #
44 | # Returns true if modified since last write.
45 | def modified?
46 | @@mtimes[path] != mtime
47 | end
48 |
49 | # Write the static file to the destination directory (if modified).
50 | # +dest+ is the String path to the destination dir
51 | #
52 | # Returns false if the file was not modified since last time (no-op).
53 | def write(dest)
54 | dest_path = destination(dest)
55 |
56 | return false if File.exist? dest_path and !modified?
57 | @@mtimes[path] = mtime
58 |
59 | FileUtils.mkdir_p(File.dirname(dest_path))
60 | FileUtils.cp(path, dest_path)
61 |
62 | true
63 | end
64 |
65 | # Reset the mtimes cache (for testing purposes).
66 | #
67 | # Returns nothing.
68 | def self.reset_cache
69 | @@mtimes = Hash.new
70 |
71 | nil
72 | end
73 | end
74 |
75 | end
76 |
--------------------------------------------------------------------------------
/README.textile:
--------------------------------------------------------------------------------
1 | h1. Jekyll
2 |
3 | By Tom Preston-Werner, Nick Quaranto, and many awesome contributors!
4 |
5 | Jekyll is a simple, blog aware, static site generator. It takes a template directory (representing the raw form of a website), runs it through Textile or Markdown and Liquid converters, and spits out a complete, static website suitable for serving with Apache or your favorite web server. This is also the engine behind "GitHub Pages":http://pages.github.com, which you can use to host your project's page or blog right here from GitHub.
6 |
7 | h2. Getting Started
8 |
9 | * "Install":http://wiki.github.com/mojombo/jekyll/install the gem
10 | * Read up about its "Usage":http://wiki.github.com/mojombo/jekyll/usage and "Configuration":http://wiki.github.com/mojombo/jekyll/configuration
11 | * Take a gander at some existing "Sites":http://wiki.github.com/mojombo/jekyll/sites
12 | * Fork and "Contribute":http://wiki.github.com/mojombo/jekyll/contribute your own modifications
13 | * Have questions? Post them on the "Mailing List":http://groups.google.com/group/jekyll-rb
14 |
15 | h2. Diving In
16 |
17 | * "Migrate":http://wiki.github.com/mojombo/jekyll/blog-migrations from your previous system
18 | * Learn how the "YAML Front Matter":http://wiki.github.com/mojombo/jekyll/yaml-front-matter works
19 | * Put information on your site with "Template Data":http://wiki.github.com/mojombo/jekyll/template-data
20 | * Customize the "Permalinks":http://wiki.github.com/mojombo/jekyll/permalinks your posts are generated with
21 | * Use the built-in "Liquid Extensions":http://wiki.github.com/mojombo/jekyll/liquid-extensions to make your life easier
22 |
23 | h2. Runtime Dependencies
24 |
25 | * RedCloth: Textile support (Ruby)
26 | * Liquid: Templating system (Ruby)
27 | * Classifier: Generating related posts (Ruby)
28 | * Maruku: Default markdown engine (Ruby)
29 | * Directory Watcher: Auto-regeneration of sites (Ruby)
30 | * Open4: Talking to pygments for syntax highlighting (Ruby)
31 | * Pygments: Syntax highlighting (Python)
32 |
33 | h2. Developer Dependencies
34 |
35 | * Shoulda: Test framework (Ruby)
36 | * RR: Mocking (Ruby)
37 | * RedGreen: Nicer test output (Ruby)
38 |
39 | h2. License
40 |
41 | See LICENSE.
42 |
--------------------------------------------------------------------------------
/lib/jekyll/migrators/wordpress.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'sequel'
3 | require 'fileutils'
4 | require 'yaml'
5 |
6 | # NOTE: This converter requires Sequel and the MySQL gems.
7 | # The MySQL gem can be difficult to install on OS X. Once you have MySQL
8 | # installed, running the following commands should work:
9 | # $ sudo gem install sequel
10 | # $ sudo gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config
11 |
12 | module Jekyll
13 | module WordPress
14 |
15 | # Reads a MySQL database via Sequel and creates a post file for each
16 | # post in wp_posts that has post_status = 'publish'.
17 | # This restriction is made because 'draft' posts are not guaranteed to
18 | # have valid dates.
19 | QUERY = "select post_title, post_name, post_date, post_content, post_excerpt, ID, guid from wp_posts where post_status = 'publish' and post_type = 'post'"
20 |
21 | def self.process(dbname, user, pass, host = 'localhost')
22 | db = Sequel.mysql(dbname, :user => user, :password => pass, :host => host, :encoding => 'utf8')
23 |
24 | FileUtils.mkdir_p "_posts"
25 |
26 | db[QUERY].each do |post|
27 | # Get required fields and construct Jekyll compatible name
28 | title = post[:post_title]
29 | slug = post[:post_name]
30 | date = post[:post_date]
31 | content = post[:post_content]
32 | name = "%02d-%02d-%02d-%s.markdown" % [date.year, date.month, date.day,
33 | slug]
34 |
35 | # Get the relevant fields as a hash, delete empty fields and convert
36 | # to YAML for the header
37 | data = {
38 | 'layout' => 'post',
39 | 'title' => title.to_s,
40 | 'excerpt' => post[:post_excerpt].to_s,
41 | 'wordpress_id' => post[:ID],
42 | 'wordpress_url' => post[:guid],
43 | 'date' => date
44 | }.delete_if { |k,v| v.nil? || v == ''}.to_yaml
45 |
46 | # Write out the data and content to file
47 | File.open("_posts/#{name}", "w") do |f|
48 | f.puts data
49 | f.puts "---"
50 | f.puts content
51 | end
52 | end
53 |
54 | end
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/lib/jekyll/plugin.rb:
--------------------------------------------------------------------------------
1 | module Jekyll
2 |
3 | class Plugin
4 | PRIORITIES = { :lowest => -100,
5 | :low => -10,
6 | :normal => 0,
7 | :high => 10,
8 | :highest => 100 }
9 |
10 | # Install a hook so that subclasses are recorded. This method is only
11 | # ever called by Ruby itself.
12 | #
13 | # base - The Class subclass.
14 | #
15 | # Returns nothing.
16 | def self.inherited(base)
17 | subclasses << base
18 | subclasses.sort!
19 | end
20 |
21 | # The list of Classes that have been subclassed.
22 | #
23 | # Returns an Array of Class objects.
24 | def self.subclasses
25 | @subclasses ||= []
26 | end
27 |
28 | # Get or set the priority of this plugin. When called without an
29 | # argument it returns the priority. When an argument is given, it will
30 | # set the priority.
31 | #
32 | # priority - The Symbol priority (default: nil). Valid options are:
33 | # :lowest, :low, :normal, :high, :highest
34 | #
35 | # Returns the Symbol priority.
36 | def self.priority(priority = nil)
37 | if priority && PRIORITIES.has_key?(priority)
38 | @priority = priority
39 | end
40 | @priority || :normal
41 | end
42 |
43 | # Get or set the safety of this plugin. When called without an argument
44 | # it returns the safety. When an argument is given, it will set the
45 | # safety.
46 | #
47 | # safe - The Boolean safety (default: nil).
48 | #
49 | # Returns the safety Boolean.
50 | def self.safe(safe = nil)
51 | if safe
52 | @safe = safe
53 | end
54 | @safe || false
55 | end
56 |
57 | # Spaceship is priority [higher -> lower]
58 | #
59 | # other - The class to be compared.
60 | #
61 | # Returns -1, 0, 1.
62 | def self.<=>(other)
63 | PRIORITIES[other.priority] <=> PRIORITIES[self.priority]
64 | end
65 |
66 | # Initialize a new plugin. This should be overridden by the subclass.
67 | #
68 | # config - The Hash of configuration options.
69 | #
70 | # Returns a new instance.
71 | def initialize(config = {})
72 | # no-op for default
73 | end
74 | end
75 |
76 | end
77 |
--------------------------------------------------------------------------------
/lib/jekyll/tags/highlight.rb:
--------------------------------------------------------------------------------
1 | module Jekyll
2 |
3 | class HighlightBlock < Liquid::Block
4 | include Liquid::StandardFilters
5 |
6 | # we need a language, but the linenos argument is optional.
7 | SYNTAX = /(\w+)\s?([\w\s=]+)*/
8 |
9 | def initialize(tag_name, markup, tokens)
10 | super
11 | if markup =~ SYNTAX
12 | @lang = $1
13 | if defined? $2
14 | tmp_options = {}
15 | $2.split.each do |opt|
16 | key, value = opt.split('=')
17 | if value.nil?
18 | if key == 'linenos'
19 | value = 'inline'
20 | else
21 | value = true
22 | end
23 | end
24 | tmp_options[key] = value
25 | end
26 | tmp_options = tmp_options.to_a.collect { |opt| opt.join('=') }
27 | # additional options to pass to Albino.
28 | @options = { 'O' => tmp_options.join(',') }
29 | else
30 | @options = {}
31 | end
32 | else
33 | raise SyntaxError.new("Syntax Error in 'highlight' - Valid syntax: highlight [linenos]")
34 | end
35 | end
36 |
37 | def render(context)
38 | if context.registers[:site].pygments
39 | render_pygments(context, super.join)
40 | else
41 | render_codehighlighter(context, super.join)
42 | end
43 | end
44 |
45 | def render_pygments(context, code)
46 | output = add_code_tags(Albino.new(code, @lang).to_s(@options), @lang)
47 | output = context["pygments_prefix"] + output if context["pygments_prefix"]
48 | output = output + context["pygments_suffix"] if context["pygments_suffix"]
49 | output
50 | end
51 |
52 | def render_codehighlighter(context, code)
53 | #The div is required because RDiscount blows ass
54 | <<-HTML
55 |
56 |
57 | #{h(code).strip}
58 |
59 |
60 | HTML
61 | end
62 |
63 | def add_code_tags(code, lang)
64 | # Add nested tags to code blocks
65 | code = code.sub(/
/,'
')
66 | code = code.sub(/<\/pre>/,"
")
67 | end
68 |
69 | end
70 |
71 | end
72 |
73 | Liquid::Template.register_tag('highlight', Jekyll::HighlightBlock)
74 |
--------------------------------------------------------------------------------
/test/test_generated_site.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/helper'
2 |
3 | class TestGeneratedSite < Test::Unit::TestCase
4 | context "generated sites" do
5 | setup do
6 | clear_dest
7 | stub(Jekyll).configuration do
8 | Jekyll::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir})
9 | end
10 |
11 | @site = Site.new(Jekyll.configuration)
12 | @site.process
13 | @index = File.read(dest_dir('index.html'))
14 | end
15 |
16 | should "ensure post count is as expected" do
17 | assert_equal 26, @site.posts.size
18 | end
19 |
20 | should "insert site.posts into the index" do
21 | assert @index.include?("#{@site.posts.size} Posts")
22 | end
23 |
24 | should "render latest post's content" do
25 | assert @index.include?(@site.posts.last.content)
26 | end
27 |
28 | should "hide unpublished posts" do
29 | published = Dir[dest_dir('publish_test/2008/02/02/*.html')].map {|f| File.basename(f)}
30 |
31 | assert_equal 1, published.size
32 | assert_equal "published.html", published.first
33 | end
34 |
35 | should "not copy _posts directory" do
36 | assert !File.exist?(dest_dir('_posts'))
37 | end
38 |
39 | should "process other static files and generate correct permalinks" do
40 | assert File.exists?(dest_dir('/about/index.html'))
41 | assert File.exists?(dest_dir('/contacts.html'))
42 | end
43 | end
44 |
45 | context "generating limited posts" do
46 | setup do
47 | clear_dest
48 | stub(Jekyll).configuration do
49 | Jekyll::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir, 'limit_posts' => 5})
50 | end
51 |
52 | @site = Site.new(Jekyll.configuration)
53 | @site.process
54 | @index = File.read(dest_dir('index.html'))
55 | end
56 |
57 | should "generate only the specified number of posts" do
58 | assert_equal 5, @site.posts.size
59 | end
60 |
61 | should "ensure limit posts is 1 or more" do
62 | assert_raise ArgumentError do
63 | clear_dest
64 | stub(Jekyll).configuration do
65 | Jekyll::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir, 'limit_posts' => 0})
66 | end
67 |
68 | @site = Site.new(Jekyll.configuration)
69 | end
70 | end
71 | end
72 | end
73 |
--------------------------------------------------------------------------------
/test/test_core_ext.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/helper'
2 |
3 | class TestCoreExt < Test::Unit::TestCase
4 | context "hash" do
5 |
6 | context "pluralized_array" do
7 |
8 | should "return empty array with no values" do
9 | data = {}
10 | assert_equal [], data.pluralized_array('tag', 'tags')
11 | end
12 |
13 | should "return empty array with no matching values" do
14 | data = { 'foo' => 'bar' }
15 | assert_equal [], data.pluralized_array('tag', 'tags')
16 | end
17 |
18 | should "return empty array with matching nil singular" do
19 | data = { 'foo' => 'bar', 'tag' => nil, 'tags' => ['dog', 'cat'] }
20 | assert_equal [], data.pluralized_array('tag', 'tags')
21 | end
22 |
23 | should "return single value array with matching singular" do
24 | data = { 'foo' => 'bar', 'tag' => 'dog', 'tags' => ['dog', 'cat'] }
25 | assert_equal ['dog'], data.pluralized_array('tag', 'tags')
26 | end
27 |
28 | should "return single value array with matching singular with spaces" do
29 | data = { 'foo' => 'bar', 'tag' => 'dog cat', 'tags' => ['dog', 'cat'] }
30 | assert_equal ['dog cat'], data.pluralized_array('tag', 'tags')
31 | end
32 |
33 | should "return empty array with matching nil plural" do
34 | data = { 'foo' => 'bar', 'tags' => nil }
35 | assert_equal [], data.pluralized_array('tag', 'tags')
36 | end
37 |
38 | should "return empty array with matching empty array" do
39 | data = { 'foo' => 'bar', 'tags' => [] }
40 | assert_equal [], data.pluralized_array('tag', 'tags')
41 | end
42 |
43 | should "return single value array with matching plural with single string value" do
44 | data = { 'foo' => 'bar', 'tags' => 'dog' }
45 | assert_equal ['dog'], data.pluralized_array('tag', 'tags')
46 | end
47 |
48 | should "return multiple value array with matching plural with single string value with spaces" do
49 | data = { 'foo' => 'bar', 'tags' => 'dog cat' }
50 | assert_equal ['dog', 'cat'], data.pluralized_array('tag', 'tags')
51 | end
52 |
53 | should "return single value array with matching plural with single value array" do
54 | data = { 'foo' => 'bar', 'tags' => ['dog'] }
55 | assert_equal ['dog'], data.pluralized_array('tag', 'tags')
56 | end
57 |
58 | should "return multiple value array with matching plural with multiple value array" do
59 | data = { 'foo' => 'bar', 'tags' => ['dog', 'cat'] }
60 | assert_equal ['dog', 'cat'], data.pluralized_array('tag', 'tags')
61 | end
62 |
63 | end
64 |
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/lib/jekyll/generators/pagination.rb:
--------------------------------------------------------------------------------
1 | module Jekyll
2 |
3 | class Pagination < Generator
4 | safe true
5 |
6 | def generate(site)
7 | site.pages.dup.each do |page|
8 | paginate(site, page) if Pager.pagination_enabled?(site.config, page.name)
9 | end
10 | end
11 |
12 | # Paginates the blog's posts. Renders the index.html file into paginated
13 | # directories, ie: page2/index.html, page3/index.html, etc and adds more
14 | # site-wide data.
15 | # +page+ is the index.html Page that requires pagination
16 | #
17 | # {"paginator" => { "page" => ,
18 | # "per_page" => ,
19 | # "posts" => [],
20 | # "total_posts" => ,
21 | # "total_pages" => ,
22 | # "previous_page" => ,
23 | # "next_page" => }}
24 | def paginate(site, page)
25 | all_posts = site.site_payload['site']['posts']
26 | pages = Pager.calculate_pages(all_posts, site.config['paginate'].to_i)
27 | (1..pages).each do |num_page|
28 | pager = Pager.new(site.config, num_page, all_posts, pages)
29 | if num_page > 1
30 | newpage = Page.new(site, site.source, page.dir, page.name)
31 | newpage.pager = pager
32 | newpage.dir = File.join(page.dir, "page#{num_page}")
33 | site.pages << newpage
34 | else
35 | page.pager = pager
36 | end
37 | end
38 | end
39 |
40 | end
41 |
42 | class Pager
43 | attr_reader :page, :per_page, :posts, :total_posts, :total_pages, :previous_page, :next_page
44 |
45 | def self.calculate_pages(all_posts, per_page)
46 | num_pages = all_posts.size / per_page.to_i
47 | num_pages = num_pages + 1 if all_posts.size % per_page.to_i != 0
48 | num_pages
49 | end
50 |
51 | def self.pagination_enabled?(config, file)
52 | file == 'index.html' && !config['paginate'].nil?
53 | end
54 |
55 | def initialize(config, page, all_posts, num_pages = nil)
56 | @page = page
57 | @per_page = config['paginate'].to_i
58 | @total_pages = num_pages || Pager.calculate_pages(all_posts, @per_page)
59 |
60 | if @page > @total_pages
61 | raise RuntimeError, "page number can't be greater than total pages: #{@page} > #{@total_pages}"
62 | end
63 |
64 | init = (@page - 1) * @per_page
65 | offset = (init + @per_page - 1) >= all_posts.size ? all_posts.size : (init + @per_page - 1)
66 |
67 | @total_posts = all_posts.size
68 | @posts = all_posts[init..offset]
69 | @previous_page = @page != 1 ? @page - 1 : nil
70 | @next_page = @page != @total_pages ? @page + 1 : nil
71 | end
72 |
73 | def to_liquid
74 | {
75 | 'page' => page,
76 | 'per_page' => per_page,
77 | 'posts' => posts,
78 | 'total_posts' => total_posts,
79 | 'total_pages' => total_pages,
80 | 'previous_page' => previous_page,
81 | 'next_page' => next_page
82 | }
83 | end
84 | end
85 |
86 |
87 | end
88 |
--------------------------------------------------------------------------------
/lib/jekyll/convertible.rb:
--------------------------------------------------------------------------------
1 | # Convertible provides methods for converting a pagelike item
2 | # from a certain type of markup into actual content
3 | #
4 | # Requires
5 | # self.site -> Jekyll::Site
6 | # self.content
7 | # self.content=
8 | # self.data=
9 | # self.ext=
10 | # self.output=
11 | module Jekyll
12 | module Convertible
13 | # Return the contents as a string
14 | def to_s
15 | self.content || ''
16 | end
17 |
18 | # Read the YAML frontmatter
19 | # +base+ is the String path to the dir containing the file
20 | # +name+ is the String filename of the file
21 | #
22 | # Returns nothing
23 | def read_yaml(base, name)
24 | self.content = File.read(File.join(base, name))
25 |
26 | if self.content =~ /^(---\s*\n.*?\n?)^(---\s*$\n?)/m
27 | self.content = self.content[($1.size + $2.size)..-1]
28 |
29 | begin
30 | self.data = YAML.load($1)
31 | rescue => e
32 | puts "YAML Exception: #{e.message}"
33 | end
34 | end
35 |
36 | self.data ||= {}
37 | end
38 |
39 | # Transform the contents based on the content type.
40 | #
41 | # Returns nothing
42 | def transform
43 | self.content = converter.convert(self.content)
44 | end
45 |
46 | # Determine the extension depending on content_type
47 | #
48 | # Returns the extensions for the output file
49 | def output_ext
50 | converter.output_ext(self.ext)
51 | end
52 |
53 | # Determine which converter to use based on this convertible's
54 | # extension
55 | def converter
56 | @converter ||= self.site.converters.find { |c| c.matches(self.ext) }
57 | end
58 |
59 | # Add any necessary layouts to this convertible document
60 | # +layouts+ is a Hash of {"name" => "layout"}
61 | # +site_payload+ is the site payload hash
62 | #
63 | # Returns nothing
64 | def do_layout(payload, layouts)
65 | info = { :filters => [Jekyll::Filters], :registers => { :site => self.site } }
66 |
67 | # render and transform content (this becomes the final content of the object)
68 | payload["pygments_prefix"] = converter.pygments_prefix
69 | payload["pygments_suffix"] = converter.pygments_suffix
70 |
71 | begin
72 | self.content = Liquid::Template.parse(self.content).render(payload, info)
73 | rescue => e
74 | puts "Liquid Exception: #{e.message} in #{self.data["layout"]}"
75 | end
76 |
77 | self.transform
78 |
79 | # output keeps track of what will finally be written
80 | self.output = self.content
81 |
82 | # recursively render layouts
83 | layout = layouts[self.data["layout"]]
84 | while layout
85 | payload = payload.deep_merge({"content" => self.output, "page" => layout.data})
86 |
87 | begin
88 | self.output = Liquid::Template.parse(layout.content).render(payload, info)
89 | rescue => e
90 | puts "Liquid Exception: #{e.message} in #{self.data["layout"]}"
91 | end
92 |
93 | layout = layouts[layout.data["layout"]]
94 | end
95 | end
96 | end
97 | end
98 |
--------------------------------------------------------------------------------
/features/embed_filters.feature:
--------------------------------------------------------------------------------
1 | Feature: Embed filters
2 | As a hacker who likes to blog
3 | I want to be able to transform text inside a post or page
4 | In order to perform cool stuff in my posts
5 |
6 | Scenario: Convert date to XML schema
7 | Given I have a _posts directory
8 | And I have a _layouts directory
9 | And I have the following post:
10 | | title | date | layout | content |
11 | | Star Wars | 3/27/2009 | default | These aren't the droids you're looking for. |
12 | And I have a default layout that contains "{{ site.time | date_to_xmlschema }}"
13 | When I run jekyll
14 | Then the _site directory should exist
15 | And I should see today's date in "_site/2009/03/27/star-wars.html"
16 |
17 | Scenario: Escape text for XML
18 | Given I have a _posts directory
19 | And I have a _layouts directory
20 | And I have the following post:
21 | | title | date | layout | content |
22 | | Star & Wars | 3/27/2009 | default | These aren't the droids you're looking for. |
23 | And I have a default layout that contains "{{ page.title | xml_escape }}"
24 | When I run jekyll
25 | Then the _site directory should exist
26 | And I should see "Star & Wars" in "_site/2009/03/27/star-wars.html"
27 |
28 | Scenario: Calculate number of words
29 | Given I have a _posts directory
30 | And I have a _layouts directory
31 | And I have the following post:
32 | | title | date | layout | content |
33 | | Star Wars | 3/27/2009 | default | These aren't the droids you're looking for. |
34 | And I have a default layout that contains "{{ content | xml_escape }}"
35 | When I run jekyll
36 | Then the _site directory should exist
37 | And I should see "7" in "_site/2009/03/27/star-wars.html"
38 |
39 | Scenario: Convert an array into a sentence
40 | Given I have a _posts directory
41 | And I have a _layouts directory
42 | And I have the following post:
43 | | title | date | layout | tags | content |
44 | | Star Wars | 3/27/2009 | default | [scifi, movies, force] | These aren't the droids you're looking for. |
45 | And I have a default layout that contains "{{ page.tags | array_to_sentence_string }}"
46 | When I run jekyll
47 | Then the _site directory should exist
48 | And I should see "scifi, movies, and force" in "_site/2009/03/27/star-wars.html"
49 |
50 | Scenario: Textilize a given string
51 | Given I have a _posts directory
52 | And I have a _layouts directory
53 | And I have the following post:
54 | | title | date | layout | content |
55 | | Star Wars | 3/27/2009 | default | These aren't the droids you're looking for. |
56 | And I have a default layout that contains "By {{ '_Obi-wan_' | textilize }}"
57 | When I run jekyll
58 | Then the _site directory should exist
59 | And I should see "By
Obi-wan
" in "_site/2009/03/27/star-wars.html"
60 |
61 |
--------------------------------------------------------------------------------
/lib/jekyll/migrators/mephisto.rb:
--------------------------------------------------------------------------------
1 | # Quickly hacked together my Michael Ivey
2 | # Based on mt.rb by Nick Gerakines, open source and publically
3 | # available under the MIT license. Use this module at your own risk.
4 |
5 | require 'rubygems'
6 | require 'sequel'
7 | require 'fastercsv'
8 | require 'fileutils'
9 | require File.join(File.dirname(__FILE__),"csv.rb")
10 |
11 | # NOTE: This converter requires Sequel and the MySQL gems.
12 | # The MySQL gem can be difficult to install on OS X. Once you have MySQL
13 | # installed, running the following commands should work:
14 | # $ sudo gem install sequel
15 | # $ sudo gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config
16 |
17 | module Jekyll
18 | module Mephisto
19 | #Accepts a hash with database config variables, exports mephisto posts into a csv
20 | #export PGPASSWORD if you must
21 | def self.postgres(c)
22 | sql = <<-SQL
23 | BEGIN;
24 | CREATE TEMP TABLE jekyll AS
25 | SELECT title, permalink, body, published_at, filter FROM contents
26 | WHERE user_id = 1 AND type = 'Article' ORDER BY published_at;
27 | COPY jekyll TO STDOUT WITH CSV HEADER;
28 | ROLLBACK;
29 | SQL
30 | command = %Q(psql -h #{c[:host] || "localhost"} -c "#{sql.strip}" #{c[:database]} #{c[:username]} -o #{c[:filename] || "posts.csv"})
31 | puts command
32 | `#{command}`
33 | CSV.process
34 | end
35 |
36 | # This query will pull blog posts from all entries across all blogs. If
37 | # you've got unpublished, deleted or otherwise hidden posts please sift
38 | # through the created posts to make sure nothing is accidently published.
39 |
40 | QUERY = "SELECT id, permalink, body, published_at, title FROM contents WHERE user_id = 1 AND type = 'Article' AND published_at IS NOT NULL ORDER BY published_at"
41 |
42 | def self.process(dbname, user, pass, host = 'localhost')
43 | db = Sequel.mysql(dbname, :user => user, :password => pass, :host => host, :encoding => 'utf8')
44 |
45 | FileUtils.mkdir_p "_posts"
46 |
47 | db[QUERY].each do |post|
48 | title = post[:title]
49 | slug = post[:permalink]
50 | date = post[:published_at]
51 | content = post[:body]
52 | # more_content = ''
53 |
54 | # Be sure to include the body and extended body.
55 | # if more_content != nil
56 | # content = content + " \n" + more_content
57 | # end
58 |
59 | # Ideally, this script would determine the post format (markdown, html
60 | # , etc) and create files with proper extensions. At this point it
61 | # just assumes that markdown will be acceptable.
62 | name = [date.year, date.month, date.day, slug].join('-') + ".markdown"
63 |
64 | data = {
65 | 'layout' => 'post',
66 | 'title' => title.to_s,
67 | 'mt_id' => post[:entry_id],
68 | }.delete_if { |k,v| v.nil? || v == ''}.to_yaml
69 |
70 | File.open("_posts/#{name}", "w") do |f|
71 | f.puts data
72 | f.puts "---"
73 | f.puts content
74 | end
75 | end
76 |
77 | end
78 | end
79 | end
80 |
--------------------------------------------------------------------------------
/lib/jekyll/migrators/mt.rb:
--------------------------------------------------------------------------------
1 | # Created by Nick Gerakines, open source and publically available under the
2 | # MIT license. Use this module at your own risk.
3 | # I'm an Erlang/Perl/C++ guy so please forgive my dirty ruby.
4 |
5 | require 'rubygems'
6 | require 'sequel'
7 | require 'fileutils'
8 | require 'yaml'
9 |
10 | # NOTE: This converter requires Sequel and the MySQL gems.
11 | # The MySQL gem can be difficult to install on OS X. Once you have MySQL
12 | # installed, running the following commands should work:
13 | # $ sudo gem install sequel
14 | # $ sudo gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config
15 |
16 | module Jekyll
17 | module MT
18 | # This query will pull blog posts from all entries across all blogs. If
19 | # you've got unpublished, deleted or otherwise hidden posts please sift
20 | # through the created posts to make sure nothing is accidently published.
21 | QUERY = "SELECT entry_id, entry_basename, entry_text, entry_text_more, entry_authored_on, entry_title, entry_convert_breaks FROM mt_entry"
22 |
23 | def self.process(dbname, user, pass, host = 'localhost')
24 | db = Sequel.mysql(dbname, :user => user, :password => pass, :host => host, :encoding => 'utf8')
25 |
26 | FileUtils.mkdir_p "_posts"
27 |
28 | db[QUERY].each do |post|
29 | title = post[:entry_title]
30 | slug = post[:entry_basename].gsub(/_/, '-')
31 | date = post[:entry_authored_on]
32 | content = post[:entry_text]
33 | more_content = post[:entry_text_more]
34 | entry_convert_breaks = post[:entry_convert_breaks]
35 |
36 | # Be sure to include the body and extended body.
37 | if more_content != nil
38 | content = content + " \n" + more_content
39 | end
40 |
41 | # Ideally, this script would determine the post format (markdown, html
42 | # , etc) and create files with proper extensions. At this point it
43 | # just assumes that markdown will be acceptable.
44 | name = [date.year, date.month, date.day, slug].join('-') + '.' + self.suffix(entry_convert_breaks)
45 |
46 | data = {
47 | 'layout' => 'post',
48 | 'title' => title.to_s,
49 | 'mt_id' => post[:entry_id],
50 | 'date' => date
51 | }.delete_if { |k,v| v.nil? || v == ''}.to_yaml
52 |
53 | File.open("_posts/#{name}", "w") do |f|
54 | f.puts data
55 | f.puts "---"
56 | f.puts content
57 | end
58 | end
59 | end
60 |
61 | def self.suffix(entry_type)
62 | if entry_type.nil? || entry_type.include?("markdown")
63 | # The markdown plugin I have saves this as "markdown_with_smarty_pants", so I just look for "markdown".
64 | "markdown"
65 | elsif entry_type.include?("textile")
66 | # This is saved as "textile_2" on my installation of MT 5.1.
67 | "textile"
68 | elsif entry_type == "0" || entry_type.include?("richtext")
69 | # richtext looks to me like it's saved as HTML, so I include it here.
70 | "html"
71 | else
72 | # Other values might need custom work.
73 | entry_type
74 | end
75 | end
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/lib/jekyll/migrators/drupal.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'sequel'
3 | require 'fileutils'
4 | require 'yaml'
5 |
6 | # NOTE: This converter requires Sequel and the MySQL gems.
7 | # The MySQL gem can be difficult to install on OS X. Once you have MySQL
8 | # installed, running the following commands should work:
9 | # $ sudo gem install sequel
10 | # $ sudo gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config
11 |
12 | module Jekyll
13 | module Drupal
14 |
15 | # Reads a MySQL database via Sequel and creates a post file for each
16 | # post in wp_posts that has post_status = 'publish'.
17 | # This restriction is made because 'draft' posts are not guaranteed to
18 | # have valid dates.
19 | QUERY = "SELECT node.nid, node.title, node_revisions.body, node.created, node.status FROM node, node_revisions WHERE (node.type = 'blog' OR node.type = 'story') AND node.vid = node_revisions.vid"
20 |
21 | def self.process(dbname, user, pass, host = 'localhost')
22 | db = Sequel.mysql(dbname, :user => user, :password => pass, :host => host, :encoding => 'utf8')
23 |
24 | FileUtils.mkdir_p "_posts"
25 | FileUtils.mkdir_p "_drafts"
26 |
27 | # Create the refresh layout
28 | # Change the refresh url if you customized your permalink config
29 | File.open("_layouts/refresh.html", "w") do |f|
30 | f.puts <
32 |
33 |
34 |
35 |
36 |
37 |
38 | EOF
39 | end
40 |
41 | db[QUERY].each do |post|
42 | # Get required fields and construct Jekyll compatible name
43 | node_id = post[:nid]
44 | title = post[:title]
45 | content = post[:body]
46 | created = post[:created]
47 | time = Time.at(created)
48 | is_published = post[:status] == 1
49 | dir = is_published ? "_posts" : "_drafts"
50 | slug = title.strip.downcase.gsub(/(&|&)/, ' and ').gsub(/[\s\.\/\\]/, '-').gsub(/[^\w-]/, '').gsub(/[-_]{2,}/, '-').gsub(/^[-_]/, '').gsub(/[-_]$/, '')
51 | name = time.strftime("%Y-%m-%d-") + slug + '.md'
52 |
53 | # Get the relevant fields as a hash, delete empty fields and convert
54 | # to YAML for the header
55 | data = {
56 | 'layout' => 'post',
57 | 'title' => title.to_s,
58 | 'created' => created,
59 | }.delete_if { |k,v| v.nil? || v == ''}.to_yaml
60 |
61 | # Write out the data and content to file
62 | File.open("#{dir}/#{name}", "w") do |f|
63 | f.puts data
64 | f.puts "---"
65 | f.puts content
66 | end
67 |
68 | # Make a file to redirect from the old Drupal URL
69 | if is_published
70 | FileUtils.mkdir_p "node/#{node_id}"
71 | File.open("node/#{node_id}/index.md", "w") do |f|
72 | f.puts "---"
73 | f.puts "layout: refresh"
74 | f.puts "refresh_to_post_id: /#{time.strftime("%Y/%m/%d/") + slug}"
75 | f.puts "---"
76 | end
77 | end
78 | end
79 |
80 | # TODO: Make dirs & files for nodes of type 'page'
81 | # Make refresh pages for these as well
82 |
83 | # TODO: Make refresh dirs & files according to entries in url_alias table
84 | end
85 | end
86 | end
87 |
--------------------------------------------------------------------------------
/features/permalinks.feature:
--------------------------------------------------------------------------------
1 | Feature: Fancy permalinks
2 | As a hacker who likes to blog
3 | I want to be able to set permalinks
4 | In order to make my blog URLs awesome
5 |
6 | Scenario: Use none permalink schema
7 | Given I have a _posts directory
8 | And I have the following post:
9 | | title | date | content |
10 | | None Permalink Schema | 3/27/2009 | Totally nothing. |
11 | And I have a configuration file with "permalink" set to "none"
12 | When I run jekyll
13 | Then the _site directory should exist
14 | And I should see "Totally nothing." in "_site/none-permalink-schema.html"
15 |
16 | Scenario: Use pretty permalink schema
17 | Given I have a _posts directory
18 | And I have the following post:
19 | | title | date | content |
20 | | Pretty Permalink Schema | 3/27/2009 | Totally wordpress. |
21 | And I have a configuration file with "permalink" set to "pretty"
22 | When I run jekyll
23 | Then the _site directory should exist
24 | And I should see "Totally wordpress." in "_site/2009/03/27/pretty-permalink-schema/index.html"
25 |
26 | Scenario: Use pretty permalink schema for pages
27 | Given I have an "index.html" page that contains "Totally index"
28 | And I have an "awesome.html" page that contains "Totally awesome"
29 | And I have an "sitemap.xml" page that contains "Totally uhm, sitemap"
30 | And I have a configuration file with "permalink" set to "pretty"
31 | When I run jekyll
32 | Then the _site directory should exist
33 | And I should see "Totally index" in "_site/index.html"
34 | And I should see "Totally awesome" in "_site/awesome/index.html"
35 | And I should see "Totally uhm, sitemap" in "_site/sitemap.xml"
36 |
37 | Scenario: Use custom permalink schema with prefix
38 | Given I have a _posts directory
39 | And I have the following post:
40 | | title | category | date | content |
41 | | Custom Permalink Schema | stuff | 3/27/2009 | Totally custom. |
42 | And I have a configuration file with "permalink" set to "/blog/:year/:month/:day/:title"
43 | When I run jekyll
44 | Then the _site directory should exist
45 | And I should see "Totally custom." in "_site/blog/2009/03/27/custom-permalink-schema/index.html"
46 |
47 | Scenario: Use custom permalink schema with category
48 | Given I have a _posts directory
49 | And I have the following post:
50 | | title | category | date | content |
51 | | Custom Permalink Schema | stuff | 3/27/2009 | Totally custom. |
52 | And I have a configuration file with "permalink" set to "/:categories/:title.html"
53 | When I run jekyll
54 | Then the _site directory should exist
55 | And I should see "Totally custom." in "_site/stuff/custom-permalink-schema.html"
56 |
57 | Scenario: Use custom permalink schema with squished date
58 | Given I have a _posts directory
59 | And I have the following post:
60 | | title | category | date | content |
61 | | Custom Permalink Schema | stuff | 3/27/2009 | Totally custom. |
62 | And I have a configuration file with "permalink" set to "/:month-:day-:year/:title.html"
63 | When I run jekyll
64 | Then the _site directory should exist
65 | And I should see "Totally custom." in "_site/03-27-2009/custom-permalink-schema.html"
66 |
--------------------------------------------------------------------------------
/lib/jekyll/albino.rb:
--------------------------------------------------------------------------------
1 | ##
2 | # Wrapper for the Pygments command line tool, pygmentize.
3 | #
4 | # Pygments: http://pygments.org/
5 | #
6 | # Assumes pygmentize is in the path. If not, set its location
7 | # with Albino.bin = '/path/to/pygmentize'
8 | #
9 | # Use like so:
10 | #
11 | # @syntaxer = Albino.new('/some/file.rb', :ruby)
12 | # puts @syntaxer.colorize
13 | #
14 | # This'll print out an HTMLized, Ruby-highlighted version
15 | # of '/some/file.rb'.
16 | #
17 | # To use another formatter, pass it as the third argument:
18 | #
19 | # @syntaxer = Albino.new('/some/file.rb', :ruby, :bbcode)
20 | # puts @syntaxer.colorize
21 | #
22 | # You can also use the #colorize class method:
23 | #
24 | # puts Albino.colorize('/some/file.rb', :ruby)
25 | #
26 | # Another also: you get a #to_s, for somewhat nicer use in Rails views.
27 | #
28 | # ... helper file ...
29 | # def highlight(text)
30 | # Albino.new(text, :ruby)
31 | # end
32 | #
33 | # ... view file ...
34 | # <%= highlight text %>
35 | #
36 | # The default lexer is 'text'. You need to specify a lexer yourself;
37 | # because we are using STDIN there is no auto-detect.
38 | #
39 | # To see all lexers and formatters available, run `pygmentize -L`.
40 | #
41 | # Chris Wanstrath // chris@ozmm.org
42 | # GitHub // http://github.com
43 | #
44 |
45 | class Albino
46 | @@bin = Rails.development? ? 'pygmentize' : '/usr/bin/pygmentize' rescue 'pygmentize'
47 |
48 | def self.bin=(path)
49 | @@bin = path
50 | end
51 |
52 | def self.colorize(*args)
53 | new(*args).colorize
54 | end
55 |
56 | def initialize(target, lexer = :text, format = :html)
57 | @target = target
58 | @options = { :l => lexer, :f => format, :O => 'encoding=utf-8' }
59 | end
60 |
61 | def execute(command)
62 | output = ''
63 | IO.popen(command, mode='r+') do |p|
64 | p.write @target
65 | p.close_write
66 | output = p.read.strip
67 | end
68 | output
69 | end
70 |
71 | def colorize(options = {})
72 | html = execute(@@bin + convert_options(options))
73 | # Work around an RDiscount bug: http://gist.github.com/97682
74 | html.to_s.sub(%r{
\Z}, "\n")
75 | end
76 | alias_method :to_s, :colorize
77 |
78 | def convert_options(options = {})
79 | @options.merge(options).inject('') do |string, (flag, value)|
80 | string + " -#{flag} #{value}"
81 | end
82 | end
83 | end
84 |
85 | if $0 == __FILE__
86 | require 'rubygems'
87 | require 'test/spec'
88 | require 'mocha'
89 | begin require 'redgreen'; rescue LoadError; end
90 |
91 | context "Albino" do
92 | setup do
93 | @syntaxer = Albino.new(__FILE__, :ruby)
94 | end
95 |
96 | specify "defaults to text" do
97 | syntaxer = Albino.new(__FILE__)
98 | syntaxer.expects(:execute).with('pygmentize -f html -l text').returns(true)
99 | syntaxer.colorize
100 | end
101 |
102 | specify "accepts options" do
103 | @syntaxer.expects(:execute).with('pygmentize -f html -l ruby').returns(true)
104 | @syntaxer.colorize
105 | end
106 |
107 | specify "works with strings" do
108 | syntaxer = Albino.new('class New; end', :ruby)
109 | assert_match %r(highlight), syntaxer.colorize
110 | end
111 |
112 | specify "aliases to_s" do
113 | assert_equal @syntaxer.colorize, @syntaxer.to_s
114 | end
115 |
116 | specify "class method colorize" do
117 | assert_equal @syntaxer.colorize, Albino.colorize(__FILE__, :ruby)
118 | end
119 | end
120 | end
121 |
--------------------------------------------------------------------------------
/test/test_pager.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/helper'
2 |
3 | class TestPager < Test::Unit::TestCase
4 |
5 | should "calculate number of pages" do
6 | assert_equal(0, Pager.calculate_pages([], '2'))
7 | assert_equal(1, Pager.calculate_pages([1], '2'))
8 | assert_equal(1, Pager.calculate_pages([1,2], '2'))
9 | assert_equal(2, Pager.calculate_pages([1,2,3], '2'))
10 | assert_equal(2, Pager.calculate_pages([1,2,3,4], '2'))
11 | assert_equal(3, Pager.calculate_pages([1,2,3,4,5], '2'))
12 | end
13 |
14 | context "pagination disabled" do
15 | setup do
16 | stub(Jekyll).configuration do
17 | Jekyll::DEFAULTS.merge({
18 | 'source' => source_dir,
19 | 'destination' => dest_dir
20 | })
21 | end
22 | @config = Jekyll.configuration
23 | end
24 |
25 | should "report that pagination is disabled" do
26 | assert !Pager.pagination_enabled?(@config, 'index.html')
27 | end
28 |
29 | end
30 |
31 | context "pagination enabled for 2" do
32 | setup do
33 | stub(Jekyll).configuration do
34 | Jekyll::DEFAULTS.merge({
35 | 'source' => source_dir,
36 | 'destination' => dest_dir,
37 | 'paginate' => 2
38 | })
39 | end
40 |
41 | @config = Jekyll.configuration
42 | @site = Site.new(@config)
43 | @site.process
44 | @posts = @site.posts
45 | end
46 |
47 | should "report that pagination is enabled" do
48 | assert Pager.pagination_enabled?(@config, 'index.html')
49 | end
50 |
51 | context "with 4 posts" do
52 | setup do
53 | @posts = @site.posts[1..4] # limit to 4
54 | end
55 |
56 | should "create first pager" do
57 | pager = Pager.new(@config, 1, @posts)
58 | assert_equal(2, pager.posts.size)
59 | assert_equal(2, pager.total_pages)
60 | assert_nil(pager.previous_page)
61 | assert_equal(2, pager.next_page)
62 | end
63 |
64 | should "create second pager" do
65 | pager = Pager.new(@config, 2, @posts)
66 | assert_equal(2, pager.posts.size)
67 | assert_equal(2, pager.total_pages)
68 | assert_equal(1, pager.previous_page)
69 | assert_nil(pager.next_page)
70 | end
71 |
72 | should "not create third pager" do
73 | assert_raise(RuntimeError) { Pager.new(@config, 3, @posts) }
74 | end
75 |
76 | end
77 |
78 | context "with 5 posts" do
79 | setup do
80 | @posts = @site.posts[1..5] # limit to 5
81 | end
82 |
83 | should "create first pager" do
84 | pager = Pager.new(@config, 1, @posts)
85 | assert_equal(2, pager.posts.size)
86 | assert_equal(3, pager.total_pages)
87 | assert_nil(pager.previous_page)
88 | assert_equal(2, pager.next_page)
89 | end
90 |
91 | should "create second pager" do
92 | pager = Pager.new(@config, 2, @posts)
93 | assert_equal(2, pager.posts.size)
94 | assert_equal(3, pager.total_pages)
95 | assert_equal(1, pager.previous_page)
96 | assert_equal(3, pager.next_page)
97 | end
98 |
99 | should "create third pager" do
100 | pager = Pager.new(@config, 3, @posts)
101 | assert_equal(1, pager.posts.size)
102 | assert_equal(3, pager.total_pages)
103 | assert_equal(2, pager.previous_page)
104 | assert_nil(pager.next_page)
105 | end
106 |
107 | should "not create fourth pager" do
108 | assert_raise(RuntimeError) { Pager.new(@config, 4, @posts) }
109 | end
110 |
111 | end
112 | end
113 | end
114 |
--------------------------------------------------------------------------------
/test/test_tags.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/helper'
2 |
3 | class TestTags < Test::Unit::TestCase
4 |
5 | def create_post(content, override = {}, converter_class = Jekyll::MarkdownConverter)
6 | stub(Jekyll).configuration do
7 | Jekyll::DEFAULTS.merge({'pygments' => true}).merge(override)
8 | end
9 | site = Site.new(Jekyll.configuration)
10 | info = { :filters => [Jekyll::Filters], :registers => { :site => site } }
11 | @converter = site.converters.find { |c| c.class == converter_class }
12 | payload = { "pygments_prefix" => @converter.pygments_prefix,
13 | "pygments_suffix" => @converter.pygments_suffix }
14 |
15 | @result = Liquid::Template.parse(content).render(payload, info)
16 | @result = @converter.convert(@result)
17 | end
18 |
19 | def fill_post(code, override = {})
20 | content = <test\n}, @result
43 | end
44 | end
45 |
46 | context "post content has highlight with file reference" do
47 | setup do
48 | fill_post("./jekyll.gemspec")
49 | end
50 |
51 | should "not embed the file" do
52 | assert_match %{
./jekyll.gemspec\n
}, @result
53 | end
54 | end
55 |
56 | context "post content has highlight tag with UTF character" do
57 | setup do
58 | fill_post("Æ")
59 | end
60 |
61 | should "render markdown with pygments line handling" do
62 | assert_match %{
Æ\n
}, @result
63 | end
64 | end
65 |
66 | context "simple post with markdown and pre tags" do
67 | setup do
68 | @content = < }, @result
91 | end
92 | end
93 |
94 | context "using Maruku" do
95 | setup do
96 | create_post(@content)
97 | end
98 |
99 | should "parse correctly" do
100 | assert_match %r{FIGHT!}, @result
101 | assert_match %r{FINISH HIM}, @result
102 | end
103 | end
104 |
105 | context "using RDiscount" do
106 | setup do
107 | create_post(@content, 'markdown' => 'rdiscount')
108 | end
109 |
110 | should "parse correctly" do
111 | assert_match %r{FIGHT!}, @result
112 | assert_match %r{FINISH HIM}, @result
113 | end
114 | end
115 |
116 | context "using Kramdown" do
117 | setup do
118 | create_post(@content, 'markdown' => 'kramdown')
119 | end
120 |
121 | should "parse correctly" do
122 | assert_match %r{FIGHT!}, @result
123 | assert_match %r{FINISH HIM}, @result
124 | end
125 | end
126 | end
127 | end
128 |
--------------------------------------------------------------------------------
/test/test_page.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/helper'
2 |
3 | class TestPage < Test::Unit::TestCase
4 | def setup_page(file)
5 | @page = Page.new(@site, source_dir, '', file)
6 | end
7 |
8 | def do_render(page)
9 | layouts = { "default" => Layout.new(@site, source_dir('_layouts'), "simple.html")}
10 | page.render(layouts, {"site" => {"posts" => []}})
11 | end
12 |
13 | context "A Page" do
14 | setup do
15 | clear_dest
16 | stub(Jekyll).configuration { Jekyll::DEFAULTS }
17 | @site = Site.new(Jekyll.configuration)
18 | end
19 |
20 | context "processing pages" do
21 | should "create url based on filename" do
22 | @page = setup_page('contacts.html')
23 | assert_equal "/contacts.html", @page.url
24 | end
25 |
26 | should "deal properly with extensions" do
27 | @page = setup_page('deal.with.dots.html')
28 | assert_equal ".html", @page.ext
29 | end
30 |
31 | should "deal properly with dots" do
32 | @page = setup_page('deal.with.dots.html')
33 | assert_equal "deal.with.dots", @page.basename
34 | end
35 |
36 | context "with pretty url style" do
37 | setup do
38 | @site.permalink_style = :pretty
39 | end
40 |
41 | should "return dir correctly" do
42 | @page = setup_page('contacts.html')
43 | assert_equal '/contacts/', @page.dir
44 | end
45 |
46 | should "return dir correctly for index page" do
47 | @page = setup_page('index.html')
48 | assert_equal '/', @page.dir
49 | end
50 | end
51 |
52 | context "with any other url style" do
53 | should "return dir correctly" do
54 | @site.permalink_style = nil
55 | @page = setup_page('contacts.html')
56 | assert_equal '/', @page.dir
57 | end
58 | end
59 |
60 | should "respect permalink in yaml front matter" do
61 | file = "about.html"
62 | @page = setup_page(file)
63 |
64 | assert_equal "/about/", @page.permalink
65 | assert_equal @page.permalink, @page.url
66 | assert_equal "/about/", @page.dir
67 | end
68 | end
69 |
70 | context "rendering" do
71 | setup do
72 | clear_dest
73 | end
74 |
75 | should "write properly" do
76 | page = setup_page('contacts.html')
77 | do_render(page)
78 | page.write(dest_dir)
79 |
80 | assert File.directory?(dest_dir)
81 | assert File.exists?(File.join(dest_dir, 'contacts.html'))
82 | end
83 |
84 | should "write properly without html extension" do
85 | page = setup_page('contacts.html')
86 | page.site.permalink_style = :pretty
87 | do_render(page)
88 | page.write(dest_dir)
89 |
90 | assert File.directory?(dest_dir)
91 | assert File.exists?(File.join(dest_dir, 'contacts', 'index.html'))
92 | end
93 |
94 | should "write properly with extension different from html" do
95 | page = setup_page("sitemap.xml")
96 | page.site.permalink_style = :pretty
97 | do_render(page)
98 | page.write(dest_dir)
99 |
100 | assert_equal("/sitemap.xml", page.url)
101 | assert_nil(page.url[/\.html$/])
102 | assert File.directory?(dest_dir)
103 | assert File.exists?(File.join(dest_dir,'sitemap.xml'))
104 | end
105 |
106 | should "write dotfiles properly" do
107 | page = setup_page('.htaccess')
108 | do_render(page)
109 | page.write(dest_dir)
110 |
111 | assert File.directory?(dest_dir)
112 | assert File.exists?(File.join(dest_dir, '.htaccess'))
113 | end
114 | end
115 |
116 | end
117 | end
118 |
--------------------------------------------------------------------------------
/lib/jekyll/page.rb:
--------------------------------------------------------------------------------
1 | module Jekyll
2 |
3 | class Page
4 | include Convertible
5 |
6 | attr_accessor :site, :pager
7 | attr_accessor :name, :ext, :basename, :dir
8 | attr_accessor :data, :content, :output
9 |
10 | # Initialize a new Page.
11 | # +site+ is the Site
12 | # +base+ is the String path to the
13 | # +dir+ is the String path between and the file
14 | # +name+ is the String filename of the file
15 | #
16 | # Returns
17 | def initialize(site, base, dir, name)
18 | @site = site
19 | @base = base
20 | @dir = dir
21 | @name = name
22 |
23 | self.process(name)
24 | self.read_yaml(File.join(base, dir), name)
25 | self.data['layout'] ||= 'page'
26 | end
27 |
28 | # The generated directory into which the page will be placed
29 | # upon generation. This is derived from the permalink or, if
30 | # permalink is absent, set to '/'
31 | #
32 | # Returns
33 | def dir
34 | url[-1, 1] == '/' ? url : File.dirname(url)
35 | end
36 |
37 | # The full path and filename of the post.
38 | # Defined in the YAML of the post body
39 | # (Optional)
40 | #
41 | # Returns
42 | def permalink
43 | self.data && self.data['permalink']
44 | end
45 |
46 | def template
47 | if self.site.permalink_style == :pretty && !index? && html?
48 | "/:basename/"
49 | else
50 | "/:basename:output_ext"
51 | end
52 | end
53 |
54 | # The generated relative url of this page
55 | # e.g. /about.html
56 | #
57 | # Returns
58 | def url
59 | return permalink if permalink
60 |
61 | @url ||= {
62 | "basename" => self.basename,
63 | "output_ext" => self.output_ext,
64 | }.inject(template) { |result, token|
65 | result.gsub(/:#{token.first}/, token.last)
66 | }.gsub(/\/\//, "/")
67 | end
68 |
69 | # Extract information from the page filename
70 | # +name+ is the String filename of the page file
71 | #
72 | # Returns nothing
73 | def process(name)
74 | self.ext = File.extname(name)
75 | self.basename = name[0 .. -self.ext.length-1]
76 | end
77 |
78 | # Add any necessary layouts to this post
79 | # +layouts+ is a Hash of {"name" => "layout"}
80 | # +site_payload+ is the site payload hash
81 | #
82 | # Returns nothing
83 | def render(layouts, site_payload)
84 | payload = {
85 | "page" => self.to_liquid,
86 | 'paginator' => pager.to_liquid
87 | }.deep_merge(site_payload)
88 |
89 | do_layout(payload, layouts)
90 | end
91 |
92 | def to_liquid
93 | self.data.deep_merge({
94 | "url" => File.join(@dir, self.url),
95 | "content" => self.content })
96 | end
97 |
98 | # Obtain destination path.
99 | # +dest+ is the String path to the destination dir
100 | #
101 | # Returns destination file path.
102 | def destination(dest)
103 | # The url needs to be unescaped in order to preserve the correct filename
104 | path = File.join(dest, @dir, CGI.unescape(self.url))
105 | path = File.join(path, "index.html") if self.url =~ /\/$/
106 | path
107 | end
108 |
109 | # Write the generated page file to the destination directory.
110 | # +dest+ is the String path to the destination dir
111 | #
112 | # Returns nothing
113 | def write(dest)
114 | path = destination(dest)
115 | FileUtils.mkdir_p(File.dirname(path))
116 | File.open(path, 'w') do |f|
117 | f.write(self.output)
118 | end
119 | end
120 |
121 | def inspect
122 | "#"
123 | end
124 |
125 | def html?
126 | output_ext == '.html'
127 | end
128 |
129 | def index?
130 | basename == 'index'
131 | end
132 |
133 | end
134 |
135 | end
136 |
--------------------------------------------------------------------------------
/features/site_data.feature:
--------------------------------------------------------------------------------
1 | Feature: Site data
2 | As a hacker who likes to blog
3 | I want to be able to embed data into my site
4 | In order to make the site slightly dynamic
5 |
6 | Scenario: Use page variable in a page
7 | Given I have an "contact.html" page with title "Contact" that contains "{{ page.title }}: email@me.com"
8 | When I run jekyll
9 | Then the _site directory should exist
10 | And I should see "Contact: email@me.com" in "_site/contact.html"
11 |
12 | Scenario: Use site.time variable
13 | Given I have an "index.html" page that contains "{{ site.time }}"
14 | When I run jekyll
15 | Then the _site directory should exist
16 | And I should see today's time in "_site/index.html"
17 |
18 | Scenario: Use site.posts variable for latest post
19 | Given I have a _posts directory
20 | And I have an "index.html" page that contains "{{ site.posts.first.title }}: {{ site.posts.first.url }}"
21 | And I have the following posts:
22 | | title | date | content |
23 | | First Post | 3/25/2009 | My First Post |
24 | | Second Post | 3/26/2009 | My Second Post |
25 | | Third Post | 3/27/2009 | My Third Post |
26 | When I run jekyll
27 | Then the _site directory should exist
28 | And I should see "Third Post: /2009/03/27/third-post.html" in "_site/index.html"
29 |
30 | Scenario: Use site.posts variable in a loop
31 | Given I have a _posts directory
32 | And I have an "index.html" page that contains "{% for post in site.posts %} {{ post.title }} {% endfor %}"
33 | And I have the following posts:
34 | | title | date | content |
35 | | First Post | 3/25/2009 | My First Post |
36 | | Second Post | 3/26/2009 | My Second Post |
37 | | Third Post | 3/27/2009 | My Third Post |
38 | When I run jekyll
39 | Then the _site directory should exist
40 | And I should see "Third Post Second Post First Post" in "_site/index.html"
41 |
42 | Scenario: Use site.categories.code variable
43 | Given I have a _posts directory
44 | And I have an "index.html" page that contains "{% for post in site.categories.code %} {{ post.title }} {% endfor %}"
45 | And I have the following posts:
46 | | title | date | category | content |
47 | | Awesome Hack | 3/26/2009 | code | puts 'Hello World' |
48 | | Delicious Beer | 3/26/2009 | food | 1) Yuengling |
49 | When I run jekyll
50 | Then the _site directory should exist
51 | And I should see "Awesome Hack" in "_site/index.html"
52 |
53 | Scenario: Use site.tags variable
54 | Given I have a _posts directory
55 | And I have an "index.html" page that contains "{% for post in site.tags.beer %} {{ post.content }} {% endfor %}"
56 | And I have the following posts:
57 | | title | date | tag | content |
58 | | Delicious Beer | 3/26/2009 | beer | 1) Yuengling |
59 | When I run jekyll
60 | Then the _site directory should exist
61 | And I should see "Yuengling" in "_site/index.html"
62 |
63 | Scenario: Order Posts by name when on the same date
64 | Given I have a _posts directory
65 | And I have an "index.html" page that contains "{% for post in site.posts %}{{ post.title }}:{{ post.previous.title}},{{ post.next.title}} {% endfor %}"
66 | And I have the following posts:
67 | | title | date | content |
68 | | first | 2/26/2009 | first |
69 | | A | 3/26/2009 | A |
70 | | B | 3/26/2009 | B |
71 | | C | 3/26/2009 | C |
72 | | last | 4/26/2009 | last |
73 | When I run jekyll
74 | Then the _site directory should exist
75 | And I should see "last:C, C:B,last B:A,C A:first,B first:,A" in "_site/index.html"
76 |
77 | Scenario: Use configuration date in site payload
78 | Given I have an "index.html" page that contains "{{ site.url }}"
79 | And I have a configuration file with "url" set to "http://mysite.com"
80 | When I run jekyll
81 | Then the _site directory should exist
82 | And I should see "http://mysite.com" in "_site/index.html"
83 |
--------------------------------------------------------------------------------
/lib/jekyll.rb:
--------------------------------------------------------------------------------
1 | $:.unshift File.dirname(__FILE__) # For use/testing when no gem is installed
2 |
3 | # Require all of the Ruby files in the given directory.
4 | #
5 | # path - The String relative path from here to the directory.
6 | #
7 | # Returns nothing.
8 | def require_all(path)
9 | glob = File.join(File.dirname(__FILE__), path, '*.rb')
10 | Dir[glob].each do |f|
11 | require f
12 | end
13 | end
14 |
15 | # rubygems
16 | require 'rubygems'
17 |
18 | # stdlib
19 | require 'fileutils'
20 | require 'time'
21 | require 'yaml'
22 |
23 | # 3rd party
24 | require 'liquid'
25 | require 'maruku'
26 |
27 | # internal requires
28 | require 'jekyll/core_ext'
29 | require 'jekyll/site'
30 | require 'jekyll/convertible'
31 | require 'jekyll/layout'
32 | require 'jekyll/page'
33 | require 'jekyll/post'
34 | require 'jekyll/filters'
35 | require 'jekyll/albino'
36 | require 'jekyll/static_file'
37 | require 'jekyll/errors'
38 |
39 | # extensions
40 | require 'jekyll/plugin'
41 | require 'jekyll/converter'
42 | require 'jekyll/generator'
43 | require_all 'jekyll/converters'
44 | require_all 'jekyll/generators'
45 | require_all 'jekyll/tags'
46 |
47 | module Jekyll
48 | VERSION = '0.10.0'
49 |
50 | # Default options. Overriden by values in _config.yml or command-line opts.
51 | # (Strings rather symbols used for compatability with YAML).
52 | DEFAULTS = {
53 | 'safe' => false,
54 | 'auto' => false,
55 | 'server' => false,
56 | 'server_port' => 4000,
57 |
58 | 'source' => Dir.pwd,
59 | 'destination' => File.join(Dir.pwd, '_site'),
60 | 'plugins' => File.join(Dir.pwd, '_plugins'),
61 |
62 | 'future' => true,
63 | 'lsi' => false,
64 | 'pygments' => false,
65 | 'markdown' => 'maruku',
66 | 'permalink' => 'date',
67 |
68 | 'maruku' => {
69 | 'use_tex' => false,
70 | 'use_divs' => false,
71 | 'png_engine' => 'blahtex',
72 | 'png_dir' => 'images/latex',
73 | 'png_url' => '/images/latex'
74 | },
75 | 'rdiscount' => {
76 | 'extensions' => []
77 | },
78 | 'kramdown' => {
79 | 'auto_ids' => true,
80 | 'footnote_nr' => 1,
81 | 'entity_output' => 'as_char',
82 | 'toc_levels' => '1..6',
83 | 'use_coderay' => false,
84 |
85 | 'coderay' => {
86 | 'coderay_wrap' => 'div',
87 | 'coderay_line_numbers' => 'inline',
88 | 'coderay_line_number_start' => 1,
89 | 'coderay_tab_width' => 4,
90 | 'coderay_bold_every' => 10,
91 | 'coderay_css' => 'style'
92 | }
93 | }
94 | }
95 |
96 | # Generate a Jekyll configuration Hash by merging the default options
97 | # with anything in _config.yml, and adding the given options on top.
98 | #
99 | # override - A Hash of config directives that override any options in both
100 | # the defaults and the config file. See Jekyll::DEFAULTS for a
101 | # list of option names and their defaults.
102 | #
103 | # Returns the final configuration Hash.
104 | def self.configuration(override)
105 | # _config.yml may override default source location, but until
106 | # then, we need to know where to look for _config.yml
107 | source = override['source'] || Jekyll::DEFAULTS['source']
108 |
109 | # Get configuration from /_config.yml
110 | config_file = File.join(source, '_config.yml')
111 | begin
112 | config = YAML.load_file(config_file)
113 | raise "Invalid configuration - #{config_file}" if !config.is_a?(Hash)
114 | $stdout.puts "Configuration from #{config_file}"
115 | rescue => err
116 | $stderr.puts "WARNING: Could not read configuration. " +
117 | "Using defaults (and options)."
118 | $stderr.puts "\t" + err.to_s
119 | config = {}
120 | end
121 |
122 | # Merge DEFAULTS < _config.yml < override
123 | Jekyll::DEFAULTS.deep_merge(config).deep_merge(override)
124 | end
125 | end
126 |
--------------------------------------------------------------------------------
/features/step_definitions/jekyll_steps.rb:
--------------------------------------------------------------------------------
1 | Before do
2 | FileUtils.mkdir(TEST_DIR)
3 | Dir.chdir(TEST_DIR)
4 | end
5 |
6 | After do
7 | Dir.chdir(TEST_DIR)
8 | FileUtils.rm_rf(TEST_DIR)
9 | end
10 |
11 | Given /^I have a blank site in "(.*)"$/ do |path|
12 | FileUtils.mkdir(path)
13 | end
14 |
15 | # Like "I have a foo file" but gives a yaml front matter so jekyll actually processes it
16 | Given /^I have an? "(.*)" page(?: with (.*) "(.*)")? that contains "(.*)"$/ do |file, key, value, text|
17 | File.open(file, 'w') do |f|
18 | f.write < true)
115 | end
116 |
117 | When /^I change "(.*)" to contain "(.*)"$/ do |file, text|
118 | File.open(file, 'a') do |f|
119 | f.write(text)
120 | end
121 | end
122 |
123 | Then /^the (.*) directory should exist$/ do |dir|
124 | assert File.directory?(dir)
125 | end
126 |
127 | Then /^I should see "(.*)" in "(.*)"$/ do |text, file|
128 | assert_match Regexp.new(text), File.open(file).readlines.join
129 | end
130 |
131 | Then /^the "(.*)" file should exist$/ do |file|
132 | assert File.file?(file)
133 | end
134 |
135 | Then /^the "(.*)" file should not exist$/ do |file|
136 | assert !File.exists?(file)
137 | end
138 |
139 | Then /^I should see today's time in "(.*)"$/ do |file|
140 | assert_match Regexp.new(Regexp.escape(Time.now.to_s)), File.open(file).readlines.join
141 | end
142 |
143 | Then /^I should see today's date in "(.*)"$/ do |file|
144 | assert_match Regexp.new(Date.today.to_s), File.open(file).readlines.join
145 | end
146 |
--------------------------------------------------------------------------------
/lib/jekyll/converters/markdown.rb:
--------------------------------------------------------------------------------
1 | module Jekyll
2 |
3 | class MarkdownConverter < Converter
4 | safe true
5 |
6 | pygments_prefix "\n"
7 | pygments_suffix "\n"
8 |
9 | def setup
10 | return if @setup
11 | # Set the Markdown interpreter (and Maruku self.config, if necessary)
12 | case @config['markdown']
13 | when 'kramdown'
14 | begin
15 | require 'kramdown'
16 | rescue LoadError
17 | STDERR.puts 'You are missing a library required for Markdown. Please run:'
18 | STDERR.puts ' $ [sudo] gem install kramdown'
19 | raise FatalException.new("Missing dependency: kramdown")
20 | end
21 | when 'rdiscount'
22 | begin
23 | require 'rdiscount'
24 |
25 | # Load rdiscount extensions
26 | @rdiscount_extensions = @config['rdiscount']['extensions'].map { |e| e.to_sym }
27 | rescue LoadError
28 | STDERR.puts 'You are missing a library required for Markdown. Please run:'
29 | STDERR.puts ' $ [sudo] gem install rdiscount'
30 | raise FatalException.new("Missing dependency: rdiscount")
31 | end
32 | when 'maruku'
33 | begin
34 | require 'maruku'
35 |
36 | if @config['maruku']['use_divs']
37 | require 'maruku/ext/div'
38 | STDERR.puts 'Maruku: Using extended syntax for div elements.'
39 | end
40 |
41 | if @config['maruku']['use_tex']
42 | require 'maruku/ext/math'
43 | STDERR.puts "Maruku: Using LaTeX extension. Images in `#{@config['maruku']['png_dir']}`."
44 |
45 | # Switch off MathML output
46 | MaRuKu::Globals[:html_math_output_mathml] = false
47 | MaRuKu::Globals[:html_math_engine] = 'none'
48 |
49 | # Turn on math to PNG support with blahtex
50 | # Resulting PNGs stored in `images/latex`
51 | MaRuKu::Globals[:html_math_output_png] = true
52 | MaRuKu::Globals[:html_png_engine] = @config['maruku']['png_engine']
53 | MaRuKu::Globals[:html_png_dir] = @config['maruku']['png_dir']
54 | MaRuKu::Globals[:html_png_url] = @config['maruku']['png_url']
55 | end
56 | rescue LoadError
57 | STDERR.puts 'You are missing a library required for Markdown. Please run:'
58 | STDERR.puts ' $ [sudo] gem install maruku'
59 | raise FatalException.new("Missing dependency: maruku")
60 | end
61 | else
62 | STDERR.puts "Invalid Markdown processor: #{@config['markdown']}"
63 | STDERR.puts " Valid options are [ maruku | rdiscount | kramdown ]"
64 | raise FatalException.new("Invalid Markdown process: #{@config['markdown']}")
65 | end
66 | @setup = true
67 | end
68 |
69 | def matches(ext)
70 | ext =~ /(markdown|mkdn?|md)/i
71 | end
72 |
73 | def output_ext(ext)
74 | ".html"
75 | end
76 |
77 | def convert(content)
78 | setup
79 | case @config['markdown']
80 | when 'kramdown'
81 | # Check for use of coderay
82 | if @config['kramdown']['use_coderay']
83 | Kramdown::Document.new(content, {
84 | :auto_ids => @config['kramdown']['auto_ids'],
85 | :footnote_nr => @config['kramdown']['footnote_nr'],
86 | :entity_output => @config['kramdown']['entity_output'],
87 | :toc_levels => @config['kramdown']['toc_levels'],
88 |
89 | :coderay_wrap => @config['kramdown']['coderay']['coderay_wrap'],
90 | :coderay_line_numbers => @config['kramdown']['coderay']['coderay_line_numbers'],
91 | :coderay_line_number_start => @config['kramdown']['coderay']['coderay_line_number_start'],
92 | :coderay_tab_width => @config['kramdown']['coderay']['coderay_tab_width'],
93 | :coderay_bold_every => @config['kramdown']['coderay']['coderay_bold_every'],
94 | :coderay_css => @config['kramdown']['coderay']['coderay_css']
95 | }).to_html
96 | else
97 | # not using coderay
98 | Kramdown::Document.new(content, {
99 | :auto_ids => @config['kramdown']['auto_ids'],
100 | :footnote_nr => @config['kramdown']['footnote_nr'],
101 | :entity_output => @config['kramdown']['entity_output'],
102 | :toc_levels => @config['kramdown']['toc_levels']
103 | }).to_html
104 | end
105 | when 'rdiscount'
106 | RDiscount.new(content, *@rdiscount_extensions).to_html
107 | when 'maruku'
108 | Maruku.new(content).to_html
109 | end
110 | end
111 | end
112 |
113 | end
114 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'rake'
3 | require 'date'
4 |
5 | #############################################################################
6 | #
7 | # Helper functions
8 | #
9 | #############################################################################
10 |
11 | def name
12 | @name ||= Dir['*.gemspec'].first.split('.').first
13 | end
14 |
15 | def version
16 | line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/]
17 | line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
18 | end
19 |
20 | def date
21 | Date.today.to_s
22 | end
23 |
24 | def rubyforge_project
25 | name
26 | end
27 |
28 | def gemspec_file
29 | "#{name}.gemspec"
30 | end
31 |
32 | def gem_file
33 | "#{name}-#{version}.gem"
34 | end
35 |
36 | def replace_header(head, header_name)
37 | head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
38 | end
39 |
40 | #############################################################################
41 | #
42 | # Standard tasks
43 | #
44 | #############################################################################
45 |
46 | task :default => [:test, :features]
47 |
48 | require 'rake/testtask'
49 | Rake::TestTask.new(:test) do |test|
50 | test.libs << 'lib' << 'test'
51 | test.pattern = 'test/**/test_*.rb'
52 | test.verbose = true
53 | end
54 |
55 | desc "Generate RCov test coverage and open in your browser"
56 | task :coverage do
57 | require 'rcov'
58 | sh "rm -fr coverage"
59 | sh "rcov test/test_*.rb"
60 | sh "open coverage/index.html"
61 | end
62 |
63 | require 'rake/rdoctask'
64 | Rake::RDocTask.new do |rdoc|
65 | rdoc.rdoc_dir = 'rdoc'
66 | rdoc.title = "#{name} #{version}"
67 | rdoc.rdoc_files.include('README*')
68 | rdoc.rdoc_files.include('lib/**/*.rb')
69 | end
70 |
71 | desc "Open an irb session preloaded with this library"
72 | task :console do
73 | sh "irb -rubygems -r ./lib/#{name}.rb"
74 | end
75 |
76 | #############################################################################
77 | #
78 | # Custom tasks (add your own tasks here)
79 | #
80 | #############################################################################
81 |
82 | namespace :migrate do
83 | desc "Migrate from mephisto in the current directory"
84 | task :mephisto do
85 | sh %q(ruby -r './lib/jekyll/migrators/mephisto' -e 'Jekyll::Mephisto.postgres(:database => "#{ENV["DB"]}")')
86 | end
87 | desc "Migrate from Movable Type in the current directory"
88 | task :mt do
89 | sh %q(ruby -r './lib/jekyll/migrators/mt' -e 'Jekyll::MT.process("#{ENV["DB"]}", "#{ENV["USER"]}", "#{ENV["PASS"]}")')
90 | end
91 | desc "Migrate from Typo in the current directory"
92 | task :typo do
93 | sh %q(ruby -r './lib/jekyll/migrators/typo' -e 'Jekyll::Typo.process("#{ENV["DB"]}", "#{ENV["USER"]}", "#{ENV["PASS"]}")')
94 | end
95 | end
96 |
97 | begin
98 | require 'cucumber/rake/task'
99 | Cucumber::Rake::Task.new(:features) do |t|
100 | t.cucumber_opts = "--format progress"
101 | end
102 | rescue LoadError
103 | desc 'Cucumber rake task not available'
104 | task :features do
105 | abort 'Cucumber rake task is not available. Be sure to install cucumber as a gem or plugin'
106 | end
107 | end
108 |
109 | #############################################################################
110 | #
111 | # Packaging tasks
112 | #
113 | #############################################################################
114 |
115 | task :release => :build do
116 | unless `git branch` =~ /^\* master$/
117 | puts "You must be on the master branch to release!"
118 | exit!
119 | end
120 | sh "git commit --allow-empty -a -m 'Release #{version}'"
121 | sh "git tag v#{version}"
122 | sh "git push origin master"
123 | sh "git push origin v#{version}"
124 | sh "gem push pkg/#{name}-#{version}.gem"
125 | end
126 |
127 | task :build => :gemspec do
128 | sh "mkdir -p pkg"
129 | sh "gem build #{gemspec_file}"
130 | sh "mv #{gem_file} pkg"
131 | end
132 |
133 | task :gemspec do
134 | # read spec file and split out manifest section
135 | spec = File.read(gemspec_file)
136 | head, manifest, tail = spec.split(" # = MANIFEST =\n")
137 |
138 | # replace name version and date
139 | replace_header(head, :name)
140 | replace_header(head, :version)
141 | replace_header(head, :date)
142 | #comment this out if your rubyforge_project has a different name
143 | replace_header(head, :rubyforge_project)
144 |
145 | # determine file list from git ls-files
146 | files = `git ls-files`.
147 | split("\n").
148 | sort.
149 | reject { |file| file =~ /^\./ }.
150 | reject { |file| file =~ /^(rdoc|pkg|coverage)/ }.
151 | map { |file| " #{file}" }.
152 | join("\n")
153 |
154 | # piece file back together and write
155 | manifest = " s.files = %w[\n#{files}\n ]\n"
156 | spec = [head, manifest, tail].join(" # = MANIFEST =\n")
157 | File.open(gemspec_file, 'w') { |io| io.write(spec) }
158 | puts "Updated #{gemspec_file}"
159 | end
--------------------------------------------------------------------------------
/features/create_sites.feature:
--------------------------------------------------------------------------------
1 | Feature: Create sites
2 | As a hacker who likes to blog
3 | I want to be able to make a static site
4 | In order to share my awesome ideas with the interwebs
5 |
6 | Scenario: Basic site
7 | Given I have an "index.html" file that contains "Basic Site"
8 | When I run jekyll
9 | Then the _site directory should exist
10 | And I should see "Basic Site" in "_site/index.html"
11 |
12 | Scenario: Basic site with a post
13 | Given I have a _posts directory
14 | And I have the following post:
15 | | title | date | content |
16 | | Hackers | 3/27/2009 | My First Exploit |
17 | When I run jekyll
18 | Then the _site directory should exist
19 | And I should see "My First Exploit" in "_site/2009/03/27/hackers.html"
20 |
21 | Scenario: Basic site with layout and a page
22 | Given I have a _layouts directory
23 | And I have an "index.html" page with layout "default" that contains "Basic Site with Layout"
24 | And I have a default layout that contains "Page Layout: {{ content }}"
25 | When I run jekyll
26 | Then the _site directory should exist
27 | And I should see "Page Layout: Basic Site with Layout" in "_site/index.html"
28 |
29 | Scenario: Basic site with layout and a post
30 | Given I have a _layouts directory
31 | And I have a _posts directory
32 | And I have the following posts:
33 | | title | date | layout | content |
34 | | Wargames | 3/27/2009 | default | The only winning move is not to play. |
35 | And I have a default layout that contains "Post Layout: {{ content }}"
36 | When I run jekyll
37 | Then the _site directory should exist
38 | And I should see "Post Layout:
The only winning move is not to play.
" in "_site/2009/03/27/wargames.html"
39 |
40 | Scenario: Basic site with layouts, pages, posts and files
41 | Given I have a _layouts directory
42 | And I have a page layout that contains "Page {{ page.title }}: {{ content }}"
43 | And I have a post layout that contains "Post {{ page.title }}: {{ content }}"
44 | And I have an "index.html" page with layout "page" that contains "Site contains {{ site.pages.size }} pages and {{ site.posts.size }} posts"
45 | And I have a blog directory
46 | And I have a "blog/index.html" page with layout "page" that contains "blog category index page"
47 | And I have an "about.html" file that contains "No replacement {{ site.posts.size }}"
48 | And I have an "another_file" file that contains ""
49 | And I have a _posts directory
50 | And I have the following posts:
51 | | title | date | layout | content |
52 | | entry1 | 3/27/2009 | post | content for entry1. |
53 | | entry2 | 4/27/2009 | post | content for entry2. |
54 | And I have a category/_posts directory
55 | And I have the following posts in "category":
56 | | title | date | layout | content |
57 | | entry3 | 5/27/2009 | post | content for entry3. |
58 | | entry4 | 6/27/2009 | post | content for entry4. |
59 | When I run jekyll
60 | Then the _site directory should exist
61 | And I should see "Page : Site contains 2 pages and 4 posts" in "_site/index.html"
62 | And I should see "No replacement \{\{ site.posts.size \}\}" in "_site/about.html"
63 | And I should see "" in "_site/another_file"
64 | And I should see "Page : blog category index page" in "_site/blog/index.html"
65 | And I should see "Post entry1:
content for entry1.
" in "_site/2009/03/27/entry1.html"
66 | And I should see "Post entry2:
content for entry2.
" in "_site/2009/04/27/entry2.html"
67 | And I should see "Post entry3:
content for entry3.
" in "_site/category/2009/05/27/entry3.html"
68 | And I should see "Post entry4:
content for entry4.
" in "_site/category/2009/06/27/entry4.html"
69 |
70 | Scenario: Basic site with include tag
71 | Given I have a _includes directory
72 | And I have an "index.html" page that contains "Basic Site with include tag: {% include about.textile %}"
73 | And I have an "_includes/about.textile" file that contains "Generated by Jekyll"
74 | When I run jekyll
75 | Then the _site directory should exist
76 | And I should see "Basic Site with include tag: Generated by Jekyll" in "_site/index.html"
77 |
78 | Scenario: Basic site with subdir include tag
79 | Given I have a _includes directory
80 | And I have an "_includes/about.textile" file that contains "Generated by Jekyll"
81 | And I have an info directory
82 | And I have an "info/index.html" page that contains "Basic Site with subdir include tag: {% include about.textile %}"
83 | When I run jekyll
84 | Then the _site directory should exist
85 | And I should see "Basic Site with subdir include tag: Generated by Jekyll" in "_site/info/index.html"
86 |
87 | Scenario: Basic site with nested include tag
88 | Given I have a _includes directory
89 | And I have an "_includes/about.textile" file that contains "Generated by {% include jekyll.textile %}"
90 | And I have an "_includes/jekyll.textile" file that contains "Jekyll"
91 | And I have an "index.html" page that contains "Basic Site with include tag: {% include about.textile %}"
92 | When I debug jekyll
93 | Then the _site directory should exist
94 | And I should see "Basic Site with include tag: Generated by Jekyll" in "_site/index.html"
95 |
--------------------------------------------------------------------------------
/jekyll.gemspec:
--------------------------------------------------------------------------------
1 | Gem::Specification.new do |s|
2 | s.specification_version = 2 if s.respond_to? :specification_version=
3 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
4 | s.rubygems_version = '1.3.5'
5 |
6 | s.name = 'jekyll'
7 | s.version = '0.10.0'
8 | s.date = '2010-12-16'
9 | s.rubyforge_project = 'jekyll'
10 |
11 | s.summary = "A simple, blog aware, static site generator."
12 | s.description = "Jekyll is a simple, blog aware, static site generator."
13 |
14 | s.authors = ["Tom Preston-Werner"]
15 | s.email = 'tom@mojombo.com'
16 | s.homepage = 'http://github.com/mojombo/jekyll'
17 |
18 | s.require_paths = %w[lib]
19 |
20 | s.executables = ["jekyll"]
21 | s.default_executable = 'jekyll'
22 |
23 | s.rdoc_options = ["--charset=UTF-8"]
24 | s.extra_rdoc_files = %w[README.textile LICENSE]
25 |
26 | s.add_runtime_dependency('liquid', [">= 1.9.0"])
27 | s.add_runtime_dependency('classifier', [">= 1.3.1"])
28 | s.add_runtime_dependency('directory_watcher', [">= 1.1.1"])
29 | s.add_runtime_dependency('maruku', [">= 0.5.9"])
30 |
31 | s.add_development_dependency('redgreen', [">= 4.2.1"])
32 | s.add_development_dependency('shoulda', [">= 4.2.1"])
33 | s.add_development_dependency('rr', [">= 4.2.1"])
34 | s.add_development_dependency('cucumber', [">= 4.2.1"])
35 | s.add_development_dependency('RedCloth', [">= 4.2.1"])
36 | s.add_development_dependency('kramdown', [">= 0.12.0"])
37 |
38 | # = MANIFEST =
39 | s.files = %w[
40 | History.txt
41 | LICENSE
42 | README.textile
43 | Rakefile
44 | bin/jekyll
45 | cucumber.yml
46 | features/create_sites.feature
47 | features/embed_filters.feature
48 | features/markdown.feature
49 | features/pagination.feature
50 | features/permalinks.feature
51 | features/post_data.feature
52 | features/site_configuration.feature
53 | features/site_data.feature
54 | features/step_definitions/jekyll_steps.rb
55 | features/support/env.rb
56 | jekyll.gemspec
57 | lib/jekyll.rb
58 | lib/jekyll/albino.rb
59 | lib/jekyll/converter.rb
60 | lib/jekyll/converters/identity.rb
61 | lib/jekyll/converters/markdown.rb
62 | lib/jekyll/converters/textile.rb
63 | lib/jekyll/convertible.rb
64 | lib/jekyll/core_ext.rb
65 | lib/jekyll/errors.rb
66 | lib/jekyll/filters.rb
67 | lib/jekyll/generator.rb
68 | lib/jekyll/generators/pagination.rb
69 | lib/jekyll/layout.rb
70 | lib/jekyll/migrators/csv.rb
71 | lib/jekyll/migrators/drupal.rb
72 | lib/jekyll/migrators/marley.rb
73 | lib/jekyll/migrators/mephisto.rb
74 | lib/jekyll/migrators/mt.rb
75 | lib/jekyll/migrators/textpattern.rb
76 | lib/jekyll/migrators/typo.rb
77 | lib/jekyll/migrators/wordpress.com.rb
78 | lib/jekyll/migrators/wordpress.rb
79 | lib/jekyll/page.rb
80 | lib/jekyll/plugin.rb
81 | lib/jekyll/post.rb
82 | lib/jekyll/site.rb
83 | lib/jekyll/static_file.rb
84 | lib/jekyll/tags/highlight.rb
85 | lib/jekyll/tags/include.rb
86 | test/helper.rb
87 | test/source/.htaccess
88 | test/source/_includes/sig.markdown
89 | test/source/_layouts/default.html
90 | test/source/_layouts/simple.html
91 | test/source/_posts/2008-02-02-not-published.textile
92 | test/source/_posts/2008-02-02-published.textile
93 | test/source/_posts/2008-10-18-foo-bar.textile
94 | test/source/_posts/2008-11-21-complex.textile
95 | test/source/_posts/2008-12-03-permalinked-post.textile
96 | test/source/_posts/2008-12-13-include.markdown
97 | test/source/_posts/2009-01-27-array-categories.textile
98 | test/source/_posts/2009-01-27-categories.textile
99 | test/source/_posts/2009-01-27-category.textile
100 | test/source/_posts/2009-01-27-empty-categories.textile
101 | test/source/_posts/2009-01-27-empty-category.textile
102 | test/source/_posts/2009-03-12-hash-#1.markdown
103 | test/source/_posts/2009-05-18-empty-tag.textile
104 | test/source/_posts/2009-05-18-empty-tags.textile
105 | test/source/_posts/2009-05-18-tag.textile
106 | test/source/_posts/2009-05-18-tags.textile
107 | test/source/_posts/2009-06-22-empty-yaml.textile
108 | test/source/_posts/2009-06-22-no-yaml.textile
109 | test/source/_posts/2010-01-08-triple-dash.markdown
110 | test/source/_posts/2010-01-09-date-override.textile
111 | test/source/_posts/2010-01-09-time-override.textile
112 | test/source/_posts/2010-01-09-timezone-override.textile
113 | test/source/_posts/2010-01-16-override-data.textile
114 | test/source/about.html
115 | test/source/category/_posts/2008-9-23-categories.textile
116 | test/source/contacts.html
117 | test/source/css/screen.css
118 | test/source/deal.with.dots.html
119 | test/source/foo/_posts/bar/2008-12-12-topical-post.textile
120 | test/source/index.html
121 | test/source/sitemap.xml
122 | test/source/win/_posts/2009-05-24-yaml-linebreak.markdown
123 | test/source/z_category/_posts/2008-9-23-categories.textile
124 | test/suite.rb
125 | test/test_configuration.rb
126 | test/test_core_ext.rb
127 | test/test_filters.rb
128 | test/test_generated_site.rb
129 | test/test_kramdown.rb
130 | test/test_page.rb
131 | test/test_pager.rb
132 | test/test_post.rb
133 | test/test_rdiscount.rb
134 | test/test_site.rb
135 | test/test_tags.rb
136 | ]
137 | # = MANIFEST =
138 |
139 | s.test_files = s.files.select { |path| path =~ /^test\/test_.*\.rb/ }
140 | end
141 |
--------------------------------------------------------------------------------
/bin/jekyll:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
4 |
5 | help = < ./_site
10 | jekyll # . ->
11 | jekyll # ->
12 |
13 | Configuration is read from '/_config.yml' but can be overriden
14 | using the following options:
15 |
16 | HELP
17 |
18 | require 'optparse'
19 | require 'jekyll'
20 |
21 | exec = {}
22 | options = {}
23 | opts = OptionParser.new do |opts|
24 | opts.banner = help
25 |
26 | opts.on("--[no-]safe", "Safe mode (default unsafe)") do |safe|
27 | options['safe'] = safe
28 | end
29 |
30 | opts.on("--[no-]auto", "Auto-regenerate") do |auto|
31 | options['auto'] = auto
32 | end
33 |
34 | opts.on("--server [PORT]", "Start web server (default port 4000)") do |port|
35 | options['server'] = true
36 | options['server_port'] = port unless port.nil?
37 | end
38 |
39 | opts.on("--no-server", "Do not start a web server") do |part|
40 | options['server'] = false
41 | end
42 |
43 | opts.on("--base-url [BASE_URL]", "Serve website from a given base URL (default '/'") do |baseurl|
44 | options['baseurl'] = baseurl
45 | end
46 |
47 | opts.on("--[no-]lsi", "Use LSI for better related posts") do |lsi|
48 | options['lsi'] = lsi
49 | end
50 |
51 | opts.on("--[no-]pygments", "Use pygments to highlight code") do |pygments|
52 | options['pygments'] = pygments
53 | end
54 |
55 | opts.on("--rdiscount", "Use rdiscount gem for Markdown") do
56 | options['markdown'] = 'rdiscount'
57 | end
58 |
59 | opts.on("--kramdown", "Use kramdown gem for Markdown") do
60 | options['markdown'] = 'kramdown'
61 | end
62 |
63 | opts.on("--time [TIME]", "Time to generate the site for") do |time|
64 | options['time'] = Time.parse(time)
65 | end
66 |
67 | opts.on("--[no-]future", "Render future dated posts") do |future|
68 | options['future'] = future
69 | end
70 |
71 | opts.on("--permalink [TYPE]", "Use 'date' (default) for YYYY/MM/DD") do |style|
72 | options['permalink'] = style unless style.nil?
73 | end
74 |
75 | opts.on("--paginate [POSTS_PER_PAGE]", "Paginate a blog's posts") do |per_page|
76 | begin
77 | options['paginate'] = per_page.to_i
78 | raise ArgumentError if options['paginate'] == 0
79 | rescue
80 | puts 'you must specify a number of posts by page bigger than 0'
81 | exit 0
82 | end
83 | end
84 |
85 | opts.on("--limit_posts [MAX_POSTS]", "Limit the number of posts to publish") do |limit_posts|
86 | begin
87 | options['limit_posts'] = limit_posts.to_i
88 | raise ArgumentError if options['limit_posts'] < 1
89 | rescue
90 | puts 'you must specify a number of posts by page bigger than 0'
91 | exit 0
92 | end
93 | end
94 |
95 | opts.on("--url [URL]", "Set custom site.url") do |url|
96 | options['url'] = url
97 | end
98 |
99 | opts.on("--version", "Display current version") do
100 | puts "Jekyll " + Jekyll::VERSION
101 | exit 0
102 | end
103 | end
104 |
105 | # Read command line options into `options` hash
106 | opts.parse!
107 |
108 | # Get source and destintation from command line
109 | case ARGV.size
110 | when 0
111 | when 1
112 | options['destination'] = ARGV[0]
113 | when 2
114 | options['source'] = ARGV[0]
115 | options['destination'] = ARGV[1]
116 | else
117 | puts "Invalid options. Run `jekyll --help` for assistance."
118 | exit(1)
119 | end
120 |
121 | options = Jekyll.configuration(options)
122 |
123 | # Get source and destination directories (possibly set by config file)
124 | source = options['source']
125 | destination = options['destination']
126 |
127 | # Files to watch
128 | def globs(source)
129 | Dir.chdir(source) do
130 | dirs = Dir['*'].select { |x| File.directory?(x) }
131 | dirs -= ['_site']
132 | dirs = dirs.map { |x| "#{x}/**/*" }
133 | dirs += ['*']
134 | end
135 | end
136 |
137 | # Create the Site
138 | site = Jekyll::Site.new(options)
139 |
140 | # Run the directory watcher for auto-generation, if required
141 | if options['auto']
142 | require 'directory_watcher'
143 |
144 | puts "Auto-regenerating enabled: #{source} -> #{destination}"
145 |
146 | dw = DirectoryWatcher.new(source)
147 | dw.interval = 1
148 | dw.glob = globs(source)
149 |
150 | dw.add_observer do |*args|
151 | t = Time.now.strftime("%Y-%m-%d %H:%M:%S")
152 | puts "[#{t}] regeneration: #{args.size} files changed"
153 | site.process
154 | end
155 |
156 | dw.start
157 |
158 | unless options['server']
159 | loop { sleep 1000 }
160 | end
161 | else
162 | puts "Building site: #{source} -> #{destination}"
163 | begin
164 | site.process
165 | rescue Jekyll::FatalException
166 | exit(1)
167 | end
168 | puts "Successfully generated site: #{source} -> #{destination}"
169 | end
170 |
171 | # Run the server on the specified port, if required
172 | if options['server']
173 | require 'webrick'
174 | include WEBrick
175 |
176 | FileUtils.mkdir_p(destination)
177 |
178 | mime_types = WEBrick::HTTPUtils::DefaultMimeTypes
179 | mime_types.store 'js', 'application/javascript'
180 |
181 | s = HTTPServer.new(
182 | :Port => options['server_port'],
183 | :MimeTypes => mime_types
184 | )
185 | s.mount(options['baseurl'], HTTPServlet::FileHandler, destination)
186 | t = Thread.new {
187 | s.start
188 | }
189 |
190 | trap("INT") { s.shutdown }
191 | t.join()
192 | end
193 |
--------------------------------------------------------------------------------
/test/test_site.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/helper'
2 |
3 | class TestSite < Test::Unit::TestCase
4 | context "creating sites" do
5 | setup do
6 | stub(Jekyll).configuration do
7 | Jekyll::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir})
8 | end
9 | @site = Site.new(Jekyll.configuration)
10 | end
11 |
12 | should "have an empty tag hash by default" do
13 | assert_equal Hash.new, @site.tags
14 | end
15 |
16 | should "reset data before processing" do
17 | clear_dest
18 | @site.process
19 | before_posts = @site.posts.length
20 | before_layouts = @site.layouts.length
21 | before_categories = @site.categories.length
22 | before_tags = @site.tags.length
23 | before_pages = @site.pages.length
24 | before_static_files = @site.static_files.length
25 | before_time = @site.time
26 |
27 | @site.process
28 | assert_equal before_posts, @site.posts.length
29 | assert_equal before_layouts, @site.layouts.length
30 | assert_equal before_categories, @site.categories.length
31 | assert_equal before_tags, @site.tags.length
32 | assert_equal before_pages, @site.pages.length
33 | assert_equal before_static_files, @site.static_files.length
34 | assert before_time <= @site.time
35 | end
36 |
37 | should "write only modified static files" do
38 | clear_dest
39 | StaticFile.reset_cache
40 |
41 | @site.process
42 | some_static_file = @site.static_files[0].path
43 | dest = File.expand_path(@site.static_files[0].destination(@site.dest))
44 | mtime1 = File.stat(dest).mtime.to_i # first run must generate dest file
45 |
46 | # need to sleep because filesystem timestamps have best resolution in seconds
47 | sleep 1
48 | @site.process
49 | mtime2 = File.stat(dest).mtime.to_i
50 | assert_equal mtime1, mtime2
51 |
52 | # simulate file modification by user
53 | FileUtils.touch some_static_file
54 |
55 | sleep 1
56 | @site.process
57 | mtime3 = File.stat(dest).mtime.to_i
58 | assert_not_equal mtime2, mtime3 # must be regenerated!
59 |
60 | sleep 1
61 | @site.process
62 | mtime4 = File.stat(dest).mtime.to_i
63 | assert_equal mtime3, mtime4 # no modifications, so must be the same
64 | end
65 |
66 | should "write static files if not modified but missing in destination" do
67 | clear_dest
68 | StaticFile.reset_cache
69 |
70 | @site.process
71 | some_static_file = @site.static_files[0].path
72 | dest = File.expand_path(@site.static_files[0].destination(@site.dest))
73 | mtime1 = File.stat(dest).mtime.to_i # first run must generate dest file
74 |
75 | # need to sleep because filesystem timestamps have best resolution in seconds
76 | sleep 1
77 | @site.process
78 | mtime2 = File.stat(dest).mtime.to_i
79 | assert_equal mtime1, mtime2
80 |
81 | # simulate destination file deletion
82 | File.unlink dest
83 |
84 | sleep 1
85 | @site.process
86 | mtime3 = File.stat(dest).mtime.to_i
87 | assert_not_equal mtime2, mtime3 # must be regenerated and differ!
88 |
89 | sleep 1
90 | @site.process
91 | mtime4 = File.stat(dest).mtime.to_i
92 | assert_equal mtime3, mtime4 # no modifications, so must be the same
93 | end
94 |
95 | should "read layouts" do
96 | @site.read_layouts
97 | assert_equal ["default", "simple"].sort, @site.layouts.keys.sort
98 | end
99 |
100 | should "read posts" do
101 | @site.read_posts('')
102 | posts = Dir[source_dir('_posts', '*')]
103 | assert_equal posts.size - 1, @site.posts.size
104 | end
105 |
106 | should "deploy payload" do
107 | clear_dest
108 | @site.process
109 |
110 | posts = Dir[source_dir("**", "_posts", "*")]
111 | categories = %w(bar baz category foo z_category publish_test win).sort
112 |
113 | assert_equal posts.size - 1, @site.posts.size
114 | assert_equal categories, @site.categories.keys.sort
115 | assert_equal 4, @site.categories['foo'].size
116 | end
117 |
118 | should "filter entries" do
119 | ent1 = %w[foo.markdown bar.markdown baz.markdown #baz.markdown#
120 | .baz.markdow foo.markdown~]
121 | ent2 = %w[.htaccess _posts _pages bla.bla]
122 |
123 | assert_equal %w[foo.markdown bar.markdown baz.markdown], @site.filter_entries(ent1)
124 | assert_equal %w[.htaccess bla.bla], @site.filter_entries(ent2)
125 | end
126 |
127 | should "filter entries with exclude" do
128 | excludes = %w[README TODO]
129 | includes = %w[index.html site.css]
130 |
131 | @site.exclude = excludes
132 | assert_equal includes, @site.filter_entries(excludes + includes)
133 | end
134 |
135 | context 'with orphaned files in destination' do
136 | setup do
137 | clear_dest
138 | @site.process
139 | # generate some orphaned files:
140 | # hidden file
141 | File.open(dest_dir('.htpasswd'), 'w')
142 | # single file
143 | File.open(dest_dir('obsolete.html'), 'w')
144 | # single file in sub directory
145 | FileUtils.mkdir(dest_dir('qux'))
146 | File.open(dest_dir('qux/obsolete.html'), 'w')
147 | # empty directory
148 | FileUtils.mkdir(dest_dir('quux'))
149 | end
150 |
151 | teardown do
152 | FileUtils.rm_f(dest_dir('.htpasswd'))
153 | FileUtils.rm_f(dest_dir('obsolete.html'))
154 | FileUtils.rm_rf(dest_dir('qux'))
155 | FileUtils.rm_f(dest_dir('quux'))
156 | end
157 |
158 | should 'remove orphaned files in destination' do
159 | @site.process
160 | assert !File.exist?(dest_dir('.htpasswd'))
161 | assert !File.exist?(dest_dir('obsolete.html'))
162 | assert !File.exist?(dest_dir('qux'))
163 | assert !File.exist?(dest_dir('quux'))
164 | end
165 |
166 | end
167 |
168 | context 'with an invalid markdown processor in the configuration' do
169 | should 'not throw an error at initialization time' do
170 | bad_processor = 'not a processor name'
171 | assert_nothing_raised do
172 | Site.new(Jekyll.configuration.merge({ 'markdown' => bad_processor }))
173 | end
174 | end
175 |
176 | should 'throw FatalException at process time' do
177 | bad_processor = 'not a processor name'
178 | s = Site.new(Jekyll.configuration.merge({ 'markdown' => bad_processor }))
179 | assert_raise Jekyll::FatalException do
180 | s.process
181 | end
182 | end
183 | end
184 |
185 | end
186 | end
187 |
--------------------------------------------------------------------------------
/features/site_configuration.feature:
--------------------------------------------------------------------------------
1 | Feature: Site configuration
2 | As a hacker who likes to blog
3 | I want to be able to configure jekyll
4 | In order to make setting up a site easier
5 |
6 | Scenario: Change destination directory
7 | Given I have a blank site in "_sourcedir"
8 | And I have an "_sourcedir/index.html" file that contains "Changing source directory"
9 | And I have a configuration file with "source" set to "_sourcedir"
10 | When I run jekyll
11 | Then the _site directory should exist
12 | And I should see "Changing source directory" in "_site/index.html"
13 |
14 | Scenario: Change destination directory
15 | Given I have an "index.html" file that contains "Changing destination directory"
16 | And I have a configuration file with "destination" set to "_mysite"
17 | When I run jekyll
18 | Then the _mysite directory should exist
19 | And I should see "Changing destination directory" in "_mysite/index.html"
20 |
21 | Scenario: Exclude files inline
22 | Given I have an "Rakefile" file that contains "I want to be excluded"
23 | And I have an "README" file that contains "I want to be excluded"
24 | And I have an "index.html" file that contains "I want to be included"
25 | And I have a configuration file with "exclude" set to "Rakefile", "README"
26 | When I run jekyll
27 | Then I should see "I want to be included" in "_site/index.html"
28 | And the "_site/Rakefile" file should not exist
29 | And the "_site/README" file should not exist
30 |
31 | Scenario: Exclude files with YAML array
32 | Given I have an "Rakefile" file that contains "I want to be excluded"
33 | And I have an "README" file that contains "I want to be excluded"
34 | And I have an "index.html" file that contains "I want to be included"
35 | And I have a configuration file with "exclude" set to:
36 | | value |
37 | | README |
38 | | Rakefile |
39 | When I run jekyll
40 | Then I should see "I want to be included" in "_site/index.html"
41 | And the "_site/Rakefile" file should not exist
42 | And the "_site/README" file should not exist
43 |
44 | Scenario: Use RDiscount for markup
45 | Given I have an "index.markdown" page that contains "[Google](http://google.com)"
46 | And I have a configuration file with "markdown" set to "rdiscount"
47 | When I run jekyll
48 | Then the _site directory should exist
49 | And I should see "Google" in "_site/index.html"
50 |
51 | Scenario: Use Kramdown for markup
52 | Given I have an "index.markdown" page that contains "[Google](http://google.com)"
53 | And I have a configuration file with "markdown" set to "kramdown"
54 | When I run jekyll
55 | Then the _site directory should exist
56 | And I should see "Google" in "_site/index.html"
57 |
58 | Scenario: Use Maruku for markup
59 | Given I have an "index.markdown" page that contains "[Google](http://google.com)"
60 | And I have a configuration file with "markdown" set to "maruku"
61 | When I run jekyll
62 | Then the _site directory should exist
63 | And I should see "Google" in "_site/index.html"
64 |
65 | Scenario: Highlight code with pygments
66 | Given I have an "index.html" file that contains "{% highlight ruby %} puts 'Hello world!' {% endhighlight %}"
67 | And I have a configuration file with "pygments" set to "true"
68 | When I run jekyll
69 | Then the _site directory should exist
70 | And I should see "puts 'Hello world!'" in "_site/index.html"
71 |
72 | Scenario: Set time and no future dated posts
73 | Given I have a _layouts directory
74 | And I have a page layout that contains "Page Layout: {{ site.posts.size }} on {{ site.time | date: "%Y-%m-%d" }}"
75 | And I have a post layout that contains "Post Layout: {{ content }}"
76 | And I have an "index.html" page with layout "page" that contains "site index page"
77 | And I have a configuration file with:
78 | | key | value |
79 | | time | 2010-01-01 |
80 | | future | false |
81 | And I have a _posts directory
82 | And I have the following posts:
83 | | title | date | layout | content |
84 | | entry1 | 12/31/2007 | post | content for entry1. |
85 | | entry2 | 01/31/2020 | post | content for entry2. |
86 | When I run jekyll
87 | Then the _site directory should exist
88 | And I should see "Page Layout: 1 on 2010-01-01" in "_site/index.html"
89 | And I should see "Post Layout:
content for entry1.
" in "_site/2007/12/31/entry1.html"
90 | And the "_site/2020/01/31/entry2.html" file should not exist
91 |
92 | Scenario: Set time and future dated posts allowed
93 | Given I have a _layouts directory
94 | And I have a page layout that contains "Page Layout: {{ site.posts.size }} on {{ site.time | date: "%Y-%m-%d" }}"
95 | And I have a post layout that contains "Post Layout: {{ content }}"
96 | And I have an "index.html" page with layout "page" that contains "site index page"
97 | And I have a configuration file with:
98 | | key | value |
99 | | time | 2010-01-01 |
100 | | future | true |
101 | And I have a _posts directory
102 | And I have the following posts:
103 | | title | date | layout | content |
104 | | entry1 | 12/31/2007 | post | content for entry1. |
105 | | entry2 | 01/31/2020 | post | content for entry2. |
106 | When I run jekyll
107 | Then the _site directory should exist
108 | And I should see "Page Layout: 2 on 2010-01-01" in "_site/index.html"
109 | And I should see "Post Layout:
content for entry1.
" in "_site/2007/12/31/entry1.html"
110 | And I should see "Post Layout:
content for entry2.
" in "_site/2020/01/31/entry2.html"
111 |
112 | Scenario: Limit the number of posts generated by most recent date
113 | Given I have a _posts directory
114 | And I have a configuration file with:
115 | | key | value |
116 | | limit_posts | 2 |
117 | And I have the following posts:
118 | | title | date | content |
119 | | Apples | 3/27/2009 | An article about apples |
120 | | Oranges | 4/1/2009 | An article about oranges |
121 | | Bananas | 4/5/2009 | An article about bananas |
122 | When I run jekyll
123 | Then the _site directory should exist
124 | And the "_site/2009/04/05/bananas.html" file should exist
125 | And the "_site/2009/04/01/oranges.html" file should exist
126 | And the "_site/2009/03/27/apples.html" file should not exist
127 |
--------------------------------------------------------------------------------
/lib/jekyll/post.rb:
--------------------------------------------------------------------------------
1 | module Jekyll
2 |
3 | class Post
4 | include Comparable
5 | include Convertible
6 |
7 | class << self
8 | attr_accessor :lsi
9 | end
10 |
11 | MATCHER = /^(.+\/)*(\d+-\d+-\d+-)?(.*)(\.[^.]+)$/
12 |
13 | # Post name validator. Post filenames must be like:
14 | # 2008-11-05-my-awesome-post.textile
15 | #
16 | # Returns
17 | def self.valid?(name)
18 | name =~ MATCHER
19 | end
20 |
21 | attr_accessor :site
22 | attr_accessor :data, :content, :output, :ext
23 | attr_accessor :date, :slug, :published, :tags, :categories
24 |
25 | # Initialize this Post instance.
26 | # +site+ is the Site
27 | # +base+ is the String path to the dir containing the post file
28 | # +name+ is the String filename of the post file
29 | # +categories+ is an Array of Strings for the categories for this post
30 | #
31 | # Returns
32 | def initialize(site, source, dir, name)
33 | @site = site
34 | @base = File.join(source, dir, '_posts')
35 | @name = name
36 |
37 | self.categories = @name.split('/').tap{|o| o.pop}.reject { |x| x.empty? }
38 | self.process(name)
39 | self.read_yaml(@base, name)
40 | self.data['layout'] ||= 'post'
41 |
42 | #If we've added a date and time to the yaml, use that instead of the filename date
43 | #Means we'll sort correctly.
44 | if self.data.has_key?('date')
45 | # ensure Time via to_s and reparse
46 | self.date = Time.parse(self.data["date"].to_s.gsub(/-$/,""))
47 | end
48 |
49 | if self.data.has_key?('published') && self.data['published'] == false
50 | self.published = false
51 | else
52 | self.published = true
53 | end
54 |
55 | self.tags = self.data.pluralized_array("tag", "tags")
56 |
57 | if self.categories.empty?
58 | self.categories = self.data.pluralized_array('category', 'categories')
59 | end
60 | end
61 |
62 | # Spaceship is based on Post#date, slug
63 | #
64 | # Returns -1, 0, 1
65 | def <=>(other)
66 | if self.date && other.date
67 | cmp = self.date <=> other.date
68 | else
69 | cmp = self.slug <=> other.slug
70 | end
71 | if 0 == cmp
72 | cmp = self.slug <=> other.slug
73 | end
74 | return cmp
75 | end
76 |
77 | # Extract information from the post filename
78 | # +name+ is the String filename of the post file
79 | #
80 | # Returns nothing
81 | def process(name)
82 | m, cats, date, slug, ext = *name.match(MATCHER)
83 | self.date = Time.parse(date) if date
84 | self.slug = slug
85 | self.ext = ext
86 | end
87 |
88 | # The generated directory into which the post will be placed
89 | # upon generation. This is derived from the permalink or, if
90 | # permalink is absent, set to the default date
91 | # e.g. "/2008/11/05/" if the permalink style is :date, otherwise nothing
92 | #
93 | # Returns
94 | def dir
95 | File.dirname(url)
96 | end
97 |
98 | # The full path and filename of the post.
99 | # Defined in the YAML of the post body
100 | # (Optional)
101 | #
102 | # Returns
103 | def permalink
104 | self.data && self.data['permalink']
105 | end
106 |
107 | def template
108 | case self.site.permalink_style
109 | when :pretty
110 | "/:categories/:year/:month/:day/:title/"
111 | when :none
112 | "/:categories/:title.html"
113 | when :date
114 | "/:categories/:year/:month/:day/:title.html"
115 | else
116 | self.site.permalink_style.to_s
117 | end
118 | end
119 |
120 | # The generated relative url of this post
121 | # e.g. /2008/11/05/my-awesome-post.html
122 | #
123 | # Returns
124 | def url
125 | return permalink if permalink
126 |
127 | @url ||= {
128 | "year" => (date ? date.strftime("%Y") : nil).to_s,
129 | "month" => (date ? date.strftime("%m") : nil).to_s,
130 | "day" => (date ? date.strftime("%d") : nil).to_s,
131 | "title" => CGI.escape(slug),
132 | "i_day" => (date ? date.strftime("%d").to_i.to_s : nil).to_s,
133 | "i_month" => (date ? date.strftime("%m").to_i.to_s : nil).to_s,
134 | "categories" => categories.join('/'),
135 | "output_ext" => self.output_ext
136 | }.inject(template) { |result, token|
137 | result.gsub(/:#{Regexp.escape token.first}/, token.last)
138 | }.gsub(/\/\//, "/")
139 | end
140 |
141 | # The UID for this post (useful in feeds)
142 | # e.g. /2008/11/05/my-awesome-post
143 | #
144 | # Returns
145 | def id
146 | File.join(self.dir, self.slug)
147 | end
148 |
149 | # Calculate related posts.
150 | #
151 | # Returns []
152 | def related_posts(posts)
153 | return [] unless posts.size > 1
154 |
155 | if self.site.lsi
156 | self.class.lsi ||= begin
157 | puts "Running the classifier... this could take a while."
158 | lsi = Classifier::LSI.new
159 | posts.each { |x| $stdout.print(".");$stdout.flush;lsi.add_item(x) }
160 | puts ""
161 | lsi
162 | end
163 |
164 | related = self.class.lsi.find_related(self.content, 11)
165 | related - [self]
166 | else
167 | (posts - [self])[0..9]
168 | end
169 | end
170 |
171 | # Add any necessary layouts to this post
172 | # +layouts+ is a Hash of {"name" => "layout"}
173 | # +site_payload+ is the site payload hash
174 | #
175 | # Returns nothing
176 | def render(layouts, site_payload)
177 | # construct payload
178 | payload = {
179 | "site" => { "related_posts" => related_posts(site_payload["site"]["posts"]) },
180 | "page" => self.to_liquid
181 | }.deep_merge(site_payload)
182 |
183 | do_layout(payload, layouts)
184 | end
185 |
186 | # Obtain destination path.
187 | # +dest+ is the String path to the destination dir
188 | #
189 | # Returns destination file path.
190 | def destination(dest)
191 | # The url needs to be unescaped in order to preserve the correct filename
192 | path = File.join(dest, CGI.unescape(self.url))
193 | path = File.join(path, "index.html") if template[/\.html$/].nil?
194 | path
195 | end
196 |
197 | # Write the generated post file to the destination directory.
198 | # +dest+ is the String path to the destination dir
199 | #
200 | # Returns nothing
201 | def write(dest)
202 | path = destination(dest)
203 | FileUtils.mkdir_p(File.dirname(path))
204 | File.open(path, 'w') do |f|
205 | f.write(self.output)
206 | end
207 | end
208 |
209 | # Convert this post into a Hash for use in Liquid templates.
210 | #
211 | # Returns
212 | def to_liquid
213 | self.data.deep_merge({
214 | "title" => self.data["title"] || self.slug.split('-').select {|w| w.capitalize! || w }.join(' '),
215 | "url" => self.url,
216 | "date" => self.date,
217 | "id" => self.id,
218 | "categories" => self.categories,
219 | "next" => self.next,
220 | "previous" => self.previous,
221 | "tags" => self.tags,
222 | "content" => self.content })
223 | end
224 |
225 | def inspect
226 | ""
227 | end
228 |
229 | def next
230 | pos = self.site.posts.index(self)
231 |
232 | if pos && pos < self.site.posts.length-1
233 | self.site.posts[pos+1]
234 | else
235 | nil
236 | end
237 | end
238 |
239 | def previous
240 | pos = self.site.posts.index(self)
241 | if pos && pos > 0
242 | self.site.posts[pos-1]
243 | else
244 | nil
245 | end
246 | end
247 | end
248 |
249 | end
250 |
--------------------------------------------------------------------------------
/features/post_data.feature:
--------------------------------------------------------------------------------
1 | Feature: Post data
2 | As a hacker who likes to blog
3 | I want to be able to embed data into my posts
4 | In order to make the posts slightly dynamic
5 |
6 | Scenario: Use post.title variable
7 | Given I have a _posts directory
8 | And I have a _layouts directory
9 | And I have the following post:
10 | | title | date | layout | content |
11 | | Star Wars | 3/27/2009 | simple | Luke, I am your father. |
12 | And I have a simple layout that contains "Post title: {{ page.title }}"
13 | When I run jekyll
14 | Then the _site directory should exist
15 | And I should see "Post title: Star Wars" in "_site/2009/03/27/star-wars.html"
16 |
17 | Scenario: Use post.url variable
18 | Given I have a _posts directory
19 | And I have a _layouts directory
20 | And I have the following post:
21 | | title | date | layout | content |
22 | | Star Wars | 3/27/2009 | simple | Luke, I am your father. |
23 | And I have a simple layout that contains "Post url: {{ page.url }}"
24 | When I run jekyll
25 | Then the _site directory should exist
26 | And I should see "Post url: /2009/03/27/star-wars.html" in "_site/2009/03/27/star-wars.html"
27 |
28 | Scenario: Use post.date variable
29 | Given I have a _posts directory
30 | And I have a _layouts directory
31 | And I have the following post:
32 | | title | date | layout | content |
33 | | Star Wars | 3/27/2009 | simple | Luke, I am your father. |
34 | And I have a simple layout that contains "Post date: {{ page.date }}"
35 | When I run jekyll
36 | Then the _site directory should exist
37 | And I should see "Post date: Fri Mar 27" in "_site/2009/03/27/star-wars.html"
38 |
39 | Scenario: Use post.id variable
40 | Given I have a _posts directory
41 | And I have a _layouts directory
42 | And I have the following post:
43 | | title | date | layout | content |
44 | | Star Wars | 3/27/2009 | simple | Luke, I am your father. |
45 | And I have a simple layout that contains "Post id: {{ page.id }}"
46 | When I run jekyll
47 | Then the _site directory should exist
48 | And I should see "Post id: /2009/03/27/star-wars" in "_site/2009/03/27/star-wars.html"
49 |
50 | Scenario: Use post.content variable
51 | Given I have a _posts directory
52 | And I have a _layouts directory
53 | And I have the following post:
54 | | title | date | layout | content |
55 | | Star Wars | 3/27/2009 | simple | Luke, I am your father. |
56 | And I have a simple layout that contains "Post content: {{ content }}"
57 | When I run jekyll
58 | Then the _site directory should exist
59 | And I should see "Post content:
Luke, I am your father.
" in "_site/2009/03/27/star-wars.html"
60 |
61 | Scenario: Use post.categories variable when category is in a folder
62 | Given I have a movies directory
63 | And I have a movies/_posts directory
64 | And I have a _layouts directory
65 | And I have the following post in "movies":
66 | | title | date | layout | content |
67 | | Star Wars | 3/27/2009 | simple | Luke, I am your father. |
68 | And I have a simple layout that contains "Post category: {{ page.categories }}"
69 | When I run jekyll
70 | Then the _site directory should exist
71 | And I should see "Post category: movies" in "_site/movies/2009/03/27/star-wars.html"
72 |
73 | Scenario: Use post.tags variable
74 | Given I have a _posts directory
75 | And I have a _layouts directory
76 | And I have the following post:
77 | | title | date | layout | tag | content |
78 | | Star Wars | 5/18/2009 | simple | twist | Luke, I am your father. |
79 | And I have a simple layout that contains "Post tags: {{ page.tags }}"
80 | When I run jekyll
81 | Then the _site directory should exist
82 | And I should see "Post tags: twist" in "_site/2009/05/18/star-wars.html"
83 |
84 | Scenario: Use post.categories variable when categories are in folders
85 | Given I have a scifi directory
86 | And I have a scifi/movies directory
87 | And I have a scifi/movies/_posts directory
88 | And I have a _layouts directory
89 | And I have the following post in "scifi/movies":
90 | | title | date | layout | content |
91 | | Star Wars | 3/27/2009 | simple | Luke, I am your father. |
92 | And I have a simple layout that contains "Post categories: {{ page.categories | array_to_sentence_string }}"
93 | When I run jekyll
94 | Then the _site directory should exist
95 | And I should see "Post categories: scifi and movies" in "_site/scifi/movies/2009/03/27/star-wars.html"
96 |
97 | Scenario: Use post.categories variable when category is in YAML
98 | Given I have a _posts directory
99 | And I have a _layouts directory
100 | And I have the following post:
101 | | title | date | layout | category | content |
102 | | Star Wars | 3/27/2009 | simple | movies | Luke, I am your father. |
103 | And I have a simple layout that contains "Post category: {{ page.categories }}"
104 | When I run jekyll
105 | Then the _site directory should exist
106 | And I should see "Post category: movies" in "_site/movies/2009/03/27/star-wars.html"
107 |
108 | Scenario: Use post.categories variable when categories are in YAML
109 | Given I have a _posts directory
110 | And I have a _layouts directory
111 | And I have the following post:
112 | | title | date | layout | categories | content |
113 | | Star Wars | 3/27/2009 | simple | ['scifi', 'movies'] | Luke, I am your father. |
114 | And I have a simple layout that contains "Post categories: {{ page.categories | array_to_sentence_string }}"
115 | When I run jekyll
116 | Then the _site directory should exist
117 | And I should see "Post categories: scifi and movies" in "_site/scifi/movies/2009/03/27/star-wars.html"
118 |
119 | Scenario: Disable a post from being published
120 | Given I have a _posts directory
121 | And I have an "index.html" file that contains "Published!"
122 | And I have the following post:
123 | | title | date | layout | published | content |
124 | | Star Wars | 3/27/2009 | simple | false | Luke, I am your father. |
125 | When I run jekyll
126 | Then the _site directory should exist
127 | And the "_site/2009/03/27/star-wars.html" file should not exist
128 | And I should see "Published!" in "_site/index.html"
129 |
130 | Scenario: Use a custom variable
131 | Given I have a _posts directory
132 | And I have a _layouts directory
133 | And I have the following post:
134 | | title | date | layout | author | content |
135 | | Star Wars | 3/27/2009 | simple | Darth Vader | Luke, I am your father. |
136 | And I have a simple layout that contains "Post author: {{ page.author }}"
137 | When I run jekyll
138 | Then the _site directory should exist
139 | And I should see "Post author: Darth Vader" in "_site/2009/03/27/star-wars.html"
140 |
141 | Scenario: Previous and next posts title
142 | Given I have a _posts directory
143 | And I have a _layouts directory
144 | And I have the following posts:
145 | | title | date | layout | author | content |
146 | | Star Wars | 3/27/2009 | ordered | Darth Vader | Luke, I am your father. |
147 | | Some like it hot | 4/27/2009 | ordered | Osgood | Nobody is perfect. |
148 | | Terminator | 5/27/2009 | ordered | Arnold | Sayonara, baby |
149 | And I have a ordered layout that contains "Previous post: {{ page.previous.title }} and next post: {{ page.next.title }}"
150 | When I run jekyll
151 | Then the _site directory should exist
152 | And I should see "next post: Some like it hot" in "_site/2009/03/27/star-wars.html"
153 | And I should see "Previous post: Some like it hot" in "_site/2009/05/27/terminator.html"
154 |
--------------------------------------------------------------------------------
/lib/jekyll/site.rb:
--------------------------------------------------------------------------------
1 | module Jekyll
2 |
3 | class Site
4 | attr_accessor :config, :layouts, :posts, :pages, :static_files,
5 | :categories, :exclude, :source, :dest, :lsi, :pygments,
6 | :permalink_style, :tags, :time, :future, :safe, :plugins, :limit_posts
7 |
8 | attr_accessor :converters, :generators
9 |
10 | # Initialize the site
11 | # +config+ is a Hash containing site configurations details
12 | #
13 | # Returns
14 | def initialize(config)
15 | self.config = config.clone
16 |
17 | self.safe = config['safe']
18 | self.source = File.expand_path(config['source'])
19 | self.dest = File.expand_path(config['destination'])
20 | self.plugins = File.expand_path(config['plugins'])
21 | self.lsi = config['lsi']
22 | self.pygments = config['pygments']
23 | self.permalink_style = config['permalink'].to_sym
24 | self.exclude = config['exclude'] || []
25 | self.future = config['future']
26 | self.limit_posts = config['limit_posts'] || nil
27 |
28 | self.reset
29 | self.setup
30 | end
31 |
32 | def reset
33 | self.time = if self.config['time']
34 | Time.parse(self.config['time'].to_s)
35 | else
36 | Time.now
37 | end
38 | self.layouts = {}
39 | self.posts = []
40 | self.pages = []
41 | self.static_files = []
42 | self.categories = Hash.new { |hash, key| hash[key] = [] }
43 | self.tags = Hash.new { |hash, key| hash[key] = [] }
44 |
45 | raise ArgumentError, "Limit posts must be nil or >= 1" if !self.limit_posts.nil? && self.limit_posts < 1
46 | end
47 |
48 | def setup
49 | require 'classifier' if self.lsi
50 |
51 | # If safe mode is off, load in any ruby files under the plugins
52 | # directory.
53 | unless self.safe
54 | Dir[File.join(self.plugins, "**/*.rb")].each do |f|
55 | require f
56 | end
57 | end
58 |
59 | self.converters = Jekyll::Converter.subclasses.select do |c|
60 | !self.safe || c.safe
61 | end.map do |c|
62 | c.new(self.config)
63 | end
64 |
65 | self.generators = Jekyll::Generator.subclasses.select do |c|
66 | !self.safe || c.safe
67 | end.map do |c|
68 | c.new(self.config)
69 | end
70 | end
71 |
72 | # Do the actual work of processing the site and generating the
73 | # real deal. 5 phases; reset, read, generate, render, write. This allows
74 | # rendering to have full site payload available.
75 | #
76 | # Returns nothing
77 | def process
78 | self.reset
79 | self.read
80 | self.generate
81 | self.render
82 | self.cleanup
83 | self.write
84 | end
85 |
86 | def read
87 | self.read_layouts # existing implementation did this at top level only so preserved that
88 | self.read_directories
89 | end
90 |
91 | # Read all the files in //_layouts and create a new Layout
92 | # object with each one.
93 | #
94 | # Returns nothing
95 | def read_layouts(dir = '')
96 | base = File.join(self.source, dir, "_layouts")
97 | return unless File.exists?(base)
98 | entries = []
99 | Dir.chdir(base) { entries = filter_entries(Dir['*.*']) }
100 |
101 | entries.each do |f|
102 | name = f.split(".")[0..-2].join(".")
103 | self.layouts[name] = Layout.new(self, base, f)
104 | end
105 | end
106 |
107 | # Read all the files in //_posts and create a new Post
108 | # object with each one.
109 | #
110 | # Returns nothing
111 | def read_posts(dir)
112 | base = File.join(self.source, dir, '_posts')
113 | return unless File.exists?(base)
114 | entries = Dir.chdir(base) { filter_entries(Dir['**/*']) }
115 |
116 | # first pass processes, but does not yet render post content
117 | entries.each do |f|
118 | if Post.valid?(f)
119 | post = Post.new(self, self.source, dir, f)
120 |
121 | if post.published && (self.future || post.date <= self.time)
122 | self.posts << post
123 | post.categories.each { |c| self.categories[c] << post }
124 | post.tags.each { |c| self.tags[c] << post }
125 | end
126 | end
127 | end
128 |
129 | self.posts.sort!
130 |
131 | # limit the posts if :limit_posts option is set
132 | self.posts = self.posts[-limit_posts, limit_posts] if limit_posts
133 | end
134 |
135 | def generate
136 | self.generators.each do |generator|
137 | generator.generate(self)
138 | end
139 | end
140 |
141 | def render
142 | self.posts.each do |post|
143 | post.render(self.layouts, site_payload)
144 | end
145 |
146 | self.pages.each do |page|
147 | page.render(self.layouts, site_payload)
148 | end
149 |
150 | self.categories.values.map { |ps| ps.sort! { |a, b| b <=> a} }
151 | self.tags.values.map { |ps| ps.sort! { |a, b| b <=> a} }
152 | rescue Errno::ENOENT => e
153 | # ignore missing layout dir
154 | end
155 |
156 | # Remove orphaned files and empty directories in destination
157 | #
158 | # Returns nothing
159 | def cleanup
160 | # all files and directories in destination, including hidden ones
161 | dest_files = []
162 | Dir.glob(File.join(self.dest, "**", "*"), File::FNM_DOTMATCH) do |file|
163 | dest_files << file unless file =~ /\/\.{1,2}$/
164 | end
165 |
166 | # files to be written
167 | files = []
168 | self.posts.each do |post|
169 | files << post.destination(self.dest)
170 | end
171 | self.pages.each do |page|
172 | files << page.destination(self.dest)
173 | end
174 | self.static_files.each do |sf|
175 | files << sf.destination(self.dest)
176 | end
177 |
178 | # adding files' parent directories
179 | files.each { |file| files << File.dirname(file) unless files.include? File.dirname(file) }
180 |
181 | obsolete_files = dest_files - files
182 |
183 | FileUtils.rm_rf(obsolete_files)
184 | end
185 |
186 | # Write static files, pages and posts
187 | #
188 | # Returns nothing
189 | def write
190 | self.posts.each do |post|
191 | post.write(self.dest)
192 | end
193 | self.pages.each do |page|
194 | page.write(self.dest)
195 | end
196 | self.static_files.each do |sf|
197 | sf.write(self.dest)
198 | end
199 | end
200 |
201 | # Reads the directories and finds posts, pages and static files that will
202 | # become part of the valid site according to the rules in +filter_entries+.
203 | # The +dir+ String is a relative path used to call this method
204 | # recursively as it descends through directories
205 | #
206 | # Returns nothing
207 | def read_directories(dir = '')
208 | base = File.join(self.source, dir)
209 | entries = filter_entries(Dir.entries(base))
210 |
211 | self.read_posts(dir)
212 |
213 | entries.each do |f|
214 | f_abs = File.join(base, f)
215 | f_rel = File.join(dir, f)
216 | if File.directory?(f_abs)
217 | next if self.dest.sub(/\/$/, '') == f_abs
218 | read_directories(f_rel)
219 | elsif !File.symlink?(f_abs)
220 | first3 = File.open(f_abs) { |fd| fd.read(3) }
221 | if first3 == "---"
222 | # file appears to have a YAML header so process it as a page
223 | pages << Page.new(self, self.source, dir, f)
224 | else
225 | # otherwise treat it as a static file
226 | static_files << StaticFile.new(self, self.source, dir, f)
227 | end
228 | end
229 | end
230 | end
231 |
232 | # Constructs a hash map of Posts indexed by the specified Post attribute
233 | #
234 | # Returns {post_attr => []}
235 | def post_attr_hash(post_attr)
236 | # Build a hash map based on the specified post attribute ( post attr => array of posts )
237 | # then sort each array in reverse order
238 | hash = Hash.new { |hash, key| hash[key] = Array.new }
239 | self.posts.each { |p| p.send(post_attr.to_sym).each { |t| hash[t] << p } }
240 | hash.values.map { |sortme| sortme.sort! { |a, b| b <=> a} }
241 | return hash
242 | end
243 |
244 | # The Hash payload containing site-wide data
245 | #
246 | # Returns {"site" => {"time" =>