Jimmy
Jimmy

Reputation: 9815

using Liquid variables inside of a liquid tag call

I made a custom link tag in Liquid and I am trying to be able to pass liquid variables into the call for that tag like so

{{ assign id = 'something' }} // this value is actual dynamic while looping through data 
{% link_to article: id, text: 'Click Me!' %} // my custom tag

However this results in the article parameter being passed in as 'id' instead of 'something' as per the assign statement above it.

Does anyone know how to pass variables into tag calls?

Upvotes: 22

Views: 11712

Answers (6)

depau
depau

Reputation: 464

You can just parse the variable like Liquid itself does, see for instance how the expression in assign: link.

This is a sample custom tag that evaluates the entire markup as an expression:

require 'liquid/variable'

module Jekyll
  class MyTag < Liquid::Tag
    def initialize(tag_name, markup, parse_context)
      super
      # Parse the variable
      @var = Liquid::Variable.new(markup.strip, parse_context)
    end

    def render(context)
      # Evaluate it within the usage context
      var = @var.render(context)
      "Variable: #{var}"
    end
  end
end

Liquid::Template.register_tag('mytag', Jekyll::MyTag)

You may want to use regular expressions to carve out expressions from your complex markup.

Here's a simple function that does that. Note that, since it uses regular expressions, it can't handle more complex expressions that need a context-free grammar, such as those containing multiple nested parentheses.

def parse_markup(markup, parse_context)
  params = {}
  regex = /(\w+):\s*(\([^)]+\)|"[^"]+"|[^,]+)/

  markup.scan(regex) do |param_name, expression|
    params[param_name] = Liquid::Variable.new(expression.strip, parse_context)
  end

  params
end

Here's a simple tag that makes use of it:

require 'liquid/variable'

module Jekyll
  class MyTag < Liquid::Tag
    def initialize(tag_name, markup, parse_context)
      super
      # Parse all parameters
      @params = parse_markup(markup, parse_context)
    end

    def render(context)
      # Evaluate foo and bar
      foo = @params["foo"].render(context)
      bar = @params["bar"].render(context)
      
      "foo: #{foo}, bar: #{bar}"
    end
  end
end

Liquid::Template.register_tag('mytag', Jekyll::MyTag)

Upvotes: 0

Martin Puppe
Martin Puppe

Reputation: 34

This does not strictly answer the question, but it may help others who are new to Liquid (like myself) and try something like this. Instead of implementing a custom tag, consider implementing a custom filter instead. Variables are resolved before they are passed into filters.

Ruby code:

module MyFilters
  def link_to_article(input, text)
    "<a href='https://example.org/article/#{input}'>#{text}</a>"
  end
end

Liquid::Template.register_filter(MyFilters)

Liquid template:

{% assign id = 'something' %} 
{{ id | link_to_article: 'Click Me!' }}

Output:

<a href='https://example.org/article/something'>Click Me!</a>

You can also use variables as parameters. So the following would have the same output:

{% assign id = 'something' %} 
{% assign text = 'Click Me!' %}
{{ id | link_to_article: text }}

And filters can have zero or more (comma-separated) parameters:

{{ 'input' | filter_with_zero_parameters }}
{{ 'input' | filter_with_two_parameters: 'parameter 1', 'parameter 2' }}

Upvotes: 0

DimitriVdP
DimitriVdP

Reputation: 31

It would be great to have a tag that can be called with literals and variables like

{% assign v = 'art' %}
{% link_to_article v %}

or

{% link_to_article 'art' %}

or

{% link_to_article "art" %}

and also of course

{% link_to_article include.article %}

In order to so I propose a helper function

def get_value(context, expression)
  if (expression[0]=='"' and expression[-1]=='"') or (expression[0]=="'" and expression[-1]=="'")
    # it is a literal
    return expression[1..-2]
  else
    # it is a variable
    lookup_path = expression.split('.')
    result = context
    puts lookup_path
    lookup_path.each do |variable|
      result = result[variable] if result
    end
    return result
  end
end

And in the render just call the helper function to get the value of the literal or variable.

def render(context)
  v = get_value(context, @markup.strip)
end

FYI, the initialiser would look like this:

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

Upvotes: 3

This solved the case for me context[@markup.strip].

My problem was that i wanted to be able to pass a variable to my custom Liquid tag like this: {% get_menu main_menu navigation.html settings.theme.id %}

In order to do this i first split the variable string into different varaibles on every space character.

class GetMenu < Liquid::Tag
    include ApplicationHelper
    def initialize(tag_name, variables, tokens)

        @variables = variables.split(" ")

        @menu_object = @variables[0]
        @file_name = @variables[1]
        @theme_id = @variables[2]

        super
    end

    def render(context)

        # This is where i use context[@theme_id.strip] to get the variable of "settings.theme.id"
        content = CodeFile.find_by(hierarchy: 'snippet', name: @file_name.to_s, theme_id: context[@theme_id.strip])

        @menu ||= Menu.find_by_slug(@menu_object)

        context.merge('menu' => @menu)

        Liquid::Template.parse(content.code).render(context)

    end

end

Liquid::Template.register_tag('get_menu', GetMenu)

*This is just a more rich example that the answer above by Jonathan Julian

Upvotes: 8

Jonathan Julian
Jonathan Julian

Reputation: 12264

I've recently solved this very simply with Jekyll 0.11.2 and Liquid 2.3.0 by passing the name of the variable as the tag parameter.

{% assign v = 'art' %}
{% link_to_article v %}

You can also pass the name of the control var while in a loop, like article above.

In Liquid::Tag.initialize, @markup is the second parameter, the string following the tag name. The assigned variables are available in the top level of the context.

def render(context)
  "/#{context[@markup.strip]}/"
end

This obviously only allows one param to be passed. A more complex solution would parse params like x: 2, y: 3.

Upvotes: 11

Jimmy
Jimmy

Reputation: 9815

Doesn't look like this is possible, my solution was to just pass the variable name in to the tag and grab it out of the context the tag is being rendered in. Like so:

{% for article in category.articles %}
  {% link_to variable: article, text: title %}
{% endfor %}

in my tag code (condensed):

def render(context)
  uri = "article/#{context[@options[:variable]]['id']}"
  "<a href='#{uri}'>#{build_link_text context}</a>"
end

Upvotes: 7

Related Questions