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 closed or unclosed:

{% mytag [args]* %}

{% 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 part is accomplished like this:
    String result = "";             
    // override the place where the text goes so that we can capture it
    renderingVisitor.PushTextAccumulator(str => result += str);
    new LiquidASTRenderer().StartVisiting(renderingVisitor, liquidBlock);
    // reset the capturing to whatever happened before.
    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.

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);
  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 pass it to a LiquidASTRenderer to get the final rendered content:

    new LiquidASTRenderer().StartVisiting(renderingVisitor, liquidBlock);
  1. The args is a list of IExpressionConstants

A Custom Block Example

public class WordReverserBlockTag : ICustomBlockTagRenderer
{
    public StringValue Render(
        RenderingVisitor renderingVisitor,
        ITemplateContext templatecontext,
        TreeNode<IASTNode> liquidBlock,
        IList<Option<IExpressionConstant>> args)
    {
        var result = EvalLiquidBlock(renderingVisitor, liquidBlock);

        var words = result.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

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

    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