Skip to content

How to Write a Tag or Block

Mike Bridge edited this page Nov 11, 2015 · 16 revisions

Updated for v0.9.6

A tag can be either unclosed:

{% mytag [args]* %}

or closed:

{% myblock [args]* %}
    Inside block
{% endmyblock %}

If a tag has an "end" markup then it's a block.

Blocks

To create a block you need to create a class which overrides ICustomBlockTagRenderer. This has one method:

LiquidString Render(RenderingVisitor renderingVisitor,
                   ITemplateContext templateContext,
                   TreeNode<IASTNode> liquidBlock,
                   IList<Option<ILiquidValue>> args);

The arguments give you access to something that allows you to render the arguments, the current state of the rendering, the pre-rendered content, and the the tag's arguments:

  1. renderingVisitor: can render your content. Most of the time you will want to 1) evaluate the AST with the renderingVisitor and capture the result, then 2) manipulate that result and give it back to the renderer or the templateContext. The first may follow this pattern:
    String evaledBlock = "";             

    // evaluate the tree and place the text result into "evaledBlock".  Note that 
    // if we don't override the second argument `Action<String>`,
    // the renderingVisitor will put the result of parsing the liquidBlock
    // onto it's own current accumulator, meaning that the text will be written
    // directly into the current result.
    renderingVisitor.StartWalking(liquidBlock, str => evaledBlock += str);

    DoSomethingWithTheResult(evaledBlock);

Note: You could create your own RenderingVisitor and capture text with that instead of using the current RenderingVisitor, but you will run into subtle bugs when your tag's content contains side-effecting tags that modify global state such as cycle. It's recommended that you use the RenderingVisitor that's passed in instead of using your own.

Normally you will either put the result directly back into the output stream, or else you'll save the result as a value somewhere. The return value of Render(...) is what gets written out to the final result:

    return LiquidString.Create(myresult);

Or you can modify the state of the template context. For example, you could write your parsed content into the global variable uglyglobal like this:

   templateContext.SymbolTableStack.DefineGlobal("uglyglobal", myresult); // set a variable
   return new LiquidString("");  // render an empty string
  1. The templateContext:

  2. The liquidBlock: is content that appears between the tags comes in the liquidBlock. It's in the form of an AST fragment, and you can render it by calling RenderingVisitor.StartWalking:

    renderingVisitor.StartWalking(liquidBlock, myAccumulator);
  1. args is a list of the ILiquidValues arguments that were passed to the block. These have already been evaluated (in contrast to other implementations of liquid).

A Custom Block Example

public class WordReverserBlockTag : ICustomBlockTagRenderer
{
    public StringValue Render(
        RenderingVisitor renderingVisitor,
        ITemplateContext templatecontext,
        TreeNode<IASTNode> liquidBlock,
        IList<Option<ILiquidValue>> args)
    {
        // evaluate the contents of the block
        var result = EvalLiquidBlock(renderingVisitor, liquidBlock);

        // perform the tag logic in the string result and return 
        // it to be rendered.
        return Reverse(result);
    }

    private static String EvalLiquidBlock(
        RenderingVisitor renderingVisitor, 
        TreeNode<IASTNode> liquidBlock)
    {
        // walk the AST with an accumulator that shunts the output 
        // temporarily to "result".
        String result = ""; 
        renderingVisitor.StartWalking(liquidBlock, str => result += str);
        return result;
    }

    private static LiquidValue Reverse(string result)
    {
        var words = result.Split(new[] {' '}, 
            StringSplitOptions.RemoveEmptyEntries);

        return LiquidValue.Create(String.Join(" ", 
            words.Select(x => new String(x.Reverse().ToArray()))));
    }

}

Once you have a block tag, you need to register it in your template context:

var ctx = new TemplateContext()
    .WithAllFilters()
    .WithCustomTagBlockRenderer<WordReverserBlockTag>("reversewords")

Now you can try it out:

{% assign myword = 'hello world' %}
{% reversewords %}{{ myword }}{% endreversewords %}
==>
   "world hello"
Clone this wiki locally