javascriptdomnunjuckstemplating-engine

Is there a way to use nunjucks to transform a DOM structure into another?


I frequently build webpages from InDesign exported HTML files. And in InDesign I have some control over the elements class attribute, but it is rather difficult to change the DOM structure itself. And it is not always sufficient to go with the DOM it outputs so I need a way to transform the DOM.

The approach I had was to run a set of jquery commands on a puppeteer instance to imperatively shape the DOM and save the output HTML to a file. It became quite unmaintainable and it's very hard to tell from the code what are the expected input and output.

So I'm trying nunjucks as a template engine to generate the output. But AFAIK Nunjucks is good at printing previously defined variables, but doesn't provide any tools to retrieve its input from a HTML block

I want to write a template/macro with selectors that find the content in the input, kinda like this:

<section>
    <header>
        <h2>{% contentFrom(".section-header") %}</h2>
    </header>
    <main>
        {% contentFrom(".content") %}
    </main>
</section>

And then just wrap my input in a nunjucks tag, like this

{% filter dont_know_yet_what_should_go_in_here }
    <p class="section-header">My_header_here</p>
    <p class="content">My_contents</p>
{% endfilter}

I imagine this could be possible with nunjucks custom filters or custom tags. But if there is a much better solution, I'll accept that as an answer as well!


Solution

  • Ok, I solved it!

    In Nunjucks, one can wrap some html in a {% call someMacro() %} tag. This way, the someMacro() macro gets called with a bonus: The wrapped content can be used inside the macro through the caller() method.

    But caller() returns the whole HTML chunk as a string, and I still need a way to parse it and pick only the elements I need. For that, I used cheerio along with some Nunjucks custom globals.

    Since one can't run pure javascript inside Nunjucks templates, in my gulp setup, I defined an alias to cheerio's load() method and passed it to my nunjucks environment via a global variable (conveniently called $):

    const cheerio = require('cheerio')
    
    function cheer(html, selector) {
        var a = cheerio.load(html.toString())
        return a(selector)
    }
    
    var manageEnvironment = function(environment) {
        environment.addGlobal('$', cheer);
    } // then pass manageEnvironment to Nunjucks initializer function
    

    Then, in my template, I assign caller() to a variable, just to make it less verbose, and then I can use my function to select and modify the input DOM before it gets inserted into my template:

    {% macro myComponent() %} 
    {% set a = caller() %} {# Assigned caller to a shorter variable because it'll be repeated a few times #}
    
    
    <section>
        <header>
            <h1>{{ $(a, '.box-title').html() }}</h1>
        </header>
    
        <main>
            {{ $(a, '._obj_box *:not(.box-title)').html() }}
        </main>
    
        <footer style="border: 2px solid red;">
            {{ $(a, ".see_more-title").html() | urlize | safe }}
        </footer>
    
    </section>
    
    {% endmacro %}
    

    Importante note: I don't know why, but Nunjucks always escaped the HTML returned from my $() method, even when I used the safe filter. Since I have a fully controlled environment, It was easier just to turn off auto escaping for the whole Nunjucks rendering.