Custom Filters
Liquid filters are JavaScript functions. A filter function is any callable that accepts at least one argument, the result of the left hand side of a filtered expression. The function's return value will be output, assigned or piped to more filters.
You can add to, remove or modify Liquid's built-in filters to suit your needs by registering filter functions with an Environment, then rendering your templates from that environment.
All built-in filters are implemented in this way, so have a look in src/builtin/filters/ for more examples.
Add a Filter
Add a custom template filter to an Environment by calling its addFilter()
method. Here's a simple example of adding JavaScript's String.prototype.endsWith
as a filter function.
- JavaScript
- TypeScript
import { Environment } from "liquidscript";
const env = new Environment();
env.addFilter("ends_with", (val, arg) => val.endsWith(arg));
import { Environment } from "liquidscript";
const env = new Environment();
env.addFilter("ends_with", (val: string, arg: string) => val.endsWith(arg));
In a template you'd use it like this.
{% assign foo = "foobar" | ends_with: "bar" %}
{% if foo %}
<!-- do something -->
{% endif %}
Replace a Filter
If given the name of an existing filter function, Environment.add_filter()
will replace it without warning. For example, suppose you wish to replace the slice filter for one which uses start and end values instead of start and length, and is a bit more forgiving in terms of allowed inputs.
import { Environment, Markup, filters, object } from "liquidscript";
function mySlice(value, start, end) {
// Make sure the input value is an array or string.
value = object.isArray(value) ? value : object.liquidStringify(value);
// Make sure `start` is a number.
start = filters.parseNumberOrZero(start);
// End is optional
if (end === undefined) return value.slice(start);
// Make sure `end` is a number.
end = filters.parseNumberOrZero(end);
return value.slice(start, end);
}
const env = new Environment();
env.addFilter("slice", mySlice);
Remove a Filter
Remove a filter by deleting it from Environment.filters
. It's a plain object mapping filter names to filter functions.
import { Environment } from "liquidscript";
const env = new Environment();
delete env.filters.base64_decode;
Filter Context
Filter functions are applied with their this
value set to a FilterContext
, giving filters access to the current environment and render context.
This example resolves the name "handle"
in the scope of the current render context, then uses the result as part of the filter's return value. We also reference the autoEscape
option set on the active environment.
- JavaScript
- TypeScript
import { Environment, Markup } from "liquidscript";
function LinkToTag(label, tag) {
const handle = this.context.resolveSync("handle");
const result = `<a title="Show tag ${tag}" href="/collections/${handle}/${tag}">${label}</a>`;
return this.context.environment.autoEscape ? new Markup(result) : result;
}
const env = new Environment();
env.addFilter("link_to_tag", LinkToTag);
import { Environment, Markup, FilterContext } from "liquidscript";
function LinkToTag(this: FilterContext, label: string, tag: string): string {
const handle = this.context.resolveSync("handle");
const result = `<a title="Show tag ${tag}" href="/collections/${handle}/${tag}">${label}</a>`;
return this.context.environment.autoEscape ? new Markup(result) : result;
}
const env = new Environment();
env.addFilter("link_to_tag", LinkToTag);
Keyword Arguments and Options
The FilterContext
also includes any keyword arguments passed to the filter. These are available as this.options
. The default
filter is the only built-in filter to use a keyword argument. For example, {{ user.name | default: 'anonymous', allow_false: false }}
In Liquid, keyword arguments can appear in any order, even before and inbetween positional arguments. It is because of this, and the desire to allow filters with rest parameters, that LiquidScript puts options in the filter context object instead of the last argument of the filter function.
Liquid Numbers and Arithmetic
Unlike JavaScript, Liquid has distinct integer and float number types. To maintain compatibility with the reference implementation of Liquid, LiquidScript defines an Integer
type, a Float
type and utility functions for converting to these types.
Both Integer
and Float
export methods for performing decimal arithmetic, as opposed to JavaScript's usual floating point arithmetic.
When writing custom filters that expect numbers as inputs, you should be prepared to handle JavaScript primitive numbers and Liquid numbers. All built-in math filters convert their arguments to Liquid's NumberT
type on input, exclusively use methods of those types for arithmetic, and return a NumberT
too.
To illustrate, here's the implementation of the plus
filter. It makes no assumptions about the type of its arguments and both arguments default to zero if they can't be converted to a number.
- JavaScript
- TypeScript
import { FilterContext, NumberT, checkArguments, filters } from "liquidscript";
function plus(left, right) {
// Throw an error if there are too many or too few arguments.
checkArguments(arguments.length, 1, 1);
return filters.parseNumberOrZero(left).plus(filters.parseNumberOrZero(right));
}
import { FilterContext, NumberT, checkArguments, filters } from "liquidscript";
function plus(this: FilterContext, left: unknown, right: unknown): NumberT {
// Throw an error if there are too many or too few arguments.
checkArguments(arguments.length, 1, 1);
return filters.parseNumberOrZero(left).plus(filters.parseNumberOrZero(right));
}
Auto-Escape and Markup
LiquidScript exports a Markup
object that wraps a string, indicating it is safe to output without HTML escaping. Most filter functions that expect strings as inputs should be prepared to handle Markup
objects too.
Here's an implementation of the append
filter that demonstrates handling of Markup
objects.
- JavaScript
- TypeScript
import {
FilterContext,
checkArguments,
Markup,
toLiquidString,
object,
} from "liquidscript";
function append(left, other) {
// Throw an error if there are too many or too few arguments.
checkArguments(arguments.length, 1, 1);
if (left instanceof Markup)
return new Markup(
left[toLiquidString]() + Markup.escape(other)[toLiquidString]()
);
if (other instanceof Markup) {
return new Markup(
Markup.escape(left)[toLiquidString]() + other[toLiquidString]()
);
}
return object.liquidStringify(left) + object.liquidStringify(other);
}
import {
FilterContext,
checkArguments,
Markup,
toLiquidString,
object,
} from "liquidscript";
function append(
this: FilterContext,
left: unknown,
other: unknown
): string | Markup {
// Throw an error if there are too many or too few arguments.
checkArguments(arguments.length, 1, 1);
if (left instanceof Markup)
return new Markup(
left[toLiquidString]() + Markup.escape(other)[toLiquidString]()
);
if (other instanceof Markup) {
return new Markup(
Markup.escape(left)[toLiquidString]() + other[toLiquidString]()
);
}
return object.liquidStringify(left) + object.liquidStringify(other);
}
Missing and Excess Arguments
All filters built in to Liquid throw a LiquidFilterArgumentError
if a required argument is missing or too many arguments are provided.
When writing custom filters, if you want to be consistent with those built-in filters, you can use checkArguments
to throw an error with a suitable message.
Undefined vs undefined
LiquidScript defines an Undefined
type, which is distinct from JavaScript's primitive undefined
value. With LaxUndefined
, Undefined
objects will be passed to filter functions if they are "called" with arguments that can not be resolved by the active render context.
If a filter function needs to detect Undefined
and undefined
arguments, it can use object.isUndefined()
.