diff --git a/lib/jekyll/tags/include.rb b/lib/jekyll/tags/include.rb index f90f8e8a..724b33c0 100644 --- a/lib/jekyll/tags/include.rb +++ b/lib/jekyll/tags/include.rb @@ -18,8 +18,6 @@ module Jekyll VALID_SYNTAX = /([\w-]+)\s*=\s*(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|([\w\.-]+))/ VARIABLE_SYNTAX = /(?[^{]*\{\{\s*(?[\w\-\.]+)\s*(\|.*)?\}\}[^\s}]*)(?.*)/ - INCLUDES_DIR = '_includes' - def initialize(tag_name, markup, tokens) super matched = markup.strip.match(VARIABLE_SYNTAX) @@ -96,8 +94,12 @@ eos end end + def includes_dir + '_includes' + end + def render(context) - dir = File.join(File.realpath(context.registers[:site].source), INCLUDES_DIR) + dir = resolved_includes_dir(context) file = render_variable(context) || @file validate_file_name(file) @@ -113,10 +115,14 @@ eos partial.render!(context) end rescue => e - raise IncludeTagError.new e.message, File.join(INCLUDES_DIR, @file) + raise IncludeTagError.new e.message, File.join(includes_dir, @file) end end + def resolved_includes_dir(context) + File.join(File.realpath(context.registers[:site].source), includes_dir) + end + def validate_path(path, dir, safe) if safe && !realpath_prefixed_with?(path, dir) raise IOError.new "The included file '#{path}' should exist and should not be a symlink" @@ -126,7 +132,7 @@ eos end def path_relative_to_source(dir, path) - File.join(INCLUDES_DIR, path.sub(Regexp.new("^#{dir}"), "")) + File.join(includes_dir, path.sub(Regexp.new("^#{dir}"), "")) end def realpath_prefixed_with?(path, dir) @@ -138,7 +144,19 @@ eos File.read(file, file_read_opts(context)) end end + + class IncludeRelativeTag < IncludeTag + def includes_dir + '.' + end + + def resolved_includes_dir(context) + page_path = context.registers[:page].nil? ? includes_dir : File.dirname(context.registers[:page]["path"]) + Jekyll.sanitized_path(context.registers[:site].source, page_path) + end + end end end Liquid::Template.register_tag('include', Jekyll::Tags::IncludeTag) +Liquid::Template.register_tag('include_relative', Jekyll::Tags::IncludeRelativeTag) diff --git a/site/_docs/templates.md b/site/_docs/templates.md index 84655db8..580d3724 100644 --- a/site/_docs/templates.md +++ b/site/_docs/templates.md @@ -292,6 +292,17 @@ These parameters are available via Liquid in the include: {% raw %}{{ include.param }}{% endraw %} {% endhighlight %} +### Including files relative to another file + +You can also choose to include files relative to the current file: + +{% highlight ruby %} +{% raw %}{% include_relative somedir/footer.html %}{% endraw %} +{% endhighlight %} + +You won't need to place your included content within the `_includes` directory. +All the other capaibilities of the `include` tag are available to the `include_relative` tag. + ### Code snippet highlighting Jekyll has built in support for syntax highlighting of [over 100 diff --git a/test/source/_posts/2014-09-02-relative-includes.markdown b/test/source/_posts/2014-09-02-relative-includes.markdown new file mode 100644 index 00000000..ab451b63 --- /dev/null +++ b/test/source/_posts/2014-09-02-relative-includes.markdown @@ -0,0 +1,29 @@ +--- +title: Post +layout: post +include1: rel_include.html +include2: include_relative/rel_include +include3: rel_INCLUDE +include4: params +include5: clude +--- + +Liquid tests +- 1 {% include_relative include_relative/{{ page.include1 }} %} +- 2 {% include_relative {{ page.include2 | append: '.html' }} %} +- 3 {% include_relative include_relative/{{ page.include3 | downcase | append: '.html' }} %} + +Whitespace tests +- 4 {% include_relative include_relative/{{page.include1}} %} +- 5 {% include_relative include_relative/{{ page.include1}} %} +- 6 {% include_relative include_relative/{{ page.include3 | downcase | append: '.html'}} %} + +Parameters test +- 7 {% include_relative include_relative/{{ page.include4 | append: '.html' }} var1='foo' var2='bar' %} + +Partial variable test +- 8 {% include_relative include_relative/rel_in{{ page.include5 }}.html %} + +Relative to self test: + +- 9 {% include_relative 2014-03-03-yaml-with-dots.md %} diff --git a/test/source/_posts/include_relative/params.html b/test/source/_posts/include_relative/params.html new file mode 100644 index 00000000..ae4c6cfa --- /dev/null +++ b/test/source/_posts/include_relative/params.html @@ -0,0 +1,7 @@ +{{include.param}} + +
    + {% for param in include %} +
  • {{param[0]}} = {{param[1]}}
  • + {% endfor %} +
diff --git a/test/source/_posts/include_relative/rel_include.html b/test/source/_posts/include_relative/rel_include.html new file mode 100644 index 00000000..5fb677b5 --- /dev/null +++ b/test/source/_posts/include_relative/rel_include.html @@ -0,0 +1 @@ +relative_included diff --git a/test/test_generated_site.rb b/test/test_generated_site.rb index ca3124c5..37818726 100644 --- a/test/test_generated_site.rb +++ b/test/test_generated_site.rb @@ -14,7 +14,7 @@ class TestGeneratedSite < Test::Unit::TestCase end should "ensure post count is as expected" do - assert_equal 42, @site.posts.size + assert_equal 43, @site.posts.size end should "insert site.posts into the index" do diff --git a/test/test_site.rb b/test/test_site.rb index 97111971..7c42862a 100644 --- a/test/test_site.rb +++ b/test/test_site.rb @@ -48,7 +48,7 @@ class TestSite < Test::Unit::TestCase Jekyll::Configuration::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir}) end @site = Site.new(Jekyll.configuration) - @num_invalid_posts = 2 + @num_invalid_posts = 4 end should "have an empty tag hash by default" do diff --git a/test/test_tags.rb b/test/test_tags.rb index 12402c7f..756bdcb5 100644 --- a/test/test_tags.rb +++ b/test/test_tags.rb @@ -510,4 +510,98 @@ CONTENT end end end + + context "relative include tag with variable and liquid filters" do + setup do + stub(Jekyll).configuration do + site_configuration({'pygments' => true}) + end + + site = Site.new(Jekyll.configuration) + post = Post.new(site, source_dir, '', "2014-09-02-relative-includes.markdown") + layouts = { "default" => Layout.new(site, source_dir('_layouts'), "simple.html")} + post.render(layouts, {"site" => {"posts" => []}}) + @content = post.content + end + + should "include file as variable with liquid filters" do + assert_match %r{1 relative_include}, @content + assert_match %r{2 relative_include}, @content + assert_match %r{3 relative_include}, @content + end + + should "include file as variable and liquid filters with arbitrary whitespace" do + assert_match %r{4 relative_include}, @content + assert_match %r{5 relative_include}, @content + assert_match %r{6 relative_include}, @content + end + + should "include file as variable and filters with additional parameters" do + assert_match '
  • var1 = foo
  • ', @content + assert_match '
  • var2 = bar
  • ', @content + end + + should "include file as partial variable" do + assert_match %r{8 relative_include}, @content + end + + should "include files relative to self" do + assert_match %r{9 —\ntitle: Test Post Where YAML}, @content + end + + context "trying to do bad stuff" do + context "include missing file" do + setup do + @content = < 'pretty', 'source' => source_dir, 'destination' => dest_dir, 'read_posts' => true}) + end + assert_equal 'Included file \'./missing.html\' not found', exception.message + end + end + end + + context "with symlink'd include" do + + should "not allow symlink includes" do + File.open("/tmp/pages-test", 'w') { |file| file.write("SYMLINK TEST") } + assert_raise IOError do + content = < 'pretty', 'source' => source_dir, 'destination' => dest_dir, 'read_posts' => true, 'safe' => true }) + end + assert_no_match /SYMLINK TEST/, @result + end + + should "not expose the existence of symlinked files" do + ex = assert_raise IOError do + content = < 'pretty', 'source' => source_dir, 'destination' => dest_dir, 'read_posts' => true, 'safe' => true }) + end + assert_match /should exist and should not be a symlink/, ex.message + end + end + end end