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, "")
}

Expresiones regulares para búsquedas avanzadas (minitutorial)

Las expresiones regulares son unas convenciones para las búsquedas de cadenas que van mucho más allá del uso de simples comodines *.
Se pueden usar en programas de edición, en javascript e incluso hay algo parecido en CSS.

En todos los programas serios  de edición de código hay un icono u opción para utilizar expresiones regulares en las búsquedas.  Para ello se utilizan una serie de caracteres que tienen un significado especial para acotar los resultados. Es un tema bastante complejo, a poco que refinas una búsqueda te encuentras con un chorro de caracteres que suena a klingon, pero con estos tips espero que sirvan para introduciros en el tema o sacaros de algun apuro.

El . significa cualquier carácter 
Por ejemplo, tod.s nos encontraría todos, todas, y hasta los alternativos todxs y tod@s.

[a-z] : se usa para definir un rango de caracteres.
Por ejemplo, haciendo una busqueda con thumb_[0-2][0-9] Nos va a encontrar todos los textos que tengan thumb_ y luego dos dígitos en el rango indicado, como thumb_26 , thumb_00,  thumb_16 dejando fuera terminos como  thumb_38,  thumb_a5  etc.

+ significa que coincide una o más veces con el elemento anterior.
Por ejemplo la búsqueda anterior nos devolveria todos los ” thumb_” y dos dígitos OJO y SOLO DOS DIGITOS, no nos devolveria  thumb_4 porque tiene solo uno, ni thumb_124,  pero si lo hacemos con thumb_[0-9]+ nos encontraría thumb_1 , thumb_24,  thumb_235,  thumb_283745 etc.

| sirve para separar cadenas opcionales.
Por ejemplo, si queremos buscar todos los sitios donde aparezca la cadena configuración pero hay faltas de ortografía  podríamos usar configuraci(o|ó)n  y nos devolverá tanto las búsquedas que tengan acento como las que no.

 
(?) sirve para añadir opciones a la busqueda.
Por ejemplo, si queremos que en la búsqueda nos encuentre tanto mayusculas como minúsculas añadiremos 
(?i) de forma que (?i) bot(o|ó)n nos encontraría Boton, botÓn, botóN, etc.

\ significa escape, o sea, “no interpretes el carácter siguiente como un comando”
Pongamos que estamos buscando el carácter 
+, como hemos visto que + significa una o más veces, ponemos la barra invertida delante para que sepa que buscamos el carácter y no lo interprete , o si buscamos las aperturas de paréntesis haríamos \( 

\s representa un espacio en blanco y * significa cero o mas veces el elemento anterior
Pongamos que queremos buscar las propiedades de la  pseudoclase header, pero solo cuando sea un nombre de clase, no un tag, no queremos que nos encuentre <header> en el html, sino las clases header{ que están puestas inline , tendríamos que hacer header{  pero no sabemos si los css están correctamente formateados, a veces está como header{ o header { , luego haremos la búsqueda como header\s*{ y al buscar cero o más espacios entre el nombre y la llave nos los encontrará todos. Aun podría pasar por alto resultados, cuando la definición de header estuviera agrupada con otros nombres de clases de forma parecida a .container, header, footer { 

Ademas para rizar el tirabuzón, vamos a buscar todos los sitios donde header tenga la propiedad display:none para lo que nos hará falta usar unos caracteres de control más
\n \r son retorno de carro y retorno de linea y (?!loquesea) se utiliza para excluir
Para empezar, añadiremos los retornos de carro y de linea entre header y display:none, y los espacios opcionales  para esquivar el mal formateado
header(.|\r\n|\r|\n)*display\s:\snone
o sea,header más los caracteres, los retornos y saltos de linea que haya más display:none
El problema es que nos podría devolver esto porque se limita a encontrar un display:none que aparezca después de un txtprecio: sin importar lo que tengan en medio.

header{ display:block}

 .logo{display:none}  
Para evitarlo, le decimos que no nos devuelva los resultados si en medio no hay un } que cierre la clase.
txtprecio((?!}).|\r\n|\r|\n)*display\s:\snone

Esto nos encontraría todos los casos de los que hablábamos antes, incluyendo cuando header se encuentra en una lista de clases compartiendo propiedades.

Todo esto apenas es la punta del iceberg, hay muchos mas carácteres de control, para ampliar la información podéis empezar por:

Y algunos sitios donde poder probar los regex de forma que pegais un texto y al escribir una regex os iluminará las coincidencias para experimentar  interactivamente.