pebble icon indicating copy to clipboard operation
pebble copied to clipboard

Closures in filters

Open thraidh opened this issue 6 years ago • 0 comments

I'd like to have filters that support closures in general or specifically map(code) and filter(code).

That would allow me to write:

{{ listOfStrings | filter(not it.isEmpty()) | map('['+it+']') | join(',') | stringify }}

where stringify is one of my own filters that turns the input into a valid Java string. For filter and map as above code always uses the implicit argument it.

This could be implemented by duplicating ArgumentsNode::getArgumentMap where every .getValueExpression().evaluate(self, context) is replaced with just .getValueExpression(). Call it getUnevaluatedArgumentMap. Add an empty interface like Unevaluated and in FilterExpression::evaluate replace:

Map<String, Object> namedArguments = args.getArgumentMap(self, context, filter);

with

Map<String, Object> namedArguments;
if (filter instanceof Unevaluated)
    namedArguments = args.getUnevaluatedArgumentMap(self, context, filter);
else
    namedArguments = args.getArgumentMap(self, context, filter);

The user would get the Expression instead of the value in his filter, but can use the implicit _self and _context to evaluate the expression multiple times with different values.

Sample implementation of filter:

class FilterFilter implements Filter, Unevaluated {
    private List<String> args=Arrays.asList("code");
    public List<String> getArgumentNames() { return args; }
    public Object apply(Object input, Map<String, Object> args) {
        if (!(input instanceof Iterable)) return input;
        EvaluationContext context=(EvaluationContext) args.get("_context");
        PebbleTemplateImpl self=(PebbleTemplateImpl) args.get("_self");
        Expression<Boolean> code=(Expression<Boolean>) args.get("code");
        ScopeChain sc=context.getScopeChain();
        sc.pushScope();
        Iterator iter=((Iterable)input).iterator();
        ArrayList<Object> ret=new ArrayList<>();
        while(iter.hasNext()) {
            Object it=iter.next();
            sc.put("it", it);
            Boolean result=code.evaluate(self,context);
            if (result!=null && result) ret.add(it);
        }
        sc.popScope();
        return ret;
    }
}

Alternatively one could add a lazy operator with precedence just above ,, whose evaluate method will just return the inner expression, without evaluating it. With that no other changes need to be made. Now that I wrote all this, I think this can already be implemented using an Extension by adding exactly that unary lazy operator.

Anyway, the first solution would allow to use filter and map without the extra operator and looks better. It is also fully compatible as the user has to opt in to get unevaluated arguments.

thraidh avatar May 14 '18 20:05 thraidh