El tiempo no solo sirve para poner las cosas en sitio, sino que revela patrones y costumbres que de otro modo permanecerían ocultos. Es como uno de los usos de la inteligencia artificial: sacar patrones allí donde el ojo humano no llega. Eso es precisamente lo que he descubierto este verano, unos comportamientos cíclicos que, preferencias personales a parte, son cuanto menos curiosos. A saber:
- Casi siempre escribo en verano. Tiene sentido, porque es cuando dispongo de algo más de tiempo.
- Casi siempre escribo sobre Jekyll o este blog. Este es más difícil de dilucidar, pero parece que últimamente todos mis registros están diseñados para documentar algún comportamiento de este blog y su tecnlogía.
- Casi siempre escribo sobre tecnología. Y es que me gusta tanto el método científico como la pluma seca del escritor. Rara avis.
¿Qué vamos a hacer hoy?
Un plugin para Jekyll que nos permita copiar los bloques de código que introduzco. Ya sabes, como el que aparece en ChatGPT y en muchos otros sitios. Para poder llevar a cabo este proyecto vamos a hacer uso de las siguiente tecnologías:
- Ruby, para programar el plugin de Jekyll.
- JavaScript, para añadir la lógica de copiar el contenido al portapapeles.
- Sass, para el diseño.
- HTML, para armar la estructura del nuevo elemento.
Antes de entrar en materia es necesario aclarar algunos conceptos básicos, y es que, para modificar algo necesitamos conocer qué es lo que vamos a modificar. Nuestro punto de partida es este blog, que usa Jekyll y markdown. Internamente, Jekyll usa rouge, que es un «destacador» de sintaxis para Ruby. De esta manera, cuando nosotros escribimos un bloque de código usando las comillas, el sistema usa rouge para resaltar la sintaxis. Usará distintas combinaciones de colores en función del lenguaje especificado. Como no podemos acceder a rouge, la idea es añadir un elemento justo encima1 de ese trozo de código que contenta el botón para copiar el contenido. Algo como esto:
Manos a la obra
Ahora sí: estamos en condiciones de picar código. Lo primero que vamos a hacer será crear la estructura del include que vamos a utilizar. Crearemos un fichero al que llamaremos code_header.html
ubicado en _includes/
. El contenido… pues helo aquí:
<div class="code-header">
<button class="copy-code-button">
<svg class="copy-icon" xmlns="http://www.w3.org/2000/svg" shape-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" viewBox="0 0 438 511.52" xmlns:v="https://vecta.io/nano"><path fill-rule="nonzero" d="M141.44 0h172.68c4.71 0 8.91 2.27 11.54 5.77L434.11 123.1a14.37 14.37 0 0 1 3.81 9.75l.08 251.18c0 17.62-7.25 33.69-18.9 45.36l-.07.07c-11.67 11.64-27.73 18.87-45.33 18.87h-20.06c-.3 17.24-7.48 32.9-18.88 44.29-11.66 11.66-27.75 18.9-45.42 18.9H64.3c-17.67 0-33.76-7.24-45.41-18.9C7.24 480.98 0 464.9 0 447.22V135.87c0-17.68 7.23-33.78 18.88-45.42C30.52 78.8 46.62 71.57 64.3 71.57h12.84V64.3c0-17.68 7.23-33.78 18.88-45.42C107.66 7.23 123.76 0 141.44 0zm30.53 250.96c-7.97 0-14.43-6.47-14.43-14.44 0-7.96 6.46-14.43 14.43-14.43h171.2c7.97 0 14.44 6.47 14.44 14.43 0 7.97-6.47 14.44-14.44 14.44h-171.2zm0 76.86c-7.97 0-14.43-6.46-14.43-14.43 0-7.96 6.46-14.43 14.43-14.43h136.42c7.97 0 14.43 6.47 14.43 14.43 0 7.97-6.46 14.43-14.43 14.43H171.97zM322.31 44.44v49.03c.96 12.3 5.21 21.9 12.65 28.26 7.8 6.66 19.58 10.41 35.23 10.69l33.39-.04-81.27-87.94zm86.83 116.78-39.17-.06c-22.79-.35-40.77-6.5-53.72-17.57-13.48-11.54-21.1-27.86-22.66-48.03l-.14-2v-64.7H141.44c-9.73 0-18.61 4-25.03 10.41C110 45.69 106 54.57 106 64.3v319.73c0 9.74 4.01 18.61 10.42 25.02 6.42 6.42 15.29 10.42 25.02 10.42H373.7c9.75 0 18.62-3.98 25.01-10.38 6.45-6.44 10.43-15.3 10.43-25.06V161.22zm-84.38 287.11H141.44c-17.68 0-33.77-7.24-45.41-18.88-11.65-11.65-18.89-27.73-18.89-45.42v-283.6H64.3c-9.74 0-18.61 4-25.03 10.41-6.41 6.42-10.41 15.29-10.41 25.03v311.35c0 9.73 4.01 18.59 10.42 25.01 6.43 6.43 15.3 10.43 25.02 10.43h225.04c9.72 0 18.59-4 25.02-10.43 6.17-6.17 10.12-14.61 10.4-23.9z"/></svg>
Copy
</button>
<svg class="windows-control-buttons" xmlns="http://www.w3.org/2000/svg" width="85.693" height="20.538" viewBox="0 0 22.673 5.434" fill="none" stroke="#000" xmlns:v="https://vecta.io/nano"><g stroke-width=".437"><path d="M8.839.219h4.854v4.854H8.839z"/><path d="M.219 2.646h4.854" stroke-linecap="square"/></g><g stroke-linecap="round" stroke-width=".463"><path d="M17.613.373l4.829 4.829"/><path d="M22.442.373l-4.829 4.829"/></g></svg>
</div>
Observará el querido lector varia parafernalia numérica. No hay de qué preocuparse. Se trata de un simple antojo por mi parte, y es que he decidido añadir alguna que otra iconografía. Con esto conseguimos tener un icono de copiar y otro con los botones de control de ventanas, a modo placebo, eso sí.
Pues easy peasy, que dirían los ingleses.
Ya estaríamos en disposición de usar este nuevo include en cada post que escribiéramos. ¿Cómo? Insertando la siguiente etiqueta justo antes de nuestro bloque de código:
{% include code_header.html %}
Pero claro, el trabajo del programador es desgastar su vida en automatizar todas las tareas posibles, así que eso es precisamente lo que vamos a hacer. Crearemos un plugin para cuando detecte un bloque de código y añada la línea anterior.
El plugin
Venga, que esta es cuasi fácil. Lo de fácil, está claro: creamos un fichero llamado copy_code.rb
ubicado en la carpeta _plugins/
. Lo de cuasi, por el contenido:
Jekyll::Hooks.register :posts, :pre_render do |post, payload|
post.content.gsub!(/(```(?:(\w+))?\s+(?:(?:{% [REPLACE_ME_START] %}(.*?){% [REPLACE_ME_END] %})|([^`]*))\s+```)/m, '{% include code_header.html %}\1')
end
¡Importante! Este código debe ser modificado, ya que las etiquetas
[REPLACE_ME_START]
y[REPLACE_ME_END]
, como muy bien indica su nombre, deben ser reemplazadas por la etiqueta que escape el contenido de Liquid. En mi caso, el reemplazo habría que hacerlo por el valorraw
yendraw
respectivamente.
No es tanto por la extensión que por la expresión (regular, se entiende). Si sabes qué quiere decir, genial. Si no, tienes una magnífica oportunidad para aprender expresiones regulares, o para acudir a ChatGPT. Como última opción (que no se vuelva a repetir), diré que el código de arriba simplemente detecta bloques de código y les concatena el include que habíamos visto antes. Pereza a la máxima expresión.
El JavaScript
Hacemos una parada en este viaje creando un fichero copy_code.js
situado en js/
2. Aquí radica la lógica necesaria, no solo del copiado, sino también para informar de que el texto ha sido copiado y para volver a su estado original pasados unos segundos.
const codeBlocks = document.querySelectorAll('.code-header + .highlighter-rouge');
const copyCodeButtons = document.querySelectorAll('.copy-code-button');
copyCodeButtons.forEach((copyCodeButton, index) => {
const code = codeBlocks[index].innerText.trimEnd();
copyCodeButton.addEventListener('click', () => {
// Copy the code to the user's clipboard
window.navigator.clipboard.writeText(code);
// Update the button text visually
const { innerText: originalText, innerHTML: svgContent } = document.querySelector('.copy-code-button');
copyCodeButton.innerText = '✓ Copied!';
// (Optional) Toggle a class for styling the button
copyCodeButton.classList.add('copied');
// After 2 seconds, reset the button to its initial UI
setTimeout(() => {
copyCodeButton.innerHTML = svgContent;
copyCodeButton.classList.remove('copied');
}, 1500);
});
});
Hay poco que mencionar, la verdad. Simplemente selecciona, por pares, un bloque de código con un bloque de header (donde está el botón de copiar). Luego le asigna listener’s y programa el comportamiento.
El CSS
Terminamos la odisea con estilo, concretamente, el fruto de compilar nuestro copy_button.scss
, perteneciente al directorio _sass
. Esta parte es muy personal, así que probablemente a ti no te funcione con un simple copiapega. Deberías modificarlo para que se ajuste a tus gustos y necesidades.
@import "uno";
$highlight_color: #99a0a8;
.code-header {
background: #1d2630;
padding: 10px;
width: 95%;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
border-bottom-right-radius: 0px;
border-bottom-left-radius: 0px;
border-bottom: solid 1px $highlight_color;
clear: both;
overflow: hidden;
display: flex;
align-items: center;
justify-content: space-between;
}
.copy-code-button {
float: left;
background-image: none;
background-color: transparent;
color: $highlight_color;
border: none;
font-size: 0.8em;
font-family: sans-serif;
margin-right: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
}
.copy-icon {
fill: $highlight_color;
height: 16px;
width: 16px;
}
.windows-control-buttons {
float: right;
stroke: $highlight_color;
width: 51px;
height: 12px;
}
Conclusiones
Ha sido interesante descubrir mis propios comportamientos y recorrer estos pasos. Me ha gustado salir un poco de Android y meterme en Ruby, JavaScript y Sass (¡ojo!, solo un poco). También he aportado valor a este blog, tanto por contenido como por funcionalidad. Así que sí, me voy contento a por un café. Mejor una cerveza, que estamos en verano 🍺.