CARVIEW |
Navigation Menu
-
Notifications
You must be signed in to change notification settings - Fork 15
How to Write a Filter
Updated for version 0.9.6
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 your filter sees them.
{{ echo_args: 1, 3.0, "HELLO", myvar}}
We can create a simple upcase
filter like this:
public class MyUpCaseFilter : FilterExpression<LiquidString, LiquidString>
{
public override LiquidExpressionResult ApplyTo(ITemplateContext ctx, LiquidString liquidExpression)
{
return LiquidExpressionResult.Success(liquidExpression.ToString().ToUpper());
}
}
The declaration FilterExpression<LiquidString, LiquidString>
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.
This filter's logic is implemented by overriding the ApplyTo
method that takes a LiquidString
as an argument. It accesses the LiquidString
's underlying String by calling called .ToString()
, though it could have called LiquidString.StrVal
which has the same effect.
When writing a filter, a developer doesn't have to worry about receiving a value with a non-String type, e.g. casting a LiquidNumeric
to a string or how to render a LiquidNumeric
, because the incoming value will be cast as a LiquidString
before the filter receives it (if possible). There is no need to do a null check either—the filter will never receive a null
argument in ApplyTo
---more on this later.
Finally, the filter needs to wrap the result of ApplyTo
in a LiquidExpressionResult
. LiquidExpressionResult
has some static helper methods that help with the wrapper classes. For now, we're returning a successful LiquidString
result, though in reality the return value is slightly more complex.
Some liquid filters can handle more than one type of input. You can do simple polymorphism over the standard ILiquidValue
-derived types by overriding the appropriate ApplyTo
methods, and providing a fallback method for any types not explicitly handled.
To write a filter, you need write a class that has:
- one or more
ApplyTo
methods 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
ILiquidValue
s.
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 ILiquidValue
and returning an ILiquidValue
constant---this will allow you to accept any type and return any type:
// this parameterized type says we'll accept any kind of ILiquidValue and return any
// kind of ILiquidValue
public class MyFilter : FilterExpression<ILiquidValue, ILiquidValue>
{
public override LiquidExpressionResult ApplyTo(ITemplateContext ctx, LiquidString str)
{
// handle a string value
}
public override LiquidExpressionResult ApplyTo(ITemplateContext ctx, LiquidNumeric num)
{
// handle a numeric value
}
public override LiquidExpressionResult ApplyTo(ITemplateContext ctx, ILiquidValue expr)
{
// fallback to handle an ILiquidValue 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 LiquidNumeric
s and LiquidString
s, 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 ILiquidValue
, 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:
// accept a string and return a number
public class CountChars : FilterExpression<LiquidString, LiquidNumeric>
{
public override LiquidExpressionResult ApplyTo(ITemplateContext ctx, LiquidString 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 LiquidString
and a LiquidNumeric
:
public MyFilter(LiquidString str, ILiquidvalue expr)
{
// when rendering the filter {{ abc | myfilter 1, 2 }}, this will receive
// str == LiquidString.Create("1") and expr == LiquidNumeric.Create(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 is an abstract class that 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 implemented this way to make it easier to handle the ad-hoc logic in ruby Liquid when it comes to how values are types are handled, without having to do a lot of parsing, type-checking, casting, and null handling.
By default, FilterExpression
handles null
for you by passing it along to the next filter, but you could intercept it and return something different. If you need to return something else, 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.