Skip to main content

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.

info

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.

import { Environment } from "liquidscript";

const env = new Environment();
env.addFilter("ends_with", (val, arg) => 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.

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);

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 }}

info

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.

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));
}

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.

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);
}

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().