Search code examples
template-engine

An idea of a template engine for plain text files


I sometimes need to generate text files. The cases so far were plain/text emails and configuration files. In the former case you must be precise, you can't get away with, "you know, those are generated, they're still readable even if there are extra spaces here and there." In the latter you can, but having a readable result is nice to have.

Now, how do I make the result precise (in terms of whitespace), and the template readable as well? More readable than what follows.

With jinja I can do this:

{%- if order.name -%}
    Name: {{ order.name + '\n' -}}
{%- endif -%}
Phone: {{ order.phone + '\n' -}}

{%- if order.branch or order.address -%}
    {{- '\n' -}}
    {%- if order.branch -%}
        Branch: {{ order.branch + '\n' -}}
    {%- endif -%}
    {%- if order.address -%}
        Address: {{ order.address + '\n' -}}
    {%- endif -%}
{%- endif -%}

{%- for pvxo in order.productvariantxorders.all() -%}
    {{- '\n' -}}
    Product: {{ (pvxo.productvariant.product.name
        if pvxo.productvariant
        else pvxo.product.name) + '\n' -}}
{%- endfor -%}

Which might produce:

Name: ...
Phone: ...

Address: ...

Product: ...

Product: ...

But it took me a while to figure out how to make jinja behave this way.

With Javascript (_.template):

const _ = require('lodash');

const tpl =
    '<% if (order.name) { %>'
        + 'Name: <%= order.name %>\n'
    + '<% } %>'
    + 'Phone: <%= order.phone %>\n'

    + '<% if (order.branch || order.address) { %>'
        + '\n'
        + '<% if (order.branch) { %>'
            + 'Branch: <%= order.branch %>\n'
        + '<% } %>'
        + '<% if (order.address) { %>'
            + 'Address: <%= order.address %>\n'
        + '<% } %>'
    + '<% } %>'

    + '<% order.productvariantxorders.forEach(pvxo => { %>'
        + '\n'
        + 'Product: <%= pvxo.productvariant'
            + ' ? pvxo.productvariant.product.name'
            + ' : pvxo.product.name %>\n'
    + '<% }) %>';

console.log(_.template(tpl)({
    order: {
        name: '...',
        phone: '...',
        address: '...',
        productvariantxorders: [
            {productvariant: {product: {name: '...'}}},
            {product: {name: '...'}},
        ],
    }
}));

Which is more straightforward.

And these 2 doesn't cover the case where you need indents. Still those are the best solutions I could come up with. It doesn't seem like there are many options. The question is language-agnostic. You can point to some particular template engine that provides for a better outcome. Or just suggest an idea of such syntax.

On a side note, here's a somewhat related question I inspected for an answer.


Solution

  • Note. I wish I found something better, but I failed.

    For now I settled with the Javascript solution because it's simpler to understand (more straightforward). An example of a template:

    module.exports =
        'server {\n'
        + '    server_name'
                + '<% domains.forEach(d => { %>'
                    + ' <%= d %>'
                + '<% }) %>'
            + ';\n'
        + '    location / {\n'
        + '        return  301  https://<%= domains[0] %>$request_uri;\n'
        + '    }\n'
        + '    location /.well-known {\n'
        + '        root  <%= ssl_docroot %>;\n'
        + '    }\n'
        + '}\n'
        + '\n'
        + 'server {\n'
        + '    server_name'
                + '<% domains.forEach(d => { %>'
                    + ' <%= d %>'
                + '<% }) %>'
            + ';\n'
        + '    listen  443  ssl;\n'
    
            + '<% const d = (simp_le ? "/etc/certs/" : "/etc/letsencrypt/live/") + domains[0] %>'
        + '    ssl_certificate   <%= d %>/fullchain.pem;\n'
            + '<% const f = simp_le ? "key.pem" : "privkey.pem" %>'
        + '    ssl_certificate_key  <%= d %>/<%= f %>;\n'
        + '\n'
        + '    access_log  /var/log/nginx/<%= domains[0] %>-access.log;\n'
        + '    error_log  /var/log/nginx/<%= domains[0] %>-error.log;\n'
        + '\n'
        + '    client_max_body_size  50m;\n'
        + '\n'
        + '    # https://raw.githubusercontent.com/h5bp/server-configs-nginx/3.3.0/h5bp/web_performance/compression.conf\n'
        + '    gzip on;\n'
        + '    gzip_comp_level 5;\n'
        + '    gzip_min_length 256;\n'
        + '    gzip_proxied any;\n'
        + '    gzip_vary on;\n'
        + '    gzip_types\n'
        + '        application/atom+xml\n'
        + '        application/geo+json\n'
        + '        application/javascript\n'
        + '        application/x-javascript\n'
        + '        application/json\n'
        + '        application/ld+json\n'
        + '        application/manifest+json\n'
        + '        application/rdf+xml\n'
        + '        application/rss+xml\n'
        + '        application/vnd.ms-fontobject\n'
        + '        application/wasm\n'
        + '        application/x-web-app-manifest+json\n'
        + '        application/xhtml+xml\n'
        + '        application/xml\n'
        + '        font/eot\n'
        + '        font/otf\n'
        + '        font/ttf\n'
        + '        image/bmp\n'
        + '        image/svg+xml\n'
        + '        text/cache-manifest\n'
        + '        text/calendar\n'
        + '        text/css\n'
        + '        text/javascript\n'
        + '        text/markdown\n'
        + '        text/plain\n'
        + '        text/xml\n'
        + '        text/vcard\n'
        + '        text/vnd.rim.location.xloc\n'
        + '        text/vtt\n'
        + '        text/x-component\n'
        + '        text/x-cross-domain-policy;\n'
        + '\n'
        + '    charset  utf-8;\n'
        + '\n'
        + '    location / {\n'
        + '        proxy_pass   http://<%= lxc_ip %>;\n'
        + '        proxy_set_header  Host  $http_host;\n'
        + '        proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;\n'
        + '        proxy_set_header  X-Forwarded-Proto  https;\n'
        + '    }\n'
        + '<% if (websockets) { %>'
        + '\n'
        + '    location /ws {\n'
        + '        proxy_pass  http://<%= lxc_ip %>;\n'
        + '        proxy_set_header  Host  $http_host;\n'
        + '        proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;\n'
        + '        proxy_set_header  X-Forwarded-Proto  https;\n'
        + '\n'
        + '        proxy_http_version  1.1;\n'
        + '        proxy_set_header  Upgrade  $http_upgrade;\n'
        + '        proxy_set_header  Connection  Upgrade;\n'
        + '    }\n'
        + '<% } %>'
        + '\n'
        + '    location /.well-known {\n'
        + '        root  <%= ssl_docroot %>;\n'
        + '    }\n'
        + '}\n';
    

    It can be used from a sh script like so:

    cat <<CODE | node - "$tpl" "$vars"
        const _ = require('lodash');
        const tpl = require('./files/' + process.argv[2] + '.tpl');
        const vars = JSON.parse(process.argv[3]);
        console.log(_.template(tpl)(vars));
    CODE