Desde hace algún tiempo he dejado de usar gist.github.com para embeber código fuente en mi blog y en su lugar he incrustado el código directamente.
Utilicé los Gists de GitHub por mucho tiempo y muchísimos posts lo contenían, pero en días anteriores me decidí a dejar de usarlo para hacer mi sitio más rápido, así que hoy vengo a compartir cómo es que hice la migración.
Dentro de mi blog tenía varios Gists que eran simples URLS con el formato "%https://gist.github.com/parzibyte/%"
. Entonces para removerlos, lo que tenía que hacer es:
Hice todo este proceso con PHP, guardando el contenido original y los detalles en una base de datos SQLite3 que más adelante me sirvió para saber cuáles posts había actualizado y así actualizar la fecha de actualización, ya que lo olvidé en el primer paso.
Para este punto bastaba con hacer una consulta a la base de datos de WordPress buscando aquellos posts que estuvieran publicados y que contuvieran la URL previamente mencionada. Lo hice así:
function obtenerPosts()
{
$baseDeDatos = obtenerBdMySQL();
$consulta = 'select
ID,
post_content
from
wp_posts
where
post_type = "post"
and post_status = "publish"
and (
post_content like "%https://gist.github.com/parzibyte/%"
)
limit
100;';
return $baseDeDatos->query($consulta)->fetchAll(PDO::FETCH_OBJ);
}
Lo hice con 100 posts en cada paso, así no gastaba mucha memoria. Simplemente estoy consultando con where
leyendo wp_posts
donde el contenido del post (post_content
) cumpliera con la expresión regular de un Gist.
Observa que estoy obteniendo el ID y el contenido del post.
Una vez que tenía la lista de posts a actualizar, tenía que extraer los hashes de cada Gist, ya que un post podía contener uno o más gists. Lo hice con preg_replace_callback
ya que además de extraer cada hash tenía que consultar la API de GitHub para indicar el nuevo contenido:
function reemplazar($contenido, $idPost)
{
$patron = '/https:\/\/gist\.github\.com\/parzibyte\/([a-z0-9]+)/m';
return preg_replace_callback($patron, function ($coincidencias) use ($idPost) {
// Coincidencias tiene la URL completa del gist
// en el índice 0 y el hash en el índice 1
$hash = $coincidencias[1];
$fragmento = obtenerFragmentoDeCodigoSegunHash($hash);
if ($fragmento === null) {
throw new Exception("El fragmento es nulo");
}
registrarContenidoDeGist($hash, $fragmento, $idPost);
return $fragmento;
}, $contenido);
}
De este modo, cada URL de un Gist era reemplazado por su contenido gracias a la función obtenerFragmentoDeCodigoSegunHash
que veremos a continuación. En dicha función es en donde se hace el consumo de la API de GitHub.
Veamos ahora la función para obtener el fragmento de código según el hash de su Gist. En este caso GitHub tiene una API muy bien documentada y que puede consumirse totalmente gratis, incluso sin autentificación.
Primero intenté consumirla sin ningún token, pero había un límite, así que me decidí por conseguir un Personal Access Token y enviarlo en el encabezado de la petición. Todo eso dentro de la función para obtener el contenido de un Gist con la API:
function obtenerContenidoDeGistConApi($hash)
{
$url = "https://api.github.com/gists/$hash";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'User-Agent: parzibyte',
'Authorization: Bearer github_pat_123456798',
]);
$output = curl_exec($ch);
$response = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($response === 200) {
return json_decode($output);
}
printf("Código %d al consultar %s\n", $response, $url);
return null;
}
Estoy usando cURL dentro de PHP para consumir la API de GitHub, ya que viene integrado en la mayoría de entornos y es una herramienta totalmente probada. La función que acabas de ver es la encargada de obtener los detalles del Gist, pero la siguiente es la que indica el código HTML con todo y clase para el lenguaje de programación:
function obtenerFragmentoDeCodigoSegunHash($hash)
{
$detalles = obtenerContenidoDeGistConApi($hash);
if ($detalles === null) {
return null;
}
$archivos = $detalles->files;
$fragmentoDeCodigo = "";
foreach ($archivos as $nombreArchivo => $archivo) {
$contenido = $archivo->content;
$truncado = $archivo->truncated;
if ($truncado) {
return null;
}
$fragmentoDeCodigo .= sprintf(
'<pre><code class="%s">%s</code></pre>',
obtenerClaseDeHTMLSegunExtension(pathinfo($nombreArchivo, PATHINFO_EXTENSION)),
htmlspecialchars($contenido)
);
}
return $fragmentoDeCodigo;
}
Como puedes ver tengo la función que obtiene la clase del HTML según la extensión del archivo devuelta por la API de GitHub, esta función es un simple diccionario que tiene todas las posibles clases según la extensión.
Por cierto, como el contenido de WordPress es HTML, hay que escapar el código con htmlspecialchars
antes de regresar el fragmento de código. Esta función en específico es la que se encarga de convertir una URL a una cadena HTML que tiene un pre
y un code
.
Una vez que tenía el nuevo contenido a partir de la API, hacía la actualización en la base de datos de WordPress, registrando cada acción:
function actualizarPost($contenido, $id)
{
$baseDeDatos = obtenerBdMySQL();
$sentencia = $baseDeDatos->prepare("UPDATE wp_posts SET post_content = ? WHERE ID = ?");
return $sentencia->execute([$contenido, $id]);
}
Finalmente la función que hace todo el proceso e invoca a las otras funciones es la siguiente:
function reemplazarTodos()
{
$posts = obtenerPosts();
foreach ($posts as $post) {
$contenidoAnterior = $post->post_content;
$idPost = $post->ID;
try {
printf("Reemplazando con ID %d...", $idPost);
$nuevo = reemplazar($contenidoAnterior, $idPost);
registrarReemplazo($idPost, $contenidoAnterior, $nuevo);
actualizarPost($nuevo, $idPost);
printf("OK. Se puede ver en https://parzibyte.me/blog/?p=%d\n", $idPost);
} catch (Exception $e) {
printf("No se pudo reemplazar\n");
}
}
}
Te he mostrado el código más importante, pero si quieres ver el código fuente completo lo dejaré a continuación. Toma en cuenta que en este caso he dejado el diccionario de extensiones de archivo truncado, ya que si lo dejo completo sería muy largo, pero puedes modificarlo según tus necesidades.
<?php
function obtenerClaseDeHTMLSegunExtension($extension)
{
$diccionario = [
'html' => 'language-markup',
'xml' => 'language-markup',
];
// Comprobación para regresar una cadena vacía si la extensión no está en el diccionario
if (!isset($diccionario[$extension])) {
return 'language-none';
}
return $diccionario[$extension];
}
function obtenerFragmentoDeCodigoSegunHash($hash)
{
$detalles = obtenerContenidoDeGistConApi($hash);
if ($detalles === null) {
return null;
}
$archivos = $detalles->files;
$fragmentoDeCodigo = "";
foreach ($archivos as $nombreArchivo => $archivo) {
$contenido = $archivo->content;
$truncado = $archivo->truncated;
if ($truncado) {
return null;
}
$fragmentoDeCodigo .= sprintf(
'<pre><code class="%s">%s</code></pre>',
obtenerClaseDeHTMLSegunExtension(pathinfo($nombreArchivo, PATHINFO_EXTENSION)),
htmlspecialchars($contenido)
);
}
return $fragmentoDeCodigo;
}
function reemplazar($contenido, $idPost)
{
$patron = '/https:\/\/gist\.github\.com\/parzibyte\/([a-z0-9]+)/m';
return preg_replace_callback($patron, function ($coincidencias) use ($idPost) {
// Coincidencias tiene la URL completa del gist
// en el índice 0 y el hash en el índice 1
$hash = $coincidencias[1];
$fragmento = obtenerFragmentoDeCodigoSegunHash($hash);
if ($fragmento === null) {
throw new Exception("El fragmento es nulo");
}
registrarContenidoDeGist($hash, $fragmento, $idPost);
return $fragmento;
}, $contenido);
}
function extraerHashDeGist($texto)
{
$patron = '/https:\/\/gist\.github\.com\/parzibyte\/([a-z0-9]+)/m';
$coincidencias = array();
preg_match_all($patron, $texto, $coincidencias, PREG_OFFSET_CAPTURE);
return $coincidencias;
}
function obtenerContenidoDeGistConApi($hash)
{
$url = "https://api.github.com/gists/$hash";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'User-Agent: parzibyte',
'Authorization: Bearer github_pat_123456798',
]);
$output = curl_exec($ch);
$response = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($response === 200) {
return json_decode($output);
}
printf("Código %d al consultar %s\n", $response, $url);
return null;
}
function obtenerBd()
{
$baseDeDatos = new PDO("sqlite:" . __DIR__ . "/reemplazos.db");
$baseDeDatos->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
return $baseDeDatos;
}
function crearTablas()
{
$bd = obtenerBd();
$bd->exec("CREATE TABLE IF NOT EXISTS reemplazos
(idPost INTEGER,
contenidoAnterior TEXT,
contenidoNuevo TEXT
);");
$bd->exec("CREATE TABLE IF NOT EXISTS contenidos_gists
(hash TEXT,
contenido TEXT,
idPost INTEGER
);");
}
function registrarReemplazo($idPost, $contenidoAnterior, $contenidoNuevo)
{
$baseDeDatos = obtenerBd();
$sentencia = $baseDeDatos->prepare("INSERT INTO reemplazos(idPost, contenidoAnterior, contenidoNuevo) VALUES (?, ?, ?)");
$sentencia->execute([$idPost, $contenidoAnterior, $contenidoNuevo]);
}
function registrarContenidoDeGist($hash, $contenido, $idPost)
{
$baseDeDatos = obtenerBd();
$sentencia = $baseDeDatos->prepare("INSERT INTO contenidos_gists(hash, contenido, idPost) VALUES (?, ?, ?)");
$sentencia->execute([$hash, $contenido, $idPost]);
}
function reemplazarTodos()
{
$posts = obtenerPosts();
foreach ($posts as $post) {
$contenidoAnterior = $post->post_content;
$idPost = $post->ID;
try {
printf("Reemplazando con ID %d...", $idPost);
$nuevo = reemplazar($contenidoAnterior, $idPost);
registrarReemplazo($idPost, $contenidoAnterior, $nuevo);
actualizarPost($nuevo, $idPost);
printf("OK. Se puede ver en https://parzibyte.me/blog/?p=%d\n", $idPost);
} catch (Exception $e) {
printf("No se pudo reemplazar\n");
}
}
}
function obtenerBdMySQL()
{
$contraseña = "hunter2";
$usuario = "parzibyte";
$nombre_base_de_datos = "wordpress";
return new PDO('mysql:host=localhost;dbname=' . $nombre_base_de_datos . ";charset=utf8mb4", $usuario, $contraseña, [
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4"
]);
}
function actualizarPost($contenido, $id)
{
$baseDeDatos = obtenerBdMySQL();
$sentencia = $baseDeDatos->prepare("UPDATE wp_posts SET post_content = ? WHERE ID = ?");
return $sentencia->execute([$contenido, $id]);
}
function obtenerPosts()
{
$baseDeDatos = obtenerBdMySQL();
$consulta = 'select
ID,
post_content
from
wp_posts
where
post_type = "post"
and post_status = "publish"
and (
post_content like "%https://gist.github.com/parzibyte/%"
)
limit
100;';
return $baseDeDatos->query($consulta)->fetchAll(PDO::FETCH_OBJ);
}
function main()
{
crearTablas();
reemplazarTodos();
}
main();
Y de este modo es como actualicé 1803 posts que contenían 6117 gists. Ese proceso me habría llevado muchísimo tiempo de haberlo hecho a mano, pero al final gracias a la programación lo pude resolver en algunas horas.
Como lo dije al principio del post: cuando removí los Gists de mi blog, no guardé la nueva fecha de actualización. Gracias a que guardé todo el proceso en la base de datos de SQLite3, pude actualizar las fechas más adelante con la siguiente función de PHP:
function actualizarFechas()
{
$posts = obtenerPostsPreviamenteActualizados();
$bdMysql = obtenerBdMySQL();
foreach ($posts as $post) {
$ahora = date("Y-m-d H:i:s");
$ahoraGmt = gmdate("Y-m-d H:i:s");
$sentencia = $bdMysql->prepare("UPDATE wp_posts SET post_modified = ?, post_modified_gmt = ? WHERE id = ?");
$sentencia->execute([$ahora, $ahoraGmt, $post->id]);
printf("Guardando post con id %d. Su post_modified será %s y su post_modified_gmt será %s\n", $post->id, $ahora, $ahoraGmt);
guardarPostActualizado($post->id);
}
}
En este post te voy a enseñar a designar una carpeta para imprimir todos los…
En este artículo te voy a enseñar la guía para imprimir en una impresora térmica…
Hoy te voy a mostrar un ejemplo de programación para agregar un módulo de tasa…
Los usuarios del plugin para impresoras térmicas pueden contratar licencias, y en ocasiones me han…
Hoy voy a enseñarte cómo imprimir el € en una impresora térmica. Vamos a ver…
En este post te enseñaré a imprimir la letra ñ en una impresora térmica. Voy…
Esta web usa cookies.