mustache

Can I render a verbal list using mustache?


I want to render a list like ["foo", "bar", "baz"] into a string like "foo, bar, and baz" using mustache with a function.

After reading the documentation and messing with the playground, I found that something like

{
    list: ['foo', 'bar', 'baz'],
    render_list: function(section) {
        return section[1] + ',' + section[2] + ', and ' + section[3];
    },
}
{{#render_list}}{{list}}{{/render_list}}

doesn't work because section is the string literal "{{list}}" and doesn't contain any info about the actual item list.

Savestate for the playground:

{"data":{"text":"{\n    list: ['foo', 'bar', 'baz'],\n    render_list: function(section) {\n        return section[1] + ',' + section[2] + ', and ' + section[3];\n    },\n}"},"templates":[{"name":"main","text":"{{#render_list}}{{list}}{{/render_list}}"}]}

Solution

  • Mustache has the infamous trailing comma problem, which refers to the fact that there is no straightforward way with only template code to join a list with commas (or some other infix) while omitting a comma at the end. This question poses a more advanced variant of this problem, where we not only want to prevent a trailing comma, but we also want to use a different infix for the final join. I hereby propose to call this the trailing comma plus one problem. The lack of a ready-to-use template notation for this common problem is the price we have to pay for the language being logic-free.

    Despite this being a "problem", it can still be done. As the asker already realized, we somehow need to transform the data using the host programming language. In the playground, that host programming language is JavaScript. I will be using JavaScript in the examples below as well, but I will address portability to other programming languages along the way.

    Different ways to transform

    One possible way to transform a list is to directly render it to the joined string that we want to obtain. This is the approach that the asker was taking. A generic function that does this job might look like this:

    function render(list) {
        var text = list[0];
        for (var l = list.length - 1, i = 1; i < l; ++i) {
            text += ', ' + list[i];
        }
        return text + ' and ' + list[list.length - 1];
    }
    

    In this case, the template does not need to iterate over the list, because it has already been reduced to a single string. We will be using a plain interpolation tag, something along the lines of {{renderedList}}.

    Another approach would be to enrich the list with enough information, so that the joining can be correctly done in the template instead of in the programming language. In this case, this means that we need to be able to determine whether we are currently rendering the first element, the last element, or an element in between. A generic function that does such enrichment might look like this:

    function enumerate(list) {
        var mapped = [{value: list[0], first: true}];
        for (var l = list.length - 1, i = 1; i < l; ++i) {
            mapped.push({value: list[i]});
        }
        mapped.push({value: list[list.length - 1], last: true});
        return mapped;
    }
    

    In this case, we will be using a section tag in order to do the iteration inside the template. We use the first and last booleans in order to determine which infix to apply (, , and or none). The template code is a bit complicated, a single iteration within the section will look like this:

    {{^first}}{{^last}}, {{/last}}{{#last}} and {{/last}}{{/first}}{{value}}
    

    While the code is a bit more complex overall, this approach has two advantages. Firstly, the list remains a list, so we can also iterate over it in other ways. Secondly, we now have a generic enumerate function that we could use for other purposes. For example, we might use it to render column headers in a table, and we might also want to add an index to each enriched value, so that we can render this in the template as well.

    When to transform

    With either approach, the question remains how to apply the transformation (the render or enumerate function) to the list. There are two main flavors: (1) apply the transformation already before the list is passed to the template, or (2) add a notation to the template that causes the transformation to be applied to the list while rendering.

    Flavor (1) is very straightforward: in the host programming language, we simply apply the function to the raw list. Then, we pass the result of the transformation to the template instead of (or in addition to) the original list.

    In flavor (2), we only pass the raw list to the template, as well as the transforming function. In the template code, we are then free to apply or not apply the transformation. This is what the asker was attempting with the lambda notation. Lambdas do not work for this purpose, as the asker correctly observed, because they operate on the template itself rather than on any context value. However, we can use a different mechanism: dotted names.

    According to the specification, when the Mustache engine encounters a dotted name of the form part1.part2 inside an interpolation or section tag, it must first look up part1 in the context. It then looks for part2 inside the value it just found. Depending on the programming language, this generally means that part2 must be a member/property/attribute/entry/field/projection of part1. If there are more parts, this process is repeated. The final value is used to render the tag.

    In this process, if any found value is a function, it must be invoked, and the return value must be used to continue the lookup. Now, if part2 names a function, in most programming languages, this means that it is a method of part1. In many programming languages, such a method will be able to access part1 through its this or self binding.

    In JavaScript, we can exploit these rules by making the transformation a method of the list. To make this work, we have to adjust the above functions a bit: instead of a list argument, we operate on this. We are lucky that JavaScript allows us to assign any function as a method to any object. In other programming languages, there is often still a way to do it, but it might require defining a wrapper type for the list.

    Putting things together

    With the above approaches and flavors combined, let us explore four spec-compliant ways to solve the trailing commas plus one problem. In each of the following examples, we will be rendering a list of fermented foods as cheese, wine, kimchi and natto.

    Join in host language, transform before render

    This is arguably the simplest and most direct approach, but it amounts to doing all the work in JavaScript. The Mustache template is trivial.

    payload

    fermented_foods = render(['cheese', 'wine', 'kimchi', 'natto']);
    

    template

    {{fermented_foods}}
    

    savestate

    {"data":{"text":"(function() {\n    function render(list) {\n        var text = list[0];\n        for (var l = list.length - 1, i = 1; i < l; ++i) {\n            text += ', ' + list[i];\n        }\n        return text + ' and ' + list[list.length - 1];\n    }\n    return {\n        fermented_foods: render(['cheese', 'wine', 'kimchi', 'natto']),\n    };\n}())"},"templates":[{"name":"main","text":"{{fermented_foods}}"}]}
    

    Join in host language, transform during render

    payload

    fermented_foods = ['cheese', 'wine', 'kimchi', 'natto'];
    fermented_foods.render = render;
    

    template

    {{fermented_foods.render}}
    

    savestate

    {"data":{"text":"(function() {\n    function render() {\n        var text = this[0];\n        for (var l = this.length - 1, i = 1; i < l; ++i) {\n            text += ', ' + this[i];\n        }\n        return text + ' and ' + this[this.length - 1];\n    }\n    var fermented_foods = ['cheese', 'wine', 'kimchi', 'natto'];\n    fermented_foods.render = render;\n    return {fermented_foods};\n}())"},"templates":[{"name":"main","text":"{{fermented_foods.render}}"}]}
    

    Enrich list, transform before render

    payload

    fermented_foods = enumerate(['cheese', 'wine', 'kimchi', 'natto']);
    

    template

    {{#fermented_foods
    }}{{^first}}{{^last}}, {{/last}}{{#last}} and {{/last}}{{/first}}{{value
    }}{{/fermented_foods}}
    

    savestate

    {"data":{"text":"(function() {\n    function enumerate(list) {\n        var mapped = [{value: list[0], first: true}];\n        for (var l = list.length - 1, i = 1; i < l; ++i) {\n            mapped.push({value: list[i]});\n        }\n        mapped.push({value: list[list.length - 1], last: true});\n        return mapped;\n    }\n    return {\n        fermented_foods: enumerate(['cheese', 'wine', 'kimchi', 'natto']),\n    };\n}())"},"templates":[{"name":"main","text":"{{#fermented_foods\n}}{{^first\n    }}{{^last}}, {{/last\n    }}{{#last}} and {{/last\n}}{{/first\n}}{{value}}{{/fermented_foods}}"}]}
    

    Enrich list, transform during render

    This is the most portable variant, because most of the rendering takes place in the template.

    payload

    fermented_foods = ['cheese', 'wine', 'kimchi', 'natto'];
    fermented_foods.enumerate = enumerate;
    

    template

    {{#fermented_foods.enumerate
    }}{{^first}}{{^last}}, {{/last}}{{#last}} and {{/last}}{{/first}}{{value
    }}{{/fermented_foods.enumerate}}
    

    savestate

    {"data":{"text":"(function() {\n    function enumerate() {\n        var mapped = [{value: this[0], first: true}];\n        for (var l = this.length - 1, i = 1; i < l; ++i) {\n            mapped.push({value: this[i]});\n        }\n        mapped.push({value: this[this.length - 1], last: true});\n        return mapped;\n    }\n    var fermented_foods = ['cheese', 'wine', 'kimchi', 'natto'];\n    fermented_foods.enumerate = enumerate;\n    return {fermented_foods};\n}())"},"templates":[{"name":"main","text":"{{#fermented_foods.enumerate\n}}{{^first\n    }}{{^last}}, {{/last\n    }}{{#last}} and {{/last\n}}{{/first\n}}{{value}}{{/fermented_foods.enumerate}}"}]}
    

    Nonstandard ways to transform during render

    Admittedly, it is inconvenient that we can only transform inside the template if the transformation function is a method of the value to transform. It would be great if the transformation could just be a standalone function that we could pass an arbitrary context value to. Various ways to do this have been proposed. I will mention three of them below. Do keep in mind that none of these notations is currently portable across different Mustache implementations.

    Filters

    This would let you write {{fermented_foods | render}} or {{#fermented_foods | enumerate}}...{{/fermented_foods | enumerate}}. While not everyone agrees whether filters should make it into the specification, this feature does seem to enjoy the most consensus on how it should work. With this notation, render and enumerate could literally be the example functions that I wrote above.

    Several Mustache engines already implement filters. As far as I'm aware, there is currently no Mustache engine for JavaScript that provides filters, though it is planned in Wontache.

    More-powerful lambdas

    Many implementations provide extensions that somehow allow a lambda to access the current context, but they all work differently. The transformation function will often become more complicated that the examples I wrote above. The template might look like {{render.fermented_foods}} or {{#enumerate.fermented_foods}}...{{/enumerate.fermented_foods}}, but it depends.

    (Wontache note: I plan to extend lambdas with additional capabilities, but not in a way that would allow them to access the context, because I believe that filters address that use case more elegantly.)

    Lambdas with arguments

    Would allow you to write something like {{render(fermented_foods)}} or {{#enumerate(fermented_foods)}}...{{/enumerate(fermented_foods)}}. There are a few implementations that do this, but among the three extensions mentioned here, this one enjoys the least consensus.

    Note on the Oxford comma

    The original question would actually write cheese, wine, kimchi, and natto, with a comma directly before the final and. I omitted this for the following reason.

    In the variants in which the list is enriched and the joining happens inside the template, the Oxford comma would allow us to simplify the section body to the following:

    {{^first}}, {{/first}}{{#last}}and {{/last}}{{value}}
    

    but from there, it is not obvious how one could remove the Oxford comma again.

    Conversely, based on the solutions that I provided without the Oxford comma, it is trivial to add it back. Simply replace  and  in any of the code examples by , and . Hence, people who prefer the Oxford comma are better served with a solution that excludes it than the other way around.

    Note on empty lists

    My example code for render and enumerate blindly assumes that list[0] or this[0] exists. In a JavaScript Mustache engine, this is more or less "safe" because list[0] defaults to undefined and because undefined is rendered as an empty string. However, in general, this assumption is obviously not safe. A production-ready implementation of these functions should start by checking that the list is nonempty, and immediately return an empty string or list otherwise.

    Note on functional style

    In my example implementations of render and enumerate, I wrote hand-rolled for loops and used almost no library functions. I did this with the intention to make the code recognizable for as many people as possible, because the question is not specific to JavaScript. However, it would reduce code complexity and maintenance burden to use as many library functions as possible. I believe that the implementations should actually look like this:

    function render(list) {
        var last = _.last(list);
        var initial = list.slice(0, -1).join(', ');
        if (initial && last) return `${initial} and ${last}`;
        return last;
    }
    
    var asValue = value => ({value});
    
    function enumerate(list) {
        var wrapped = list.map(asValue);
        if (wrapped.length) {
            _.first(wrapped).first = true;
            _.last(wrapped).last = true;
        }
        return wrapped;
    }