in packages/jinja/src/runtime.ts [690:1004]
private applyFilter(operand: AnyRuntimeValue, filterNode: Identifier | CallExpression, environment: Environment) {
// For now, we only support the built-in filters
// TODO: Add support for non-identifier filters
// e.g., functions which return filters: {{ numbers | select("odd") }}
// TODO: Add support for user-defined filters
// const filter = environment.lookupVariable(node.filter.value);
// if (!(filter instanceof FunctionValue)) {
// throw new Error(`Filter must be a function: got ${filter.type}`);
// }
// return filter.value([operand], environment);
// https://jinja.palletsprojects.com/en/3.0.x/templates/#list-of-builtin-filters
if (filterNode.type === "Identifier") {
const filter = filterNode as Identifier;
if (filter.value === "tojson") {
return new StringValue(toJSON(operand));
}
if (operand instanceof ArrayValue) {
switch (filter.value) {
case "list":
return operand;
case "first":
return operand.value[0];
case "last":
return operand.value[operand.value.length - 1];
case "length":
return new IntegerValue(operand.value.length);
case "reverse":
return new ArrayValue(operand.value.reverse());
case "sort":
return new ArrayValue(
operand.value.sort((a, b) => {
if (a.type !== b.type) {
throw new Error(`Cannot compare different types: ${a.type} and ${b.type}`);
}
switch (a.type) {
case "IntegerValue":
case "FloatValue":
return (a as IntegerValue | FloatValue).value - (b as IntegerValue | FloatValue).value;
case "StringValue":
return (a as StringValue).value.localeCompare((b as StringValue).value);
default:
throw new Error(`Cannot compare type: ${a.type}`);
}
})
);
case "join":
return new StringValue(operand.value.map((x) => x.value).join(""));
case "string":
return new StringValue(toJSON(operand));
case "unique": {
const seen = new Set();
const output: AnyRuntimeValue[] = [];
for (const item of operand.value) {
if (!seen.has(item.value)) {
seen.add(item.value);
output.push(item);
}
}
return new ArrayValue(output);
}
default:
throw new Error(`Unknown ArrayValue filter: ${filter.value}`);
}
} else if (operand instanceof StringValue) {
switch (filter.value) {
// Filters that are also built-in functions
case "length":
case "upper":
case "lower":
case "title":
case "capitalize": {
const builtin = operand.builtins.get(filter.value);
if (builtin instanceof FunctionValue) {
return builtin.value(/* no arguments */ [], environment);
} else if (builtin instanceof IntegerValue) {
return builtin;
} else {
throw new Error(`Unknown StringValue filter: ${filter.value}`);
}
}
case "trim":
return new StringValue(operand.value.trim());
case "indent":
return new StringValue(
operand.value
.split("\n")
.map((x, i) =>
// By default, don't indent the first line or empty lines
i === 0 || x.length === 0 ? x : " " + x
)
.join("\n")
);
case "join":
case "string":
return operand; // no-op
case "int": {
const val = parseInt(operand.value, 10);
return new IntegerValue(isNaN(val) ? 0 : val);
}
case "float": {
const val = parseFloat(operand.value);
return new FloatValue(isNaN(val) ? 0.0 : val);
}
default:
throw new Error(`Unknown StringValue filter: ${filter.value}`);
}
} else if (operand instanceof IntegerValue || operand instanceof FloatValue) {
switch (filter.value) {
case "abs":
return operand instanceof IntegerValue
? new IntegerValue(Math.abs(operand.value))
: new FloatValue(Math.abs(operand.value));
case "int":
return new IntegerValue(Math.floor(operand.value));
case "float":
return new FloatValue(operand.value);
default:
throw new Error(`Unknown NumericValue filter: ${filter.value}`);
}
} else if (operand instanceof ObjectValue) {
switch (filter.value) {
case "items":
return new ArrayValue(
Array.from(operand.value.entries()).map(([key, value]) => new ArrayValue([new StringValue(key), value]))
);
case "length":
return new IntegerValue(operand.value.size);
default:
throw new Error(`Unknown ObjectValue filter: ${filter.value}`);
}
} else if (operand instanceof BooleanValue) {
switch (filter.value) {
case "bool":
return new BooleanValue(operand.value);
case "int":
return new IntegerValue(operand.value ? 1 : 0);
case "float":
return new FloatValue(operand.value ? 1.0 : 0.0);
case "string":
return new StringValue(operand.value ? "true" : "false");
default:
throw new Error(`Unknown BooleanValue filter: ${filter.value}`);
}
}
throw new Error(`Cannot apply filter "${filter.value}" to type: ${operand.type}`);
} else if (filterNode.type === "CallExpression") {
const filter = filterNode as CallExpression;
if (filter.callee.type !== "Identifier") {
throw new Error(`Unknown filter: ${filter.callee.type}`);
}
const filterName = (filter.callee as Identifier).value;
if (filterName === "tojson") {
const [, kwargs] = this.evaluateArguments(filter.args, environment);
const indent = kwargs.get("indent") ?? new NullValue();
if (!(indent instanceof IntegerValue || indent instanceof NullValue)) {
throw new Error("If set, indent must be a number");
}
return new StringValue(toJSON(operand, indent.value));
} else if (filterName === "join") {
let value;
if (operand instanceof StringValue) {
// NOTE: string.split('') breaks for unicode characters
value = Array.from(operand.value);
} else if (operand instanceof ArrayValue) {
value = operand.value.map((x) => x.value);
} else {
throw new Error(`Cannot apply filter "${filterName}" to type: ${operand.type}`);
}
const [args, kwargs] = this.evaluateArguments(filter.args, environment);
const separator = args.at(0) ?? kwargs.get("separator") ?? new StringValue("");
if (!(separator instanceof StringValue)) {
throw new Error("separator must be a string");
}
return new StringValue(value.join(separator.value));
} else if (filterName === "int" || filterName === "float") {
const [args, kwargs] = this.evaluateArguments(filter.args, environment);
const defaultValue =
args.at(0) ?? kwargs.get("default") ?? (filterName === "int" ? new IntegerValue(0) : new FloatValue(0.0));
if (operand instanceof StringValue) {
const val = filterName === "int" ? parseInt(operand.value, 10) : parseFloat(operand.value);
return isNaN(val) ? defaultValue : filterName === "int" ? new IntegerValue(val) : new FloatValue(val);
} else if (operand instanceof IntegerValue || operand instanceof FloatValue) {
return operand;
} else if (operand instanceof BooleanValue) {
return filterName === "int"
? new IntegerValue(operand.value ? 1 : 0)
: new FloatValue(operand.value ? 1.0 : 0.0);
} else {
throw new Error(`Cannot apply filter "${filterName}" to type: ${operand.type}`);
}
} else if (filterName === "default") {
const [args, kwargs] = this.evaluateArguments(filter.args, environment);
const defaultValue = args[0] ?? new StringValue("");
const booleanValue = args[1] ?? kwargs.get("boolean") ?? new BooleanValue(false);
if (!(booleanValue instanceof BooleanValue)) {
throw new Error("`default` filter flag must be a boolean");
}
if (operand instanceof UndefinedValue || (booleanValue.value && !operand.__bool__().value)) {
return defaultValue;
}
return operand;
}
if (operand instanceof ArrayValue) {
switch (filterName) {
case "selectattr":
case "rejectattr": {
const select = filterName === "selectattr";
if (operand.value.some((x) => !(x instanceof ObjectValue))) {
throw new Error(`\`${filterName}\` can only be applied to array of objects`);
}
if (filter.args.some((x) => x.type !== "StringLiteral")) {
throw new Error(`arguments of \`${filterName}\` must be strings`);
}
const [attr, testName, value] = filter.args.map((x) => this.evaluate(x, environment)) as StringValue[];
let testFunction: (...x: AnyRuntimeValue[]) => boolean;
if (testName) {
// Get the test function from the environment
const test = environment.tests.get(testName.value);
if (!test) {
throw new Error(`Unknown test: ${testName.value}`);
}
testFunction = test;
} else {
// Default to truthiness of first argument
testFunction = (...x: AnyRuntimeValue[]) => x[0].__bool__().value;
}
// Filter the array using the test function
const filtered = (operand.value as ObjectValue[]).filter((item) => {
const a = item.value.get(attr.value);
const result = a ? testFunction(a, value) : false;
return select ? result : !result;
});
return new ArrayValue(filtered);
}
case "map": {
// Accumulate kwargs
const [, kwargs] = this.evaluateArguments(filter.args, environment);
if (kwargs.has("attribute")) {
// Mapping on attributes
const attr = kwargs.get("attribute");
if (!(attr instanceof StringValue)) {
throw new Error("attribute must be a string");
}
const defaultValue = kwargs.get("default");
const mapped = operand.value.map((item) => {
if (!(item instanceof ObjectValue)) {
throw new Error("items in map must be an object");
}
return item.value.get(attr.value) ?? defaultValue ?? new UndefinedValue();
});
return new ArrayValue(mapped);
} else {
throw new Error("`map` expressions without `attribute` set are not currently supported.");
}
}
}
throw new Error(`Unknown ArrayValue filter: ${filterName}`);
} else if (operand instanceof StringValue) {
switch (filterName) {
case "indent": {
// https://jinja.palletsprojects.com/en/3.1.x/templates/#jinja-filters.indent
// Return a copy of the string with each line indented by 4 spaces. The first line and blank lines are not indented by default.
// Parameters:
// - width: Number of spaces, or a string, to indent by.
// - first: Don't skip indenting the first line.
// - blank: Don't skip indenting empty lines.
const [args, kwargs] = this.evaluateArguments(filter.args, environment);
const width = args.at(0) ?? kwargs.get("width") ?? new IntegerValue(4);
if (!(width instanceof IntegerValue)) {
throw new Error("width must be a number");
}
const first = args.at(1) ?? kwargs.get("first") ?? new BooleanValue(false);
const blank = args.at(2) ?? kwargs.get("blank") ?? new BooleanValue(false);
const lines = operand.value.split("\n");
const indent = " ".repeat(width.value);
const indented = lines.map((x, i) =>
(!first.value && i === 0) || (!blank.value && x.length === 0) ? x : indent + x
);
return new StringValue(indented.join("\n"));
}
case "replace": {
const replaceFn = operand.builtins.get("replace");
if (!(replaceFn instanceof FunctionValue)) {
throw new Error("replace filter not available");
}
const [args, kwargs] = this.evaluateArguments(filter.args, environment);
return replaceFn.value([...args, new KeywordArgumentsValue(kwargs)], environment);
}
}
throw new Error(`Unknown StringValue filter: ${filterName}`);
} else {
throw new Error(`Cannot apply filter "${filterName}" to type: ${operand.type}`);
}
}
throw new Error(`Unknown filter: ${filterNode.type}`);
}