Change liquid converter order #897

Closed
wants to merge 6 commits into from
View
1 jekyll.gemspec
@@ -84,6 +84,7 @@ Gem::Specification.new do |s|
lib/jekyll/generator.rb
lib/jekyll/generators/pagination.rb
lib/jekyll/layout.rb
+ lib/jekyll/liquid_encoder.rb
lib/jekyll/mime.types
lib/jekyll/page.rb
lib/jekyll/plugin.rb
View
1 lib/jekyll.rb
@@ -31,6 +31,7 @@ def require_all(path)
require 'jekyll/site'
require 'jekyll/convertible'
require 'jekyll/layout'
+require 'jekyll/liquid_encoder'
require 'jekyll/page'
require 'jekyll/post'
require 'jekyll/draft'
View
13 lib/jekyll/convertible.rb
@@ -77,6 +77,15 @@ def do_layout(payload, layouts)
payload["pygments_prefix"] = converter.pygments_prefix
payload["pygments_suffix"] = converter.pygments_suffix
+ # Initialise the LiquidEncoder and encode text
+ encoder = Jekyll::LiquidEncoder.new(content)
+ self.content = encoder.encoded_content
+
+ # # Run transformation and decode Liquid
+ self.transform
+ self.content = encoder.decode(self.content)
+
+ # Run liquid
begin
self.content = Liquid::Template.parse(self.content).render!(payload, info)
rescue => e
@@ -86,9 +95,7 @@ def do_layout(payload, layouts)
end
abort("Build Failed")
end
-
- self.transform
-
+
# output keeps track of what will finally be written
self.output = self.content
View
126 lib/jekyll/liquid_encoder.rb
@@ -0,0 +1,126 @@
+require 'digest/md5'
+
+module Jekyll
+
+ # The LiquidEncoder class encodes a string's liquid commands, preventing them
+ # from being munged by converters.
+ class LiquidEncoder
+ # The encoded content
+ attr_reader :encoded_content
+
+ # Six-character salt, generated on initialization
+ attr_reader :salt
+
+ # Initialize the LiquidEncoder. If content is provided, will automatically
+ # parse content and populate encoded_content. Otherwise, will eagerly await
+ # content passed via the +encode+ method.
+ #
+ # content - a String to be encoded (default: nil).
+ #
+ # Returns the LiquidEncoder object
+ def initialize(content=nil)
+ @salt = Digest::MD5.hexdigest(Time.now.nsec.to_s)[0..5]
+ @liquid_hash = {}
+ @reserved_keys = []
+ @current_key = 0
+
+ if content
+ encode(content)
+ end
+ end
+
+ # Encode a string. Takes an optional second argument, +overwrite+, which specifies
+ # whether previous encodings should be overwritten. If +overwrite+ is false and this
+ # object has already encoded a string (and populated its tag_hash), it will throw a
+ # RuntimeException.
+ #
+ # content - the String to be encoded.
+ # overwrite - whether to overwrite previous content (default: false)
+ #
+ # Returns the encoded string, which can also be accessed via encoded_content
+ def encode(content, overwrite=false)
+ if @encoded_content
+ if overwrite
+ @encoded_content = nil
+ @liquid_hash = {}
+ @current_key = 0
+ else
+ raise RuntimeError, "LiquidEncoder#encode called with overwrite set to false, but I have already encoded a string."
+ end
+ end
+
+ # Encode data
+ in_raw_block = false
+ @encoded_content = content.gsub(/(\{\{.*?\}\})|(\{%.*?%\})/) do |m|
+ # {% endraw %} means we stop the raw block. This tag should be encoded
+ if m =~ /\{%\s*endraw\s*%\}/
+ in_raw_block = false
+ end
+
+ gsubbed_string = if in_raw_block
+ m
+ else
+ key = next_available_key
+ @liquid_hash[key] = m
+ encoded_tag(key)
+ end
+
+ # {% raw %} means we start a raw block. This tag will have been encoded
+ if m =~ /\{%\s*raw\s*%\}/
+ in_raw_block = true
+ end
+
+ gsubbed_string
+ end
+ end
+
+ # Decode a string. Finds and replaces any liquid html tags with their original liquid content.
+ #
+ # content - the String to be decoded.
+ #
+ # Returns the decoded string.
+ def decode(content)
+ content.gsub(decoder_regexp) do |m|
+ key = $1.to_i
+ if @liquid_hash.has_key? key
+ @liquid_hash[key]
+ else
+ m
+ end
+ end
+ end
+
+ # The LiquidEncoder tag for a given key
+ #
+ # key - the key for this tag
+ #
+ # Returns the liquid tag
+ def encoded_tag(key)
+ "<!--LIQUID-#{salt}-#{key}-->"
+ end
+
+ # A regexp to match all liquid keys for this encoder.
+ def decoder_regexp
+ @decoder_regexp ||= /<!--LIQUID-#{salt}-(\d+)-->/
+ end
+
+ # Finds any reserved keys (i.e. instances of the liquid encoding key) in a given content.
+ #
+ # content - the String to be scanned
+ #
+ # Returns an array of reserved keys
+ def reserved_keys_for(content)
+ content.scan(decoder_regexp).map{ |m| m[0] }
+ end
+
+ private
+ # Returns the next available key. Keys start at 0 and increment.
+ #
+ # Returns the next key.
+ def next_available_key
+ k = @current_key
+ @current_key += 1
+ k
+ end
+ end
+end
View
27 lib/jekyll/tags/include.rb
@@ -7,7 +7,8 @@ def initialize(tag_name, file, tokens)
end
def render(context)
- includes_dir = File.join(context.registers[:site].source, '_includes')
+ @site = context.registers[:site]
+ includes_dir = File.join(@site.source, '_includes')
if File.symlink?(includes_dir)
return "Includes directory '#{includes_dir}' cannot be a symlink"
@@ -20,8 +21,11 @@ def render(context)
Dir.chdir(includes_dir) do
choices = Dir['**/*'].reject { |x| File.symlink?(x) }
if choices.include?(@file)
- source = File.read(@file)
+
+ source = File.read(@file)
+ source = convert_source(source)
partial = Liquid::Template.parse(source)
+
context.stack do
partial.render(context)
end
@@ -30,6 +34,25 @@ def render(context)
end
end
end
+
+ # Converts the passed text using the first available converter that
+ # matches its extension
+ #
+ # source - the source text to convert
+ #
+ # Returns the converted text
+ def convert_source(source)
+ extension = File.extname(@file)
+ converter = @site.converters.find{ |c| c.matches(extension) }
+
+ unless converter.is_a?(Jekyll::Converters::Identity)
+ encoder = Jekyll::LiquidEncoder.new(source)
+ source = converter.convert(encoder.encoded_content)
+ source = encoder.decode(source)
+ end
+ puts "Reached end" if $debug
+ source
+ end
end
end
end
View
2 site/_posts/2012-07-01-plugins.md
@@ -168,6 +168,8 @@ run on the page.</p>
In our example, UpcaseConverter-matches checks if our filename extension is `.upcase`, and will render using the converter if it is. It will call UpcaseConverter-convert to process the content - in our simple converter we’re simply capitalizing the entire content string. Finally, when it saves the page, it will do so with the `.html` extension.
+Note that converters run on your file *before* liquid tags are evaluated.
+
## Tags
If you’d like to include custom liquid tags in your site, you can do so
View
2 site/_posts/2012-07-01-structure.md
@@ -48,7 +48,7 @@ An overview of what each of these does:
<p><code>_includes</code></p>
</td>
<td>
- <p>These are the partials that can be mixed and matched by your _layouts and _posts to facilitate reuse. The liquid tag <code>{{ "{% include file.ext " }}%}</code> can be used to include the partial in <code>_includes/file.ext</code>.</p>
+ <p>These are the partials that can be mixed and matched by your _layouts and _posts to facilitate reuse. The liquid tag <code>{{ "{% include file.ext " }}%}</code> can be used to include the partial in <code>_includes/file.ext</code>. Note that included files will be run through any relevant [converters](/2012-07-01-plugins.html).</p>
</td>
</tr>
<tr>
View
59 test/test_liquid_encoder.rb
@@ -0,0 +1,59 @@
+require 'helper'
+
+class TestLiquidEncoder < Test::Unit::TestCase
+ context "LiquidEncoder" do
+ context "#initialize" do
+
+ should "give an empty LiquidEncoder without a string" do
+ encoder = Jekyll::LiquidEncoder.new
+ assert_equal(encoder.encoded_content, nil)
+ assert_equal(encoder.salt.length, 6)
+ end
+
+ should "give a populated LiquidEncoder with a string" do
+ encoder = Jekyll::LiquidEncoder.new('foo string')
+ assert_not_equal(encoder.encoded_content, nil)
+ end
+ end
+
+ context "#encode" do
+ should "raise an error if asked to encode a string when it's already encoded a string" do
+ encoder = Jekyll::LiquidEncoder.new('foo string')
+ assert_raise(RuntimeError){ encoder.encode 'bar string' }
+ end
+
+ should "overwrite old string if second argument is +true+" do
+ encoder = Jekyll::LiquidEncoder.new('foo string')
+ old_string = encoder.encoded_content
+ new_string = encoder.encode 'bar string', true
+ assert_not_equal(old_string, new_string)
+ end
+
+ should "replace liquid tags with HTML" do
+ encoder = Jekyll::LiquidEncoder.new('This is a {{sample}} string with {%liquid%} content.')
+ assert_nil(encoder.encoded_content.index('{{'))
+ assert_nil(encoder.encoded_content.index('{%'))
+ assert_match(encoder.decoder_regexp,encoder.encoded_content)
+ end
+
+ should "keep liquid tags within raw tags" do
+ encoder = Jekyll::LiquidEncoder.new('This is a {%raw%}test of {{raw}} content{%endraw%} - {{end}}.')
+ str = encoder.encoded_content
+
+ assert !str.include?('{{end}}')
+ assert !str.include?('{%raw%}')
+ assert !str.include?('{%endraw%}')
+ assert str.include?('{{raw}}')
+ end
+ end
+
+ context "#decode" do
+
+ should "decode correctly" do
+ encoder = Jekyll::LiquidEncoder.new('Sample {{text}}')
+ decoded_string = encoder.decode(encoder.encoded_content*3)
+ assert_equal('Sample {{text}}Sample {{text}}Sample {{text}}', decoded_string)
+ end
+ end
+ end
+end
View
2 test/test_post.rb
@@ -468,7 +468,7 @@ def do_render(post)
post.site.source = File.join(File.dirname(__FILE__), 'source')
do_render(post)
- assert_equal "<<< <hr />\n<p>Tom Preston-Werner github.com/mojombo</p>\n\n<p>This <em>is</em> cool</p> >>>", post.output
+ assert_equal "<<< <hr />\n<p>Tom Preston-Werner github.com/mojombo</p>\n<p>This <em>is</em> cool</p> >>>", post.output
end
should "render date specified in front matter properly" do
View
41 test/test_tags.rb
@@ -277,4 +277,45 @@ def fill_post(code, override = {})
end
end
end
+
+ context "{% include %}" do
+
+ setup do
+ @tag = Jekyll::Tags::IncludeTag.new("include", "sample.md", nil)
+ end
+
+ def site_with_converter c
+ stub!.converters{ [c] }.subject
+ end
+
+ context "when including a file with a recognised extension" do
+ setup do
+ @sample_converter = mock!.matches(".md"){ true }.subject
+ mock(@sample_converter).is_a?(Jekyll::Converters::Identity){ false }
+ mock(@sample_converter).convert('Sample text'){ 'Converted' }
+ end
+
+ should "try to convert the included file" do
+ @tag.instance_variable_set("@site", site_with_converter(@sample_converter))
+
+ converted_source = @tag.convert_source('Sample text')
+ assert_equal('Converted', converted_source)
+ end
+ end
+
+ context "when including a file without a recognised extension" do
+ setup do
+ @sample_converter = Jekyll::Converters::Identity.new
+ end
+
+ should "not try to convert the included file" do
+ mock(@sample_converter).convert.never
+
+ @tag.instance_variable_set("@site", site_with_converter(@sample_converter))
+
+ converted_source = @tag.convert_source('Sample text')
+ assert_equal('Sample text', converted_source)
+ end
+ end
+ end
end