Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support parameters for liquid include tags [take 2] #1204

Merged
merged 18 commits into from Jul 9, 2013

Conversation

@maul-esel
Copy link

@maul-esel maul-esel commented Jun 11, 2013

This is a resubmission of #876 due to the heavy changes that have been made to master since it was submitted.

This PR adds the ability to send parameters to includes:

{% include foo.md bar="baz" %}

It can be used like this:

# in _includes/foo.md
{{ include.bar }} # outputs "baz" (without the quotes)
@parkr
Copy link
Member

@parkr parkr commented Jun 11, 2013

Thank you!! Sorry to ask yet another thing of you but would you mind writing a quick comment outlining exactly how this works from a user standpoint?

Scenario: Include a file with parameters
Given I have an _includes directory
And I have an "_includes/header.html" file that contains "<header>My awesome blog header: {{include.param}}</header>"
And I have an "_includes/params.html" file that contains "Parameters:<ul>{% for param in include %}<li>{{param[0]}} = {{param[1]}}</li>{% endfor %}</ul>"

This comment has been minimized.

@parkr

parkr Jun 11, 2013
Member

What if we made it so that we had this:

{% for param in include %}
{{ param.key }} = {{ param.value }}
{% endfor %}

I kind of like named accessors more than index accessors.

This comment has been minimized.

This comment has been minimized.

@parkr

parkr Jun 11, 2013
Member

Ok cool :) Thanks!

@maul-esel
Copy link
Author

@maul-esel maul-esel commented Jun 11, 2013

You can write your {% include %} tags like before, so all old things should still work.

But if, for a particular include, you want some extra variables available depending on where they are included from, you add parameters to the tag, like {% include param='value' %}.

Inside the include, you can then access these from the liquid variable include. You can for example use this for navigations (pass the "current" section and mark it) or conditional styling (pass the CSS class to use in the include) etc.

eos
end

while pos = markup.index(/[=\"\s]/, pos)

This comment has been minimized.

@parkr

parkr Jun 11, 2013
Member

Why not use regexp for this?

http://rubular.com/r/FbVfmDUIU6

This comment has been minimized.

@maul-esel

maul-esel Jun 12, 2013
Author

😲 How did you come up with this? I tried, but couldn't think of any regex that does the job.

This comment has been minimized.

@maul-esel

maul-esel Jun 12, 2013
Author

One thing though: Your regex supports both single and double quote, but a double quote can terminate a string started with single quote. This leads to problems with code like the following:

param='and" dg=' abc="d"

With your current regex, the dg=' portion is leftover, though I'd expect it to be part of param.

Do you have a fix for this, god of regex? Or drop single-quote support?

This comment has been minimized.

@parkr

parkr Jun 12, 2013
Member

What do you think of this?

http://rubular.com/r/6P8aGivX9g

This comment has been minimized.

@maul-esel

maul-esel Jun 12, 2013
Author

That seems to fail on your other test data - it reads everything as one param.

This comment has been minimized.

@parkr

parkr Jun 12, 2013
Member

@paulmsmith You seem pretty excited about this. Can you come up with a use-case for a param whose value includes the = character? I can't think of anything.

This comment has been minimized.

@paulmsmith

paulmsmith Jun 12, 2013

If I'm understanding you chaps correctly, then a use-case might just be a string of copy text. For example I could want to do:

{% include headline='Jekyll include parameters are here' strapline='Using them = EPIC WIN!' %} 

and from within the include:

<div class="somemodule">
<h1>{{ include.headline }}</h1>
<p>{{ include.strapline }}</p>
</div>

I'd expect that to output:

<div class="somemodule">
<h1>Jekyll include parameters are here</h1>
<p>Using them = EPIC WIN!</p>
</div>

Hope that makes sense.

This comment has been minimized.

@maul-esel

maul-esel Jun 12, 2013
Author

That's the only thing I could think about as well. On the other side, most cases like that can be solved by using an HTML entity for the equal sign - we'd just need to document it.

This comment has been minimized.

@parkr

parkr Jun 12, 2013
Member

So, guess what! @imathis (the true Regexp god) fixed everything: http://rubular.com/r/kmNhn2806o

This comment has been minimized.

@maul-esel

maul-esel Jun 13, 2013
Author

Impressive! And indeed, the regex shortens the code immensely - definitly worth it.


{% highlight ruby %}
{{ "{% include sig.textile param=value " }}%}
{% endhighlight %}

This comment has been minimized.

@parkr

parkr Jun 11, 2013
Member

Let's use {% raw %} blocks in here instead of that weird {{ "{% syntax.

This comment has been minimized.

@paulmsmith
Copy link

@paulmsmith paulmsmith commented Jun 12, 2013

You guys are awesome!

maul.esel added 4 commits Jun 12, 2013
maul.esel
This also introduces single-quote support.
maul.esel
pos = 0

# ensure the entire markup string from start to end is valid syntax, and params are separated by spaces
full_matcher = Regexp.compile('\A\s*(?:(?<=\s|\A)' + MATCHER.to_s + '\s*)*\z')

This comment has been minimized.

@maul-esel

maul-esel Jun 14, 2013
Author

I used this to make sure the entire markup is valid, not only portions of it. However, 1.8.7 doesn't like look-behind (?<=...) - any advice?

This comment has been minimized.

@parkr

parkr Jun 15, 2013
Member

Hm... not sure. What is the look-behind necessary for?

This comment has been minimized.

@maul-esel

maul-esel Jun 15, 2013
Author

To make sure there's a space between the individual params. I could solve this issue by using a look-ahead at the end of the pattern instead.

@maul-esel
Copy link
Author

@maul-esel maul-esel commented Jun 15, 2013

OK, the tests finally pass. Anything else left to do?

@imathis

This comment has been minimized.

Copy link

@imathis imathis commented on lib/jekyll/tags/include.rb in 0fa03d2 Jun 15, 2013

What if we used this regex instead. ([\w-]+)\s*=\s*(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|([\w\.-]+)) that would add a few new options.

  1. Supports spaces around the equals sign
  2. Supports variable names with a dash (like YAML does).
  3. Supports existing variables in match[4](see next comment)
@imathis

This comment has been minimized.

Copy link

@imathis imathis commented on lib/jekyll/tags/include.rb in 0fa03d2 Jun 15, 2013

If you set @markup = markup then you can move this regex parsing into the render method. Then using context, you can assign include variables using values from variables in the context. Using the new regex I commented about above, you'll have match[4] which will be an unquoted variable name derived from a match on something like foo=bar.baz. Then you can do the following:

elsif match[4]
  value = context[match[4]]
end

This will allow you to do something like this:

{% for post in site.posts reverse %}
  {% capture post_title %}{{ post.title | titlecase }}{% endcapture %}
  {% include post.html title=post_title %}
{% endfor %}

Then you can reference {{ include.title }} in your post template partial. By allowing variables as well as strings, template partials have to rely less on conditionals and global variables and aren't restricted to static strings like the current proposal would suggest.

@imathis
Copy link
Contributor

@imathis imathis commented Jun 15, 2013

Oh, if you want to see this new regex in action, here's the rubular.

@maul-esel
Copy link
Author

@maul-esel maul-esel commented Jun 19, 2013

Very interesting, thanks. I considered supporting variables, just didn't know how to. Unless @parkr or @mattr- have a different opinion, I'm happy to include it.

end
end

MATCHER = /(\w+)=(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)')/

This comment has been minimized.

@mattr-

mattr- Jun 22, 2013
Member

I'd prefer if the constant was at the top but I'm willing to be flexible.

@parkr do you have a preference here?

This comment has been minimized.

@parkr

parkr Jun 22, 2013
Member

I also prefer the top if that's ok :)

@mattr-
Copy link
Member

@mattr- mattr- commented Jun 22, 2013

I'm cool with supporting variables.

@parkr @mojombo Anything you're aware of that keeps us from supporting variables as in @imathis' example?

@parkr
Copy link
Member

@parkr parkr commented Jun 22, 2013

@mattr- @mojombo I think supporting extraction of variable values if the string input is a variable is a good idea.

@imathis also suggested we try something more direct (albeit Rails-like) with something like this:

{% include post.html locals: { author_class: "blue icon-notice", title: "Herein Lies the Story" } %}

So we'd take everything after .extname and parse it as JSON. It definitely takes more work in terms of styling, but also offers hierarchies and such. Might very well be too complicated for any solution Jekyll should hold in its core, but it's a pretty neat suggestion nevertheless. I'd say go with our current implementation in order to be more designer-friendly.

@bcomnes
Copy link
Contributor

@bcomnes bcomnes commented Jun 30, 2013

This seems similar to the extended include tag in shopify: {% include 'foo' with 'bar' %}

I can't seem to get that to work in Jekyll though, and this PR looks more useful. 👍 Can't wait to get my hands on it.

@paulmsmith
Copy link

@paulmsmith paulmsmith commented Jul 4, 2013

Sorry to push. What's the latest on this @imathis @parkr ? Going to be in Jekyll one day soon?

end

while match = MATCHER.match(markup) do
markup = markup[match.end(0)..-1]

This comment has been minimized.

@parkr

parkr Jul 4, 2013
Member

in practice, i think this will only ever execute once. @imathis's pattern above seems to encompass the entire call, no?

This comment has been minimized.

@maul-esel

maul-esel Jul 4, 2013
Author

No. If you look at the rubular he linked, there are several matches, so this should execute for each of them.

if markup.include?(' ')
separator = markup.index(' ')
@file = markup[0..separator].strip
@params = markup[separator..-1]

This comment has been minimized.

@parkr

parkr Jul 5, 2013
Member

@file, @params = markup.split(' ', 2)

should work :)

This comment has been minimized.

@parkr

parkr Jul 5, 2013
Member

and it applies to both with and without params, so both of these work:

# No params
@markup = "some_fun_thing.html"
@file, @params = markup.split(" ", 2)
@file # => "some_fun_thing.html"
@params # => nil

# With params
@markup = "some_amazing_thing.html name='henry' title='king'"
@file, @params = markup.split(" ", 2)
@file # => "some_amazing_thing.html"
@params # => "name='henry' title='king'"

# ensure the entire markup string from start to end is valid syntax, and params are separated by spaces
full_matcher = Regexp.compile('\A\s*(?:' + MATCHER.to_s + '(?=\s|\z)\s*)*\z')
if not markup =~ full_matcher

This comment has been minimized.

@parkr

parkr Jul 5, 2013
Member

unless is a bit more idiomatic here :)

@parkr
Copy link
Member

@parkr parkr commented Jul 7, 2013

@mattr-, would you please take another look at this? Looks pretty ready to me.


def parse_params(markup, context)
params = {}
pos = 0

This comment has been minimized.

@mattr-

mattr- Jul 8, 2013
Member

I think we can remove the pos variable. I'm not seeing it used anywhere.

@mattr-
Copy link
Member

@mattr- mattr- commented Jul 8, 2013

👍 from me, even with the comment above. :shipit:

params = {}
pos = 0

# ensure the entire markup string from start to end is valid syntax, and params are separated by spaces

This comment has been minimized.

@mattr-

mattr- Jul 8, 2013
Member

Looking at this again, the comment tells me that this should be in a separate method, something like below, perhaps?

def validate_syntax
  full_matcher = Regexp.compile('\A\s*(?:' + MATCHER.to_s + '(?=\s|\z)\s*)*\z')
  unless markup =~ full_matcher
    raise SyntaxError.new <<-eos
Invalid syntax for include tag:

  #{markup}

Valid syntax:

  {% include file.ext param='value' param2="value" %}

eos
  end
end

and then you can change the parse_params method to be like so:

def parse_params(markup, context)
  params = {}

  validate_syntax

  ...
end

Thoughts?

maul.esel
Remove unused variable, extract validation to method (@mattr-).
Do not require markup to be passed to parse_params as argument.
value = match[3].gsub(/\\'/, "'")
elsif match[4]
value = context[match[4]]
end

This comment has been minimized.

@parkr

parkr Jul 8, 2013
Member

what do you think about having this block return the value and setting value outside the block?

maul.esel
@maul-esel
Copy link
Author

@maul-esel maul-esel commented Jul 8, 2013

Despite the build failure, this actually runs fine.

parkr added a commit that referenced this pull request Jul 9, 2013
Support parameters for liquid include tags.
@parkr parkr merged commit 08f6f3c into jekyll:master Jul 9, 2013
1 check passed
1 check passed
default The Travis CI build passed
Details
parkr added a commit that referenced this pull request Jul 9, 2013
@parkr
Copy link
Member

@parkr parkr commented Jul 9, 2013

MERGED!! Thanks so much for your hard work and patience with us on this @maul-esel and props to @imathis for crazy amazing Regexp-fu.

@mattr-
Copy link
Member

@mattr- mattr- commented Jul 9, 2013

🎉 🎆

@maul-esel maul-esel deleted the maul-esel:include-params2 branch Jul 10, 2013
@paulmsmith
Copy link

@paulmsmith paulmsmith commented Jul 10, 2013

Awesome!! Thanks for all the work @maul-esel, @parkr, @imathis and chums! Looking forward to using it!!

@mattr-
Copy link
Member

@mattr- mattr- commented Jul 10, 2013

@maul-esel Great work! Thanks so much for sticking with this through all the various revisions and comments.

@maul-esel
Copy link
Author

@maul-esel maul-esel commented Jul 10, 2013

Happy to help this awesome project grow and develop further. And thanks to you both for bringing it back to life

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

7 participants
You can’t perform that action at this time.