mozilla / nunjucks

A powerful templating engine with inheritance, asynchronous control, and more (jinja2 inspired)
https://mozilla.github.io/nunjucks/
BSD 2-Clause "Simplified" License
8.55k stars 638 forks source link

Recursive for loops #416

Open ArmorDarks opened 9 years ago

ArmorDarks commented 9 years ago

Since there is no while nor in Jinja, nor in Nunjucks, and without it (or functions) it's quite tricky (or maybe even impossible) to, let's say, output multilevel object (which can be, for example, sitemap), it would be very nice to have recursive flag for loops, as per http://jinja.pocoo.org/docs/dev/templates/#for

jlongster commented 9 years ago

My recommendation is that you shouldn't be doing something that complex in nunjucks. You should create a global function, filter, or custom tag.

VictorioBerra commented 8 years ago

I would also like the recursive flag in Jinja to work in nunjucks.

carljm commented 8 years ago

I wasn't even aware of that Jinja feature! My goal is to get as close to Jinja feature parity as reasonably feasible, so I don't have any objection to this feature; if somebody implements it, I'll happily review and merge the PR.

VictorioBerra commented 8 years ago

In the mean time, what would be a possible solution to this issue? Not being able to call any sort of parent function is really difficult.

carljm commented 8 years ago

I guess you'd have to write a global function or custom tag like @jlongster suggested.

VictorioBerra commented 8 years ago

The filter was really simple to write for those curious. But it gives me virtually no template control since the HTML is hard coded and it requires the safe filter which the code that is produces is definitely NOT safe.

I struggled with the custom tags for a while but the documentation for them is incredibly lacking. Example nunjucks.runtime.SafeString what is this?? Is there a way to escape user submitted data and then combine it with this? Is it possible to call this.parse() multiple times? Ill update if I somehow figure this out. But it would be really really nice for someone who already understands the Nunjucks code base to add the recursive call inside the existing for loop tag system. I have no idea where to start to find that but I think it will be MUCH easier to modify that then for me to essentially re-code the for loop system in a custom tag...

Filter

nunjucs_env.addFilter('recursive', function(nodes) {

    var ret = [];

    var nodeProcessor = function(nodes){
        ret.push('<ul>');
        nodes.forEach(function(node){
            ret.push('<li>' + node.post + '</li>');
            if(node.children){
                nodeProcessor(node.children);
            }
        });
        ret.push('</ul>');
    }

    nodeProcessor(nodes);

    return ret.join('');

});
VictorioBerra commented 8 years ago

Okay, this works: what can I do better?

Custom Tag (recursivefor)

function RecursiveForExtension() {
    var self = this;

    self.tags = ['recursivefor'];

    self.parse = function(parser, nodes, lexer) {

        var tok = parser.nextToken();

        var args = parser.parseSignature(null, true);
        parser.advanceAfterBlockEnd(tok.value);

        var body_recursiveif = parser.parseUntilBlocks('recursiveif', 'recursive', 'endrecursiveif', 'endrecursivefor');

        if(parser.skipSymbol('recursiveif')) {
            parser.skip(lexer.TOKEN_BLOCK_END);
            var body_recursive = parser.parseUntilBlocks('recursive');
        }

        if(parser.skipSymbol('recursive')) {
            parser.skip(lexer.TOKEN_BLOCK_END);
            var body_endrecursiveif = parser.parseUntilBlocks('endrecursiveif');
        }

        if(parser.skipSymbol('endrecursiveif')) {
            parser.skip(lexer.TOKEN_BLOCK_END);
            var body_endrecursivefor = parser.parseUntilBlocks('endrecursivefor');
        }

        parser.advanceAfterBlockEnd();

        //See above for notes about CallExtension
        return new nodes.CallExtension(this, 'run', args, [body_recursiveif, body_recursive, body_endrecursiveif, body_endrecursivefor]);
    };

    self.run = function(context, args, body_recursiveif, body_recursive, body_endrecursiveif, body_endrecursivefor) {

        var ret = [];

        var recursionHandler = function(items) {

            items.forEach(function(item) {
                context.ctx.item = item;
                var bodyProcess = body_recursiveif();
                var endBodyProcess = body_endrecursivefor();
                ret.push(bodyProcess);
                if(item.children){
                    ret.push(body_recursive());
                    recursionHandler(item.children);
                    ret.push(body_endrecursiveif());
                };
                ret.push(endBodyProcess);
            });

        }

        recursionHandler(args);

        return new nunjucks.runtime.SafeString(ret.join(''));
    };
}

nunjucks_env.addExtension('RecursiveForExtension', new RecursiveForExtension());

test.nunjucks

<ul class="sitemap">
{%- recursivefor thread.posts %}
    <li>
      <a>{{ item.post }}</a>
      {% recursiveif %}
        <ul class="submenu">{% recursive %}</ul>
      {% endrecursiveif %}
    </li>
{%- endrecursivefor %}
</ul>

Information:

Each object in the array you pass to recursivefor MUST be called item. No tags are optional other than the HTML and the item variables of course.

ArmorDarks commented 8 years ago

The filter was really simple to write for those curious. But it gives me virtually no template control since the HTML is hard coded and it requires the safe filter which the code that is produces is definitely NOT safe.

This is why initially I've started this issue — filters obviously doesn't work well for those cases.

Thanks for sharing custom extension! Didn't try it yet, though.

jbmoelker commented 8 years ago

Just like a function a macro can call itself recursively. So I'd argue there is no need for a custom "recursive" tag in the Nunjucks core library. Here's an example of solving this with normal macro:

<nav>
    <h2>Site menu</h2>
    <ul>
        {% for item in menu %}
        {{ menuItem(item) }}
        {% endfor %}
    </ul>
</nav>

{% macro menuItem(item) %}
<li>
    {{ item.text }}
    {% if item.items %}
    <ul>
        {% for item in item.items %}
        {{ menuItem(item) }}
        {% endfor %}
    </ul>
    {% endif %}
</li>
{% endmacro %}

See Live example of menu with nested items using Nunjucks macro recursively.

VictorioBerra commented 8 years ago

I had no idea a macro could call itself. This is a much easier solution.

woodbrearlham commented 6 years ago

What I have done is used the groupBy filter so the children can select which column they are in. Then I groupby colNum

VictorioBerra commented 6 years ago

Yeah, why not close this? Seems like we have two easy ways to do this. @carljm

fdintino commented 6 years ago

I have opted not to close this because it is a feature in jinja2 and adding it would increase syntax parity.