Motor de plantillas Liquid
Los motores de plantillas se utilizan mucho hoy en día, tanto en sitios webs como en otras tareas. Entre los más usados, encontramos Handlebars y Liquid, siendo este segundo el usado de manera nativa por Jekyll. Vamos a ver cómo usarlo para personalizar la generación de nuestros sitios webs con Jekyll.
Al finalizar, sabrá:
-
Qué es un motor de plantillas.
-
Cómo usar Liquid en Jekyll.
-
Cómo utilizar expresiones, etiquetas y filtros para personalizar el contenido final.
-
Cómo controlar las líneas en blanco y los espacios en blanco extras añadidos por el motor de plantillas cuando genera el contenido final.
-
Algunos operadores definidos por Liquid.
Introducción
Una plantilla (template) no es más que un texto que sirve como referencia para obtener alguna cosa final. Estas plantillas contienen texto estricto que no debe sufrir modificaciones, pero también expresiones con las que personalizar el resultado final. Un motor de plantillas (template engine) es una aplicación que transforma plantillas a partir de unos datos dados. Estos motores utilizan su propio lenguaje. Por ejemplo, el lenguaje utilizado por Handlebars no es el mismo que el de Liquid. Tienen aspectos comunes, pero no son iguales.
Jekyll utiliza el motor de plantillas Liquid, https://shopify.github.io/liquid, el cual podemos utilizar para personalizar el resultado final de las páginas del sitio web estático generado, ya se encuentren en Markdown o HTML. Aquí, vamos a presentar los elementos más utilizados de este sistema de plantillas. Si ha trabajado con algún otro como, por ejemplo, Handlebars, y cree que podría haber alguna forma de hacer alguna cosa en Liquid de manera similar, consulte su sitio web oficial.
En NPM, existe el paquete liquidjs que permite su uso en proyectos de Node.js. Para Visual Studio Code, es recomendable la extensión Shopify Liquid.
Para Jekyll, todos los archivos Markdown y HTML ubicados en el directorio del sitio se consideran plantillas y como tales serán procesados. Esto es cierto a medias, tal y como veremos más adelante. Deben cumplir una condición, definir explícitamente lo que se conoce como un encabezamiento (front matter).
En Liquid, las plantillas pueden contener texto estricto, expresiones y etiquetas. El texto estricto (strict text) no es más que aquel que no admite interpretación, debe devolverse tal cual. Mientras que con las expresiones y las etiquetas podemos personalizar porciones de la plantilla. Todo aquello que no represente una expresión o una etiqueta se considera texto estricto.
Expresiones de Liquid
Una expresión (expression) no es más que un conjunto de operadores y operandos que se evalúan y proporcionan un valor, de manera similar a sus homólogas en lenguajes de programación como JavaScript o TypeScript. En Liquid, se delimitan entre un par de llaves dobles ({{ y }}), siguiendo el siguiente formato:
{{ expresión }}
El siguiente ejemplo muestra cómo insertar, en el resultado final, la fecha exacta en la que Jekyll generó el sitio:
{{ site.time }}
Así, por ejemplo, si tenemos la siguiente plantilla formada por texto estricto y una expresión:
Fecha de la última actualización: {{ site.time }}.
Si suponemos que la fecha y hora de la generación del sitio web es 29 de enero de 2023 a las 20:34, el resultado final obtenido será:
Fecha de la última actualización: 29-01-2023 20:34.
Las expresiones pueden acceder a datos, los cuales pueden ser textos, números, booleanos, objetos, listas o nil. Sus literales se pueden indicar como sigue:
"Los textos literales se indican entre comillas dobles"
123
456.789
true
false
nil
Recordemos que un literal es un valor estricto de un determinado tipo de datos, el cual puede aparecer en una expresión de Liquid.
En el caso de objetos, el acceso a sus propiedades o campos se hace con el operador punto, tal y como vimos en el objeto anterior para obtener la fecha de generación del sitio web. En el caso de las listas, utilice el operador de indexación ([ y ]), donde el primer elemento es el cero (0) como en C/C++, Go, JavaScript y TypeScript. Ejemplo:
{{ site.pages[0] }}
Se puede utilizar índices negativos donde el -1 indica el último.
Etiquetas
Una etiqueta (tag) es un elemento de lógica con el que condicionar el resultado. Se delimitan por {% y %}. Al igual que otros motores de plantillas como Handlebars, Liquid también permite iterar o condicionar contenido, por ejemplo, mediante elementos {% if %} o {% unless %}.
Etiquetas condicionales
Una etiqueta condicional (conditional tag) es aquella que indica una condición que debe cumplirse para generar lo indicado, de manera similar a las sentencia if de la mayoría de los lenguajes de programación. Se declaran como sigue:
{% if expresión %}
contenido a añadir si se cumple la expresión
{% endif %}
{% unless expresión %}
contenido a añadir si NO se cumple la expresión
{% endunless %}
El elemento {% if %} puede contener cláusulas elsif y else como sigue:
{% if expresión %}
contenido
{% elsif expresión %}
contenido
{% else %}
contenido
{% endif %}
En una etiqueta, cuando se indica expresión, la expresión no se delimita entre llaves.
En una condición, Liquid considera como verdadero cualquier valor salvo nil y false. Por lo tanto, trata nil y false como falso. Las cadenas de texto vacías y el cero se tratan como verdadero porque no son ni nil ni false. No olvide esta pequeña, pero importante idiosincrasia de Liquid.
Etiquetas iterativas
Una etiqueta iterativa (iterative tag) sirve para generar contenido para cada elemento de una lista, de manera similar a los bucles de los lenguajes de programación. Para iterar por el valor de una variable, utilizaremos la etiqueta {% for %}:
{% for nombreVariable in expresión %}
Contenido a generar para cada elemento de la expresión.
Podemos acceder al elemento bajo iteración mediante una
expresión como, por ejemplo, {{ nombreVariable.nombrePropiedad }}.
{% else %}
Se generará este contenido si la expresión de iteración
devuelve una lista de tamaño cero.
{% endfor %}
El siguiente ejemplo itera entre los números 1, 2, 3, 4 y 5:
{% for item in (1..5) %}
{{ i }}
{% endfor %}
Otro ejemplo:
{% for doc in docs %}
{% if doc == page %}
<span class="page-link page-active">{{ doc.title | escape }}</span>
{% else %}
<a class ="page-link" href="{{ doc.url | relative_url }}">{{ doc.title | escape }}</a>
{% endif %}
{% endfor %}
En un bucle, se puede utilizar elementos {% break %} o {% continue %} con el mismo objeto que sus homólogos de JavaScript o TypeScript:
{% break %}
{% continue %}
Etiquetas de asignación
En ocasiones, podemos desear crear una variable de datos en la plantilla o bien asignarle un valor a una existente. Esto se hace mediante las etiquetas de asignación (assignation tags) que puede ser {% assign %} o {% capture %}:
{% assign nombreVariable = valor %}
{% capture nombreVariable %}
valor
{% endcapture %}
Si la variable existe, le cambiará su valor; en otro caso, la creará.
Etiqueta {% raw %}
Aunque no es lo habitual, es muy útil añadir contenido que no debe evaluar el motor de plantillas, por ejemplo, porque en su contenido podría haber texto similar a algún elemento de Liquid. Esto lo podemos hacer con un elemento {% raw %}:
{% raw %}
contenido no evaluable por Liquid
{% endraw %}
Propiedad render_with_liquid de los encambezamientos
Si no desea que Jekyll aplique Liquid a un determinado archivo de contenido, sin necesidad de {% raw %}, fije la propiedad render_with_liquid de su encabezamiento a false. Pero cuidado, sólo se tiene en cuenta desde la versión 4.0 de Jekyll.
Comentarios
Los comentarios se indican también mediante etiquetas como sigue:
{% comment %}
aquí el comentario
que puede extenderse en
varias líneas
{% endcomment %}
Filtros de Liquid
En Liquid, las expresiones pueden contener filtros. Un filtro (filter) es una operación que se aplica a un dato obtenido de una expresión. Puede verlo como una función que recibe un dato y lo transforma en otro. Por ejemplo, si deseamos ordenar una lista de elementos podríamos tener algo como lo siguiente:
{% assign tags = site.data.tags | sort %}
Los filtros siguen un formato similar a las tuberías (pipes) de shell.
El dato se copia a una pipe (|
) y de ahí lo toma el filtro, en el ejemplo anterior, sort.
Adicionalmente, se pueden pasar argumentos extras que se indicarán tras el nombre del filtro.
En el siguiente ejemplo, el primer argumento proviene de |
y el segundo, “price”, indica que se ordene por ese campo, observe los dos puntos (:
) que son necesarios si se pasan argumentos adicionales:
{% assign tags = site.data.products | sort: "price" %}
Es posible aplicar varios filtros, basta con llevar el resultado del anterior a otra |
.
{%- assign coll = site.collections | where: "label", page.collection | first -%}
{%- for doc in coll.docs -%}
<a class ="page-link" href="{{ doc.url | relative_url }}">{{ doc.title | escape }}</a>
{%- endfor -%}
Jekyll soporta los filtros estándares de Liquid, https://jekyllrb.com/docs/liquid/filters/#standard-liquid-filters y añade algunos más que pueden resultar muy útiles en ciertas ocasiones. Vamos a listar algunos de ellos.
Filtros textuales
Entre los filtros relacionados con valores de tipo texto, encontramos:
Filtro | Descripción |
---|---|
texto | upcase | Devuelve el texto en mayúsculas. |
texto | downcase | Devuelve el texto en minúsculas. |
texto | capitalize | Devuelve el texto con el primer carácter en mayúsculas y el resto en minúsculas. |
texto | escape | Devuelve el texto proporcionado escapado para que pueda ser usado como un URL. |
texto | escape_once | Devuelve el texto proporcionado escapado sin escapar las entidades ya escapadas. |
texto | size | Devuelve el tamaño de un texto. |
texto | slice: índice | Devuelve el elemento indicado por su índice, considerando el primer elemento como el de índice cero (0). |
texto | slice: inicio, fin | Devuelve los elementos que van desde el índice de inicio al de fin indicados. |
texto | relative_url | Devuelve la ruta relativa, añadiendo la base extraída de _config.yaml, para la ruta dada. |
texto | absolute_url | Devuelve la ruta absoluta, incluidos el dominio y la base, para la ruta dada. |
texto | number_of_words | Devuelve el número de palabras que tiene un determinado texto. |
texto | append: texto | Devuelve el texto que resulta de añadir uno dado al primero. |
texto | prepend: texto | Devuelve el texto que resulta de prefijar uno dado al primero. |
Filtros de lista
Filtro | Descripción |
---|---|
array | join | Concatena los elementos de un array y devuelve el texto resultante. |
array | join: “separador” | Ídem al anterior usando el separador indicado. |
array | first | Devuelve el primer elemento del array. |
array | last | Devuelve el último elemento del array. |
array | reverse | Devuelve el array invertido. |
array | size | Devuelve el tamaño del array. |
array | slice: índice | Devuelve el elemento indicado por su índice, considerando el primer elemento como el de índice cero (0). |
array | slice: inicio, fin | Devuelve los elementos que van desde el índice de inicio al de fin indicados. |
array | sort | Ordena y devuelve un array. |
array | sort: “campo” | Ordena un array de objetos por el valor del campo indicado. |
array | uniq | Devuelve un array sin los duplicados. |
array | where: “campo”, “campo” | Devuelve un array con aquellos elementos que tengan un valor para los campos indicados. |
array | concat: array | Devuelve un array con la concatenación de los indicados. |
Filtros de fecha
Filtro | Descripción |
---|---|
fecha | date | Devuelve un texto con la fecha formateada. |
fecha | date: “formato” | Ídem al anterior, pero siguiendo el formato indicado. |
Filtros matemáticos
Es posible utilizar algunos filtros para realizar operaciones matemáticas básicas:
Filtro | Descripción |
---|---|
número | minus: número | Devuelve la resta de dos números. |
número | plus: número | Devuelve la suma de dos números. |
número | divided_by: número | Devuelve la división de dos números. El valor devuelve es redondeado. |
número | modulo: número | Devuelve el resto de la división de dos números. |
número | times: número | Devuelve la multiplicación de dos números. |
número | abs | Devuelve el valor absoluto de un número. |
Veamos un ejemplo. Supongamos que deseamos conocer el tiempo de lectura del contenido de una página. Teniendo en cuenta que el promedio de palabras leídas por minuto es de 180, podemos calcular cuántos minutos necesitaremos para leer un determinado contenido como sigue:
Tiempo de lectura: {{ page.content | number_of_words | divided_by: 180 }} minutos
Otros filtros
Filtro | Descripción |
---|---|
valor | default: valor | Devuelve un valor dado si el proporcionado es nil, false o el texto vacío. |
Creación de listas en línea
A diferencia de algunos tipos de datos como, por ejemplo, los textos, los números y los booleanos, Liquid no permite crear listas literales. Hay que buscar una solución alternativa (workaround) con la que poder crearlas en la plantilla cuando sea necesario, sin necesidad de acudir a archivos de datos de Jekyll. La opción más sencilla es crear la lista a partir de un texto literal y el filtro split. Ejemplo:
{% assign lista = "uno::dos::tres::cuatro" | split: "::" %}
La idea es listar los elementos en un texto literal con un separador que sepamos que no tendrá ningún elemento y usarlo para descomponer el texto en una lista. En el ejemplo anterior, la lista consistirá en cuatro elementos cuyos valores son uno, dos, tres y cuatro.
Control del espacio en blanco
Existen ocasiones donde los espacios en blanco en los documentos generados pueden ser importantes porque pueden añadir espacios en blanco o saltos de líneas innecesarios o porque la añadidura de espacios en blanco o saltos de líneas extras puede dar al traste con un formato elegante y fácil de leer. Imagínese un archivo JSON donde aparecen saltos de línea o campos escalonados debido a espacios innecesarios como, por ejemplo:
{
"a": 1,
"b": 2,
"c": 3,
"d": 4
}
El control del espacio en blanco (whitespace control) está relacionado con la añadidura innecesaria de espacios en blanco o saltos de línea en el contenido final generado a partir de una plantilla. Liquid, al igual que otros motores de plantillas como Handlebars, proporciona mecanismos para tratar situaciones excepcionales que pueden conducir a la añadidura de espacios en blanco innecesarios.
Se consigue mediante el uso de un guion (-) en las expresiones y las etiquetas. Este guion puede aparecer tanto en el delimitador inicial, ya sea {{- o {%-, como en el final, -}} o -%}. Le indica al procesador de Liquid algo muy sencillo: suprime el salto de línea de la etiqueta o el espacio en blanco de la expresión. Como esto puede aparecer tanto a izquierda como a derecha, se podrá indicar tanto en el delimitador inicial como en el final según el caso. Por ejemplo, supongamos lo siguiente:
{% assign txt = "Texto a mostrar" %}
{{ txt }}
Según lo anterior, el procesador de Liquid añadirá un salto de línea tras el procesamiento de la etiqueta de asignación, justo antes del texto devuelto por la expresión. Si no queremos o necesitamos este salto, no tenemos más que finalizarla con -%}. El guion lo que le dice es que no añada el salto de línea que añadiría el delimitador de fin, o sea, %}:
{% assign txt = "Texto a mostrar" -%}
{{ txt }}
Ahora, imagine lo siguiente:
{% assign fullName = "Elvis Costello" %}
{% if fullName and fullName.size > 10 %}
Vaya, {{ fullName }}, tienes un nombre completo de más de 10 caracteres.
{% else %}
Tu nombre no es superior a los 10 caracteres.
{% endif %}
Volvería a pasar algo similar:
-
La expresión de asignación generaría una línea en blanco.
-
La cláusula if generaría otra línea en blanco.
-
Como se cumple la condición de la cláusula if, aparecería el texto Vaya….
-
A continuación, aparecería otra línea en blanco debido a la cláusula else.
Cómo se resuelve el problema, mediante el uso del guion supresor; por favor, preste espacial atención a su uso:
{% assign fullName = "Elvis Costello" -%}
{%- if fullName and fullName.size > 10 -%}
Vaya, {{ fullName -}}, tienes un nombre completo de más de 10 caracteres.
{%- else -%}
Tu nombre no es superior a los 10 caracteres.
{%- endif %}
Esto sólo debe tenerlo en cuenta si las líneas en blanco o los espacios en blanco generados por las etiquetas y las expresiones, respectivamente, pueden generarle problemas en el contenido final generado.
Operadores
En las expresiones de Liquid, es posible indicar tanto operadores como operandos. Los operadores son pocos, pero suficientes para resolver la mayoría de los casos:
Operador | Descripción |
---|---|
valor == valor | Comprueba si dos valores son iguales. |
valor != valor | Comprueba si dos valores son distintos. |
valor > valor | Comprueba si el primer valor es mayor que el segundo. |
valor >= valor | Comprueba si el primer valor es mayor o igual que el segundo. |
valor < valor | Comprueba si el primer valor es menor que el segundo. |
valor <= valor | Comprueba si el primer valor es menor o igual que el segundo. |
valor and valor | Realiza una comprobación lógica Y. |
valor or valor | Realiza una comprobación lógica O. |
lista contains valor | Comprueba si una lista contiene un determinado valor. |