jekyll 0.4.1 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of jekyll might be problematic. Click here for more details.
- data/History.txt +31 -1
- data/README.textile +23 -457
- data/Rakefile +91 -0
- data/VERSION.yml +2 -2
- data/bin/jekyll +61 -57
- data/lib/jekyll.rb +48 -32
- data/lib/jekyll/albino.rb +13 -7
- data/lib/jekyll/converters/mephisto.rb +8 -8
- data/lib/jekyll/converters/mt.rb +8 -8
- data/lib/jekyll/converters/textpattern.rb +4 -4
- data/lib/jekyll/converters/typo.rb +8 -8
- data/lib/jekyll/converters/wordpress.rb +1 -2
- data/lib/jekyll/convertible.rb +33 -22
- data/lib/jekyll/core_ext.rb +5 -5
- data/lib/jekyll/filters.rb +15 -7
- data/lib/jekyll/layout.rb +9 -6
- data/lib/jekyll/page.rb +13 -10
- data/lib/jekyll/post.rb +108 -39
- data/lib/jekyll/site.rb +121 -51
- data/lib/jekyll/tags/highlight.rb +12 -9
- data/lib/jekyll/tags/include.rb +5 -5
- data/test/helper.rb +20 -6
- data/test/source/_posts/2008-02-02-not-published.textile +8 -0
- data/test/source/_posts/2008-02-02-published.textile +8 -0
- data/test/source/_posts/2009-01-27-array-categories.textile +10 -0
- data/test/source/_posts/2009-01-27-categories.textile +7 -0
- data/test/source/_posts/2009-01-27-category.textile +7 -0
- data/test/source/_posts/2009-03-12-hash-#1.markdown +6 -0
- data/test/test_filters.rb +39 -27
- data/test/test_generated_site.rb +32 -16
- data/test/test_post.rb +258 -102
- data/test/test_site.rb +60 -28
- data/test/test_tags.rb +103 -18
- metadata +25 -70
- data/test/dest/2008/10/18/foo-bar.html +0 -28
- data/test/dest/2008/11/21/complex.html +0 -29
- data/test/dest/2008/12/13/include.html +0 -30
- data/test/dest/_posts/2008-10-18-foo-bar.html +0 -28
- data/test/dest/_posts/2008-11-21-complex.html +0 -29
- data/test/dest/_posts/2008-12-03-permalinked-post.html +0 -2
- data/test/dest/_posts/2008-12-13-include.html +0 -30
- data/test/dest/category/2008/09/23/categories.html +0 -27
- data/test/dest/category/_posts/2008-9-23-categories.html +0 -27
- data/test/dest/css/screen.css +0 -76
- data/test/dest/foo/2008/12/12/topical-post.html +0 -28
- data/test/dest/foo/_posts/bar/2008-12-12-topical-post.html +0 -28
- data/test/dest/index.html +0 -60
- data/test/dest/my_category/permalinked-post +0 -2
- data/test/dest/z_category/2008/09/23/categories.html +0 -27
- data/test/dest/z_category/_posts/2008-9-23-categories.html +0 -27
- data/test/test_jekyll.rb +0 -0
data/lib/jekyll/site.rb
CHANGED
@@ -1,84 +1,147 @@
|
|
1
1
|
module Jekyll
|
2
|
-
|
2
|
+
|
3
3
|
class Site
|
4
|
-
attr_accessor :
|
5
|
-
attr_accessor :
|
6
|
-
|
4
|
+
attr_accessor :config, :layouts, :posts, :categories, :exclude
|
5
|
+
attr_accessor :source, :dest, :lsi, :pygments, :permalink_style
|
6
|
+
|
7
7
|
# Initialize the site
|
8
|
-
# +
|
9
|
-
# the proto-site
|
10
|
-
# +dest+ is the String path to the directory where the generated
|
11
|
-
# site should be written
|
8
|
+
# +config+ is a Hash containing site configurations details
|
12
9
|
#
|
13
10
|
# Returns <Site>
|
14
|
-
def initialize(
|
15
|
-
self.
|
16
|
-
|
17
|
-
self.
|
18
|
-
self.
|
19
|
-
self.
|
11
|
+
def initialize(config)
|
12
|
+
self.config = config.clone
|
13
|
+
|
14
|
+
self.source = config['source']
|
15
|
+
self.dest = config['destination']
|
16
|
+
self.lsi = config['lsi']
|
17
|
+
self.pygments = config['pygments']
|
18
|
+
self.permalink_style = config['permalink'].to_sym
|
19
|
+
self.exclude = config['exclude'] || []
|
20
|
+
|
21
|
+
self.reset
|
22
|
+
self.setup
|
23
|
+
end
|
24
|
+
|
25
|
+
def reset
|
26
|
+
self.layouts = {}
|
27
|
+
self.posts = []
|
28
|
+
self.categories = Hash.new { |hash, key| hash[key] = Array.new }
|
29
|
+
end
|
30
|
+
|
31
|
+
def setup
|
32
|
+
# Check to see if LSI is enabled.
|
33
|
+
require 'classifier' if self.lsi
|
34
|
+
|
35
|
+
# Set the Markdown interpreter (and Maruku self.config, if necessary)
|
36
|
+
case self.config['markdown']
|
37
|
+
when 'rdiscount'
|
38
|
+
begin
|
39
|
+
require 'rdiscount'
|
40
|
+
|
41
|
+
def markdown(content)
|
42
|
+
RDiscount.new(content).to_html
|
43
|
+
end
|
44
|
+
|
45
|
+
rescue LoadError
|
46
|
+
puts 'You must have the rdiscount gem installed first'
|
47
|
+
end
|
48
|
+
when 'maruku'
|
49
|
+
begin
|
50
|
+
require 'maruku'
|
51
|
+
|
52
|
+
def markdown(content)
|
53
|
+
Maruku.new(content).to_html
|
54
|
+
end
|
55
|
+
|
56
|
+
if self.config['maruku']['use_divs']
|
57
|
+
require 'maruku/ext/div'
|
58
|
+
puts 'Maruku: Using extended syntax for div elements.'
|
59
|
+
end
|
60
|
+
|
61
|
+
if self.config['maruku']['use_tex']
|
62
|
+
require 'maruku/ext/math'
|
63
|
+
puts "Maruku: Using LaTeX extension. Images in `#{self.config['maruku']['png_dir']}`."
|
64
|
+
|
65
|
+
# Switch off MathML output
|
66
|
+
MaRuKu::Globals[:html_math_output_mathml] = false
|
67
|
+
MaRuKu::Globals[:html_math_engine] = 'none'
|
68
|
+
|
69
|
+
# Turn on math to PNG support with blahtex
|
70
|
+
# Resulting PNGs stored in `images/latex`
|
71
|
+
MaRuKu::Globals[:html_math_output_png] = true
|
72
|
+
MaRuKu::Globals[:html_png_engine] = self.config['maruku']['png_engine']
|
73
|
+
MaRuKu::Globals[:html_png_dir] = self.config['maruku']['png_dir']
|
74
|
+
MaRuKu::Globals[:html_png_url] = self.config['maruku']['png_url']
|
75
|
+
end
|
76
|
+
rescue LoadError
|
77
|
+
puts "The maruku gem is required for markdown support!"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def textile(content)
|
83
|
+
RedCloth.new(content).to_html
|
20
84
|
end
|
21
|
-
|
85
|
+
|
22
86
|
# Do the actual work of processing the site and generating the
|
23
87
|
# real deal.
|
24
88
|
#
|
25
89
|
# Returns nothing
|
26
90
|
def process
|
91
|
+
self.reset
|
27
92
|
self.read_layouts
|
28
93
|
self.transform_pages
|
29
94
|
self.write_posts
|
30
95
|
end
|
31
|
-
|
32
|
-
# Read all the files in <source>/_layouts
|
33
|
-
# (end with "~") into memory for later use.
|
96
|
+
|
97
|
+
# Read all the files in <source>/_layouts into memory for later use.
|
34
98
|
#
|
35
99
|
# Returns nothing
|
36
100
|
def read_layouts
|
37
101
|
base = File.join(self.source, "_layouts")
|
38
|
-
entries =
|
39
|
-
|
40
|
-
|
41
|
-
|
102
|
+
entries = []
|
103
|
+
Dir.chdir(base) { entries = filter_entries(Dir['*.*']) }
|
104
|
+
|
42
105
|
entries.each do |f|
|
43
106
|
name = f.split(".")[0..-2].join(".")
|
44
|
-
self.layouts[name] = Layout.new(base, f)
|
107
|
+
self.layouts[name] = Layout.new(self, base, f)
|
45
108
|
end
|
46
109
|
rescue Errno::ENOENT => e
|
47
110
|
# ignore missing layout dir
|
48
111
|
end
|
49
|
-
|
50
|
-
# Read all the files in <base>/_posts
|
51
|
-
# and create a new Post object with each one.
|
112
|
+
|
113
|
+
# Read all the files in <base>/_posts and create a new Post object with each one.
|
52
114
|
#
|
53
115
|
# Returns nothing
|
54
116
|
def read_posts(dir)
|
55
117
|
base = File.join(self.source, dir, '_posts')
|
56
|
-
|
57
118
|
entries = []
|
58
|
-
Dir.chdir(base) { entries = Dir['**/*'] }
|
59
|
-
entries = entries.reject { |e| e[-1..-1] == '~' }
|
60
|
-
entries = entries.reject { |e| File.directory?(File.join(base, e)) }
|
119
|
+
Dir.chdir(base) { entries = filter_entries(Dir['**/*']) }
|
61
120
|
|
62
121
|
# first pass processes, but does not yet render post content
|
63
122
|
entries.each do |f|
|
64
123
|
if Post.valid?(f)
|
65
|
-
post = Post.new(self.source, dir, f)
|
66
|
-
|
67
|
-
post.
|
124
|
+
post = Post.new(self, self.source, dir, f)
|
125
|
+
|
126
|
+
if post.published
|
127
|
+
self.posts << post
|
128
|
+
post.categories.each { |c| self.categories[c] << post }
|
129
|
+
end
|
68
130
|
end
|
69
131
|
end
|
70
|
-
|
132
|
+
|
133
|
+
self.posts.sort!
|
134
|
+
|
71
135
|
# second pass renders each post now that full site payload is available
|
72
136
|
self.posts.each do |post|
|
73
137
|
post.render(self.layouts, site_payload)
|
74
138
|
end
|
75
|
-
|
76
|
-
self.posts.sort!
|
139
|
+
|
77
140
|
self.categories.values.map { |cats| cats.sort! { |a, b| b <=> a} }
|
78
141
|
rescue Errno::ENOENT => e
|
79
142
|
# ignore missing layout dir
|
80
143
|
end
|
81
|
-
|
144
|
+
|
82
145
|
# Write each post to <dest>/<year>/<month>/<day>/<slug>
|
83
146
|
#
|
84
147
|
# Returns nothing
|
@@ -87,10 +150,10 @@ module Jekyll
|
|
87
150
|
post.write(self.dest)
|
88
151
|
end
|
89
152
|
end
|
90
|
-
|
153
|
+
|
91
154
|
# Copy all regular files from <source> to <dest>/ ignoring
|
92
155
|
# any files/directories that are hidden or backup files (start
|
93
|
-
# with "." or end with "~") or contain site content (start with "_")
|
156
|
+
# with "." or "#" or end with "~") or contain site content (start with "_")
|
94
157
|
# unless they are "_posts" directories or web server files such as
|
95
158
|
# '.htaccess'
|
96
159
|
# The +dir+ String is a relative path used to call this method
|
@@ -99,18 +162,14 @@ module Jekyll
|
|
99
162
|
# Returns nothing
|
100
163
|
def transform_pages(dir = '')
|
101
164
|
base = File.join(self.source, dir)
|
102
|
-
entries = Dir.entries(base)
|
103
|
-
entries = entries.reject { |e| e[-1..-1] == '~' }
|
104
|
-
entries = entries.reject do |e|
|
105
|
-
(e != '_posts') and ['.', '_'].include?(e[0..0]) unless ['.htaccess'].include?(e)
|
106
|
-
end
|
165
|
+
entries = filter_entries(Dir.entries(base))
|
107
166
|
directories = entries.select { |e| File.directory?(File.join(base, e)) }
|
108
167
|
files = entries.reject { |e| File.directory?(File.join(base, e)) }
|
109
168
|
|
110
|
-
# we need to make sure to process _posts *first* otherwise they
|
169
|
+
# we need to make sure to process _posts *first* otherwise they
|
111
170
|
# might not be available yet to other templates as {{ site.posts }}
|
112
|
-
if
|
113
|
-
|
171
|
+
if directories.include?('_posts')
|
172
|
+
directories.delete('_posts')
|
114
173
|
read_posts(dir)
|
115
174
|
end
|
116
175
|
[directories, files].each do |entries|
|
@@ -120,10 +179,10 @@ module Jekyll
|
|
120
179
|
transform_pages(File.join(dir, f))
|
121
180
|
else
|
122
181
|
first3 = File.open(File.join(self.source, dir, f)) { |fd| fd.read(3) }
|
123
|
-
|
182
|
+
|
124
183
|
if first3 == "---"
|
125
184
|
# file appears to have a YAML header so process it as a page
|
126
|
-
page = Page.new(self.source, dir, f)
|
185
|
+
page = Page.new(self, self.source, dir, f)
|
127
186
|
page.render(self.layouts, site_payload)
|
128
187
|
page.write(self.dest)
|
129
188
|
else
|
@@ -156,12 +215,23 @@ module Jekyll
|
|
156
215
|
# "topics" => [<Post>] }}
|
157
216
|
def site_payload
|
158
217
|
{"site" => {
|
159
|
-
"time" => Time.now,
|
218
|
+
"time" => Time.now,
|
160
219
|
"posts" => self.posts.sort { |a,b| b <=> a },
|
161
220
|
"categories" => post_attr_hash('categories'),
|
162
221
|
"topics" => post_attr_hash('topics')
|
163
222
|
}}
|
164
223
|
end
|
165
|
-
end
|
166
224
|
|
225
|
+
# Filter out any files/directories that are hidden or backup files (start
|
226
|
+
# with "." or "#" or end with "~") or contain site content (start with "_")
|
227
|
+
# unless they are "_posts" directories or web server files such as
|
228
|
+
# '.htaccess'
|
229
|
+
def filter_entries(entries)
|
230
|
+
entries = entries.reject do |e|
|
231
|
+
unless ['_posts', '.htaccess'].include?(e)
|
232
|
+
['.', '_', '#'].include?(e[0..0]) || e[-1..-1] == '~' || self.exclude.include?(e)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
167
237
|
end
|
@@ -1,10 +1,11 @@
|
|
1
1
|
module Jekyll
|
2
|
-
|
2
|
+
|
3
3
|
class HighlightBlock < Liquid::Block
|
4
4
|
include Liquid::StandardFilters
|
5
|
+
|
5
6
|
# we need a language, but the linenos argument is optional.
|
6
7
|
SYNTAX = /(\w+)\s?(:?linenos)?\s?/
|
7
|
-
|
8
|
+
|
8
9
|
def initialize(tag_name, markup, tokens)
|
9
10
|
super
|
10
11
|
if markup =~ SYNTAX
|
@@ -19,23 +20,25 @@ module Jekyll
|
|
19
20
|
raise SyntaxError.new("Syntax Error in 'highlight' - Valid syntax: highlight <lang> [linenos]")
|
20
21
|
end
|
21
22
|
end
|
22
|
-
|
23
|
+
|
23
24
|
def render(context)
|
24
|
-
if
|
25
|
+
if context.registers[:site].pygments
|
25
26
|
render_pygments(context, super.to_s)
|
26
27
|
else
|
27
28
|
render_codehighlighter(context, super.to_s)
|
28
29
|
end
|
29
30
|
end
|
30
|
-
|
31
|
+
|
31
32
|
def render_pygments(context, code)
|
32
|
-
if
|
33
|
+
if context["content_type"] == "markdown"
|
33
34
|
return "\n" + Albino.new(code, @lang).to_s(@options) + "\n"
|
35
|
+
elsif context["content_type"] == "textile"
|
36
|
+
return "<notextile>" + Albino.new(code, @lang).to_s(@options) + "</notextile>"
|
34
37
|
else
|
35
|
-
|
38
|
+
return Albino.new(code, @lang).to_s(@options)
|
36
39
|
end
|
37
40
|
end
|
38
|
-
|
41
|
+
|
39
42
|
def render_codehighlighter(context, code)
|
40
43
|
#The div is required because RDiscount blows ass
|
41
44
|
<<-HTML
|
@@ -47,7 +50,7 @@ module Jekyll
|
|
47
50
|
HTML
|
48
51
|
end
|
49
52
|
end
|
50
|
-
|
53
|
+
|
51
54
|
end
|
52
55
|
|
53
56
|
Liquid::Template.register_tag('highlight', Jekyll::HighlightBlock)
|
data/lib/jekyll/tags/include.rb
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
module Jekyll
|
2
|
-
|
2
|
+
|
3
3
|
class IncludeTag < Liquid::Tag
|
4
4
|
def initialize(tag_name, file, tokens)
|
5
5
|
super
|
6
6
|
@file = file.strip
|
7
7
|
end
|
8
|
-
|
8
|
+
|
9
9
|
def render(context)
|
10
10
|
if @file !~ /^[a-zA-Z0-9_\/\.-]+$/ || @file =~ /\.\// || @file =~ /\/\./
|
11
11
|
return "Include file '#{@file}' contains invalid characters or sequences"
|
12
12
|
end
|
13
|
-
|
14
|
-
Dir.chdir(File.join(
|
13
|
+
|
14
|
+
Dir.chdir(File.join(context.registers[:site].source, '_includes')) do
|
15
15
|
choices = Dir['**/*'].reject { |x| File.symlink?(x) }
|
16
16
|
if choices.include?(@file)
|
17
17
|
source = File.read(@file)
|
@@ -25,7 +25,7 @@ module Jekyll
|
|
25
25
|
end
|
26
26
|
end
|
27
27
|
end
|
28
|
-
|
28
|
+
|
29
29
|
end
|
30
30
|
|
31
31
|
Liquid::Template.register_tag('include', Jekyll::IncludeTag)
|
data/test/helper.rb
CHANGED
@@ -1,13 +1,27 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'RedCloth', '= 4.1.0'
|
3
|
+
|
1
4
|
require File.join(File.dirname(__FILE__), *%w[.. lib jekyll])
|
2
5
|
|
3
6
|
require 'test/unit'
|
7
|
+
require 'redgreen'
|
8
|
+
require 'shoulda'
|
9
|
+
require 'rr'
|
4
10
|
|
5
11
|
include Jekyll
|
6
12
|
|
7
|
-
|
8
|
-
|
9
|
-
|
13
|
+
class Test::Unit::TestCase
|
14
|
+
include RR::Adapters::TestUnit
|
15
|
+
|
16
|
+
def dest_dir(*subdirs)
|
17
|
+
File.join(File.dirname(__FILE__), 'dest', *subdirs)
|
18
|
+
end
|
10
19
|
|
11
|
-
def
|
12
|
-
|
13
|
-
end
|
20
|
+
def source_dir(*subdirs)
|
21
|
+
File.join(File.dirname(__FILE__), 'source', *subdirs)
|
22
|
+
end
|
23
|
+
|
24
|
+
def clear_dest
|
25
|
+
FileUtils.rm_rf(dest_dir)
|
26
|
+
end
|
27
|
+
end
|
data/test/test_filters.rb
CHANGED
@@ -1,37 +1,49 @@
|
|
1
1
|
require File.dirname(__FILE__) + '/helper'
|
2
2
|
|
3
3
|
class TestFilters < Test::Unit::TestCase
|
4
|
-
|
5
4
|
class JekyllFilter
|
6
5
|
include Jekyll::Filters
|
7
6
|
end
|
8
|
-
|
9
|
-
def setup
|
10
|
-
@filter = JekyllFilter.new
|
11
|
-
end
|
12
7
|
|
13
|
-
|
14
|
-
|
15
|
-
|
8
|
+
context "filters" do
|
9
|
+
setup do
|
10
|
+
@filter = JekyllFilter.new
|
11
|
+
end
|
16
12
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
13
|
+
should "textilize with simple string" do
|
14
|
+
assert_equal "<p>something <strong>really</strong> simple</p>", @filter.textilize("something *really* simple")
|
15
|
+
end
|
16
|
+
|
17
|
+
should "convert array to sentence string with no args" do
|
18
|
+
assert_equal "", @filter.array_to_sentence_string([])
|
19
|
+
end
|
20
|
+
|
21
|
+
should "convert array to sentence string with one arg" do
|
22
|
+
assert_equal "1", @filter.array_to_sentence_string([1])
|
23
|
+
assert_equal "chunky", @filter.array_to_sentence_string(["chunky"])
|
24
|
+
end
|
25
|
+
|
26
|
+
should "convert array to sentence string with two args" do
|
27
|
+
assert_equal "1 and 2", @filter.array_to_sentence_string([1, 2])
|
28
|
+
assert_equal "chunky and bacon", @filter.array_to_sentence_string(["chunky", "bacon"])
|
29
|
+
end
|
30
|
+
|
31
|
+
should "convert array to sentence string with multiple args" do
|
32
|
+
assert_equal "1, 2, 3, and 4", @filter.array_to_sentence_string([1, 2, 3, 4])
|
33
|
+
assert_equal "chunky, bacon, bits, and pieces", @filter.array_to_sentence_string(["chunky", "bacon", "bits", "pieces"])
|
34
|
+
end
|
35
|
+
|
36
|
+
should "escape xml with ampersands" do
|
37
|
+
assert_equal "AT&T", @filter.xml_escape("AT&T")
|
38
|
+
assert_equal "<code>command &lt;filename&gt;</code>", @filter.xml_escape("<code>command <filename></code>")
|
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
|
35
48
|
end
|
36
|
-
|
37
49
|
end
|