Skip to content

How to Write a Tag or Block

Mike Bridge edited this page Oct 2, 2015 · 16 revisions

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:

StringValue Render(RenderingVisitor renderingVisitor,
                   ITemplateContext templateContext,
                   TreeNode<IASTNode> liquidBlock,
                   IList<Option<IExpressionConstant>> 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 result = "";             

    // override the place where the text goes so that we can capture it
    renderingVisitor.PushTextAccumulator(str => result += str);
    renderingVisitor.StartWalking(liquidBlock);

    // reset the renderingVisitor's text accumulator
    renderingVisitor.PopTextAccumulator();
    DoSomethingWithTheResult(result);

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 new StringValue(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 StringValue("");  // 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.StartVisiting(liquidBlock);
  1. args is a list of the IExpressionConstants 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<IExpressionConstant>> args)
    {
        // evaluate the contents of the block
        var result = EvalLiquidBlock(renderingVisitor, liquidBlock);

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

    private static String EvalLiquidBlock(RenderingVisitor renderingVisitor, TreeNode<IASTNode> liquidBlock)
    {
        // Create an accumulator to temporarily shunt the output to our 
        String result = ""; 
        Action<String> accumulator = str => result += str; 
        renderingVisitor.PushTextAccumulator(accumulator); // temporarily shunt the output to our accumulator

        new LiquidASTRenderer().StartVisiting(renderingVisitor, liquidBlock);
        renderingVisitor.PopTextAccumulator();
        return result;
    }

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

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

}

Clone this wiki locally