Support parameters for liquid include tags [take 2] #1204
Conversation
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>" |
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.
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.
maul-esel
Jun 11, 2013
Author
That's just standard liquid (e.g. http://stackoverflow.com/questions/8206869/iterate-over-hashes-in-liquid-templates) for hashes.
That's just standard liquid (e.g. http://stackoverflow.com/questions/8206869/iterate-over-hashes-in-liquid-templates) for hashes.
parkr
Jun 11, 2013
Member
Ok cool :) Thanks!
Ok cool :) Thanks!
You can write your 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 Inside the include, you can then access these from the liquid variable |
eos | ||
end | ||
|
||
while pos = markup.index(/[=\"\s]/, pos) |
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.
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?
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?
maul-esel
Jun 12, 2013
Author
That seems to fail on your other test data - it reads everything as one param.
That seems to fail on your other test data - it reads everything as one param.
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.
@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.
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.
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.
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.
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.
parkr
Jun 12, 2013
Member
So, guess what! @imathis (the true Regexp god) fixed everything: http://rubular.com/r/kmNhn2806o
So, guess what! @imathis (the true Regexp god) fixed everything: http://rubular.com/r/kmNhn2806o
maul-esel
Jun 13, 2013
Author
Impressive! And indeed, the regex shortens the code immensely - definitly worth it.
Impressive! And indeed, the regex shortens the code immensely - definitly worth it.
|
||
{% highlight ruby %} | ||
{{ "{% include sig.textile param=value " }}%} | ||
{% endhighlight %} |
parkr
Jun 11, 2013
Member
Let's use {% raw %} blocks in here instead of that weird {{ "{% syntax.
Let's use {% raw %} blocks in here instead of that weird {{ "{% syntax.
maul-esel
Jun 12, 2013
Author
Done.
Done.
You guys are awesome! |
This also introduces single-quote support.
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') |
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?
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?
parkr
Jun 15, 2013
Member
Hm... not sure. What is the look-behind necessary for?
Hm... not sure. What is the look-behind necessary for?
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.
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.
OK, the tests finally pass. Anything else left to do? |
This comment has been minimized.
This comment has been minimized.
What if we used this regex instead.
|
This comment has been minimized.
This comment has been minimized.
If you set elsif match[4]
value = context[match[4]]
end This will allow you to do something like this:
Then you can reference |
Oh, if you want to see this new regex in action, here's the rubular. |
end | ||
end | ||
|
||
MATCHER = /(\w+)=(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)')/ |
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?
I'd prefer if the constant was at the top but I'm willing to be flexible.
@parkr do you have a preference here?
parkr
Jun 22, 2013
Member
I also prefer the top if that's ok :)
I also prefer the top if that's ok :)
@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 |
This seems similar to the extended include tag in shopify: I can't seem to get that to work in Jekyll though, and this PR looks more useful. |
end | ||
|
||
while match = MATCHER.match(markup) do | ||
markup = markup[match.end(0)..-1] |
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?
in practice, i think this will only ever execute once. @imathis's pattern above seems to encompass the entire call, no?
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.
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] |
parkr
Jul 5, 2013
Member
@file, @params = markup.split(' ', 2)
should work :)
@file, @params = markup.split(' ', 2)
should work :)
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'"
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 |
parkr
Jul 5, 2013
Member
unless
is a bit more idiomatic here :)
unless
is a bit more idiomatic here :)
@mattr-, would you please take another look at this? Looks pretty ready to me. |
|
||
def parse_params(markup, context) | ||
params = {} | ||
pos = 0 |
mattr-
Jul 8, 2013
Member
I think we can remove the pos
variable. I'm not seeing it used anywhere.
I think we can remove the pos
variable. I'm not seeing it used anywhere.
|
params = {} | ||
pos = 0 | ||
|
||
# ensure the entire markup string from start to end is valid syntax, and params are separated by spaces |
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?
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?
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 |
parkr
Jul 8, 2013
Member
what do you think about having this block return the value and setting value
outside the block?
what do you think about having this block return the value and setting value
outside the block?
Despite the build failure, this actually runs fine. |
Support parameters for liquid include tags.
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. |
|
Awesome!! Thanks for all the work @maul-esel, @parkr, @imathis and chums! Looking forward to using it!! |
@maul-esel Great work! Thanks so much for sticking with this through all the various revisions and comments. |
Happy to help this awesome project grow and develop further. And thanks to you both for bringing it back to life |
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:
It can be used like this: