Sistema de templates realizado en Node.js

Este es un sistema sencillo de templates realizado con Node que hice como paso intermedio para adaptar nuestro ecosistema de plantillas a Twig, me permitió ir limpiando código duplicado e identificando bloques de forma rápida. La escribí desde cero y aunque es muy sencilla, funciona realmente bien. Está pensada para trabajar con plantillas php y necesita las siguientes carpetas:

/templates es rellenada automáticamente con las plantillas generadas.

 /sources los archivos de texto con la extensión .tpl que generan las templates
/base  Las plantillas base de las que derivan las demás.
 /mods  los módulos php para trabajar con los bloques

FORMATO DE LAS PLANTILLAS BASE PHP:

Se dividen los bloques de código susceptible de ser modificado con comentarios php con un nombre arbitrario con el siguiente formato (se pueden anidar):

<?php#bloque?> 

    <?php#subbloque?>

    <?php#/subbloque?> 

<?php#/bloque?>
(Los tags de inicio y fin de bloque se borran de las templates .php generadas)
Si fuera necesario usarlos como “ancla” para añadir contenido se pueden poner bloques vacíos donde sea necesario sin problema, o añadir bloques en cualquier momento que surja la necesidad.
Para cada plantilla que queramos crear, se añade un archivo de texto con la extension .tpl en /sources, en la primera linea indicamos la template maestra, en las siguientes, las modificaciones que queremos hacerle.
El motor coge cada cada archivo  sources/*.tpl y lo transforma en un templates/*.php con el mismo nombre, relleno con el código de la plantilla base indicada y modificado con lo que se haya pedido en el archivo tpl.
 
FORMATO DEL ARCHIVO .TPL:
Primera linea>> el nombre (sin la extensión) de la plantilla base
Siguientes líneas>> modificaciones a la plantilla base.
Las modificaciones se indican así: bloque (>|+|-)  modulo, que indican sustituir, añadir o quitar elementos de la plantilla base o de los módulos cargados.
El motor interpreta linea a linea secuencialmente, modificando el código resultado de aplicar la linea anterior, o sea que si en una línea cargamos un módulo, en las siguientes podemos modificar sus bloques cargando nuevos módulos en ellos y así  hasta el infinito (o se acaben las lineas del archivo .tpl, lo primero que ocurra 🙂 ).

Una plantilla podría tener el siguiente contenido:

fr_base //coge la plantilla base/fr_base.php
head > head_orange // cambia el bloque head por el contenido de mods/head_orange.php
main + thumbs  // a continuación  del bloque “main” añade el modulo mods/thumbs.php
boton – //borra el bloque boton, que puede estar en cualquiera de los bloques anteriores

 
Lo que hace la rutina es leer los .tpl, cargar el código del php indicado en la primera linea y a continuación interpreta linea a linea el resto del archivo modificando el código del php en consecuencia. Básicamente lo que hace es buscar con una expresión regular el bloque indicado a la izquierda del operador y sustituirlo o añadir el archivo php indicado  ala derecha, o borrarlo sin mas.
Toda la magia del sistema está en esta Regexp:
RegExp("<\\?php#" + origen + "\\?>((?!<\\?php#\\/\\?>).|\\r\\n|\\r|\\n)*<\\?php#\\/" + origen + "\\?>");

“Origen” es el bloque indicado a la izquierda del operador (>, + o -), con esto seleccionamos el contenido del bloque y el resto es trivial.  Permite mantener los comentarios usuales en los phps porque se limita a buscar comentarios que estén cerrados con “/”

 A continuación, el código.  Son menos de 100 lineas de código y aun se podría optimizar más, aun tiene partes del prototipado, aunque no sé si volveré a el porque ya ha cumplido su cometido, y tampoco era su función complicarse mucho más.
var fs = require('fs');
var glob = require('glob');

compila();

function compila() {
    const util = require('util');
    const glob = util.promisify(require('glob'));
    glob('../cdn.landing.engine/sources/**/*.tpl', function(err, files) {
        files.forEach(function(element) {
            lee_plantilla(element);
        });
    });
}

function lee_plantilla(template) {
    fs.readFile(template, (err, data, isdone) => {
        data = data.toString().replace(" ", "");
        if (data.charAt(data.length - 1) != "\n\n") {
            data += "\n"
        }
        if (data.split(/[\r|\n]+/).length > 1) {
            var index = 0;
            data.split(/[\r|\n]+/).forEach(function(line) {
                index++;
                switch (index) {
                    case 1:
                        codigo = fs.readFileSync("..engine/sources/base/" + line + ".php");
                        break;
                    case data.split(/[\r|\n]+/).length:
                        escribe_php(codigo.toString(), template.split("/").pop())
                        break;
                    default:
                        codigo = interpreta(codigo.toString(), line, template);
                        break;
                }
            })
        } else {
            fs.readFile("../cdn.landing.engine/templates_sources/base/" + data.toString() + ".php", (err, data, isdone) => {
                escribe_php(data.toString(), template.split("/").pop())
            })
        }
    })
}

function escribe_php(contenido, destino) {
    fs.writeFile("../engine/templates/" + destino.replace(".tpl", ".php"), limpiatags(contenido), function(err) {
        console.log("error" + err)
    });
}

function interpreta(codigo, linea, template) {
    if (linea.indexOf('+') > -1) {
        divisor = linea.indexOf('+');
        var operador = "+";
    }
    if (linea.indexOf('>') > -1) {
        divisor = linea.indexOf('>');
        var operador = ">";
    }
    if (linea.indexOf('-') > -1) {
        divisor = linea.indexOf('-');
        var operador = "-";
    }
    var origen = linea.slice(0, divisor);
    var destino = linea.slice(divisor + 1, linea.length);
    var formula = new RegExp("<\\?php#" + origen + "\\?>((?!<\\?php#\\/\\?>).|\\r\\n|\\r|\\n)*<\\?php#\\/" + origen + "\\?>");
    var a = codigo.match(formula).slice(0, -1);
    new_code = fs.readFileSync("../engine/sources/mods/" + destino + ".php");
    switch (operador) {
        case "+":
            return codigo.replace(a, (a + new_code));
            break;
        case ">":
            return codigo.replace(a, new_code);
            break;
        case "-":
            return codigo.replace(a, "");
            break;
    }
}

function limpiatags(codigo) {
    var formula = new RegExp("<\\?php#((?!>).)*\\?>", "g");
    var a = codigo.match(formula);
    if (a) {
            for (var i = 0; i < a.length; i++) {
                codigo = codigo.replace(a[i], "")
            }
    }
     return codigo.replace(/^(?:[\t ]*(?:\r?\n|\r))+/gm, "")
}

Trabajando con Node.js

He empezado a profundizar con Node, que hasta ahora no lo usaba más que como un entorno donde correr Gulp y poco más.
Es muy sencillo y usa the-old-good JS de toda la vida sin más problemas ni lios. Las dependencias se inyectan de forma muy sencilla y puedes tener toda una aplicación en un solo archivo si no es necesario más.

De repente ya no quiero tanto a Gulp, me he dado cuenta de que casi todo para lo que usaba Gulp lo puedo hacer usando Node y usando  los módulos NPM directamente sin esperar a que alguien los adapten a Gulp o  mantengan actualizados.
Por ejemplo para automatizar los pantallazos de testing la única herramienta que he encontrado en Gulp usa PhantomJs, que es un proyecto abandonado y emplea un motor de render webkit con 10 años que ni siquiera acepta Flex. Sin embargo, usando Node directamente, puedo escribir un script que use Puppeteer y renderice con Chrome headless, compare produccion y stagging y me guarde pantallazos con los errores marcados en una carpeta. Tenemos que testar mas de 300 webs, y lo hace tan rápido que llamaron del servicio de hosting creyendo que estaba recibiendo un ataque DDoS (tuve que ralentizarlo)

Conforme más aprendo de Node menos uso Gulp y creo que en breve lo utilizare solo residualmente.

Aunque hasta el momento solo lo estoy usando como motor de script,  empiezo a experimentar con servidores y es una verdadera gozada poder hacer un back-end sin salir de javascript, con las herramientas que antes estaban vetadas ejecutando en el cliente, como escribir archivos, manipular bases de datos, etc. cosas para las que hasta ahora necesitaba usar scripts intermedios en php.

Como bonus, es el lenguaje que utilizan las placas Arduino, con lo que si un dia me decido por fin a construir un ejercito de robots para destruir el mundo ya tengo parte del camino hecho.

En fin, que mola mucho.