Skip to content

How to Write a Filter

Mike Bridge edited this page Sep 15, 2015 · 12 revisions

A Liquid.NET filter looks something like this

   {{ [filter expression] | myfilter: [list of arguments] }}

An expression is either a single value, or the result of piping a value through a chain of filters:

  {{ "1,2,3," | rstrip: "," | split: "," | join: "-"}}
  --> "1-2-3"

In Shopify liquid, a filter has one argument, which a developer must parse herself. However, Liquid.NET allows a developer to pass comma-separated arguments, which will be evaluated before they are passed in.

   {{ echo_args: 1, 3.0, "HELLO", myvar}}

A Simple Example with No Polymorphism

We can create a simple upcase filter like this:

    public class MyUpCaseFilter : FilterExpression<StringValue, StringValue>
    {
        public override LiquidExpressionResult ApplyTo(ITemplateContext ctx, StringValue liquidExpression) 
        {
            return LiquidExpressionResult.Success(liquidExpression.ToString().ToUpper());
        }
    }

The declaration FilterExpression<StringValue, StringValue> says that you will accept a string, and return a string. Any value which is not a string will have been converted to a string before you see it---so liquid like {{ 1.0 | upcase }} will still work.

By overriding ApplyTo, you handle the incoming value from the filter expression. You can unwrap the StringValue's underlying String by calling StringValue.StrVal, or here we just called .toString(), which has the same effect.

You don't have to worry about casting a NumericValue to a string, how to render a NumericValue, or what to do with nil. This means that you do not need to do a null check. You will never receive a null argument in ApplyTo---more on this later.

Finally, you need to wrap the result of ApplyTo in a LiquidExpressionResult. LiquidExpressionResult has some static helper methods that help you with the wrapper classes. For now, we're returning a successful String result, though in reality the return value is slightly more complex than that.

A Simple Example with Polymorphism

Input & Output

To write a filter, you need write a class that has:

  1. one or more ApplyTo methods to handle the the "filter expression" that is being piped into your filter.
  2. a constructor that accepts the "list of arguments". The arguments are a set of IExpressionConstants.

Your ApplyTo method also receives the ITemplateContext object, which contains the current state of the rendering, such as the variable stack.

A filter will return a LiquidExpressionResult, which contains an "Error" or "Success" value.

Lastly, a filter needs to register the filter with the rendering engine.

Casting

You can write a polymorphic filter by accepting an IExpressionConstant and returning an IExpression constant---this will allow you to accept any type and return any type:

    // this parameterized type says we'll accept any kind of IExpressionConstant and return any
    // kind of IExpressionConstant 
    public class MyFilter : FilterExpression<IExpressionConstant , IExpressionConstant> 
    {
        public override LiquidExpressionResult ApplyTo(ITemplateContext ctx, StringValue str)
        {
           // handle a string value
        }
        public override LiquidExpressionResult ApplyTo(ITemplateContext ctx, NumericValue num)
        {
           // handle a numeric value
        }

        public override LiquidExpressionResult ApplyTo(ITemplateContext ctx, IExpressionConstant expr)
        {
           // handle an IExpressionConstant for which there is no override.
           return LiquidExpressionResult.Error("I don't know how to handle a value with type" + expr.LiquidTypeName);
        }
    }

The previous example specifies separate logic for handling NumericValues and StringValues, as well as a fallback method for handling all other values.

However, you could also make the Source and Destination types more specific. When you use more types that implement IExpressionConstant, input will be cast before you receive it. The following, for example, will convert the incoming result from the filter expression to a string before passing it to your ApplyTo function:

    public class CountChars : FilterExpression<StringValue, NumericValue> 
    {
        public override LiquidExpressionResult ApplyTo(ITemplateContext ctx, StringValue str)
        {
            // ...
        }
    }

The filter arguments are cast (or not) using almost the same logic. If you pass two integers as an argument to the following constructor, you'll receive a StringValue and a NumericValue:

    public MyFilter(StringValue str, IExpressionConstant expr)
    {
        // when rendering the filter {{ abc | myfilter 1, 2 }}, this will receive
        // str == StringValue("1") and expr == NumericValue(2).
    }

The only difference is with nil--- if you are implementing FilterExpression, your constructor may see a null value in the case where an argument evaluates to nil or is missing. But in an ApplyTo() command you will not receive a nil value

FilterExpression vs. IFilterExpression

You can implement IFilterExpression<in TSource, out TResult>, or you can subclass FilterExpression<in TSource, out TResult. FilterExpression gives you help with two things:

  1. it implements some poor-man's pattern matching so that you can implement different logic for different types without having to do a lot of type checking.
  2. it gives you some default logic for handling nil: it returns nothing.

This is just intended to handle Ruby-esque polymorphic logic without having to do a lot of type-checking or null handling.

Polymorphism in FilterExpressions

If you want different logic for

Nil in FilterExpressions

If you're writing a filter with FilterExpression and want it to do something else when it is passed nil, you can override ApplyToNil. See the [default filter implementation] (https://github.com/mikebridge/Liquid.NET/blob/master/Liquid.NET/src/Filters/DefaultFilter.cs) for an example of how this might work.

Clone this wiki locally