-
Notifications
You must be signed in to change notification settings - Fork 15
How to Write a Filter
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}}We can create a simple upcase filter which takes an ArrayValue and a separator and returns a StringValue
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 be converted to a string.
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 does the same thing.
Note that there is no 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.
To write a filter, you need write a class that has:
- one or more
ApplyTomethods to handle the the "filter expression" that is being piped into your filter. - 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.
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
You can implement IFilterExpression<in TSource, out TResult>, or you can subclass FilterExpression<in TSource, out TResult. FilterExpression gives you help with two things:
- 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.
- 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.
If you want different logic for
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.