rawinclude

In jekyll, the tag include can be used to include a parameterized template file. These parameters, however, are constrained to being supplied inline and constant, hence no captured text blocks can be passed. The custom tag rawinclude (see code below) allows for the instantiation of template files using the standard include parameters, but also passes a whole text block (without rendering liquid code in between) to the template file. Additionally, the block content is also passed as URI data encoded string which further allows for client-side download links for e.g. code snippets.

Example

In the following example, rawinclude is exploited to pass the given python code to a template called sourcefile which, in turn, highlights the syntax and provides a download link for the raw code.

{% rawinclude sourcefile
   fn="add.py" syntax="python"
   mime="plain" %}
def f(x, y):
  return x + y
{% endrawinclude %}
add.py
def f(x, y):
  return x + y

Implementation

The implementation is largely based on raw.rb to read the content in between without modification. After that necessary encoding and escaping of the passed arguments takes place and finally include is used with the block contents wrapped into in-line parameters.

As include disallows ‘%}’ to be contained in the string, unliquidify is required to be run on the contents to restore the original format.

rawinclude.rb
require 'uri'

module Jekyll
  module LiquidFilters
    def liquidify(str)
      str.gsub('%\\}', '%}')
    end

    def unliquidify(str)
      str.gsub('%}', '%\\}')
    end
  end

  class RawInclude < Liquid::Block
    include LiquidFilters
    FullTokenPossiblyInvalid = /\A(.*)#{Liquid::TagStart}\s*(\w+)\s*(.*)?#{Liquid::TagEnd}\z/om

    def initialize(tag_name, params, tokens)
      super
      @params = params
    end

    def parse(tokens)
      @body = ''
      while token = tokens.shift
        if token =~ FullTokenPossiblyInvalid
          @body << Regexp.last_match(1) if Regexp.last_match(1) != ''.freeze
          return if block_delimiter == Regexp.last_match(2)
        end
        @body << token unless token.empty?
      end

      raise Liquid::SyntaxError,
            parse_context.locale.t('errors.syntax.tag_never_closed'.freeze, block_name: block_name)
    end

    def render(context)
      uri_data = escape_quotes(URI.escape(@body.slice(1, @body.length)))
      Liquid::Template.parse("{% include #{@params}
                                 block='#{escape_quotes(unliquidify(@body))}'
                                 uri_data='#{uri_data}' %}").render(context)
    end

    def nodelist
      [@body]
    end

    def blank?
      @body.empty?
    end

    protected

    def escape_quotes(str)
      str.gsub("'", "\\\\'")
    end
  end

  Liquid::Template.register_filter(LiquidFilters)
  Liquid::Template.register_tag('rawinclude'.freeze, RawInclude)
end