Transformers de Shiki: líneas, foco, diff y palabras
Shiki no solo colorea: expone su árbol HAST como punto de extensión, y sobre él corren transformadores que anotan el resultado. Con el paquete @shikijs/transformers marcas líneas resaltadas, líneas en foco, líneas añadidas o eliminadas al estilo diff, y palabras concretas, todo mediante comentarios de notación que viven en el propio código. Se enchufan en shikiConfig.transformers y su apariencia final la decide tu CSS.
Colorear el código es solo la mitad de lo que la buena documentación técnica necesita. La otra mitad es dirigir la mirada: resaltar la línea que importa, atenuar lo que distrae, marcar lo que se añadió y lo que se quitó, subrayar una palabra concreta. Shiki no cierra la puerta tras tokenizar; expone su árbol de nodos como un punto de extensión, y sobre él corren los transformadores: funciones que recorren ese árbol y lo anotan con clases y atributos. Con ellos, un bloque de código deja de ser una foto fija y se vuelve una explicación con énfasis, sin dejar de resolverse íntegramente en el servidor.
- Entender un transformer como un recorrido sobre el árbol HAST que anota nodos.
- Marcar líneas resaltadas, en foco y de tipo diff con comentarios de notación.
- Resaltar palabras concretas y rangos de líneas por meta.
- Enchufar
transformersenshikiConfigy dar apariencia a sus clases con CSS.
Qué es un transformer
Después de tokenizar el código, Shiki construye un árbol HAST —una representación en árbol del HTML que va a emitir, con un nodo por cada línea y por cada fragmento coloreado—. Un transformer es una función que recibe ese árbol y lo modifica antes de que se convierta en cadena: añade una clase a una línea, envuelve una palabra, marca un nodo con un atributo. No cambia los colores; añade estructura sobre la que después actuará tu CSS.
Los transformadores más útiles vienen en el paquete @shikijs/transformers, que instalas aparte y del que importas justo los que necesites. Cada uno reconoce una notación —un comentario especial que escribes en el código— y la traduce en una anotación del árbol, eliminando el comentario del resultado visible.
// astro.config.mjs
import { defineConfig } from 'astro/config';
import {
transformerNotationHighlight,
transformerNotationFocus,
transformerNotationDiff,
transformerNotationWordHighlight,
} from '@shikijs/transformers';
export default defineConfig({
markdown: {
shikiConfig: {
transformers: [
transformerNotationHighlight(),
transformerNotationFocus(),
transformerNotationDiff(),
transformerNotationWordHighlight(),
],
},
},
});
Notaciones: highlight, focus, diff y palabra
La gracia de las notaciones es que viven dentro del código, como comentarios, de modo que la intención viaja con el propio fragmento y sobrevive a un copiar y pegar. Cada transformer reconoce la suya.
El más común es el resaltado de línea: un comentario // [!code highlight] al final de una línea marca esa línea con la clase .highlighted. El de foco, // [!code focus], marca la línea con .focused y pone .has-focused en el <pre>, para que puedas difuminar todo lo demás. Y el de diff, // [!code ++] y // [!code --], marca líneas añadidas y eliminadas con .diff.add y .diff.remove, imitando la lectura de un control de versiones.
const config = cargar();
const puerto = 8080; // [!code highlight]
config.legacyPort = 80; // [!code --]
config.port = puerto; // [!code ++]
arrancar(config);
En ese bloque, la línea del puerto queda resaltada, la línea vieja aparece marcada como eliminada y la nueva como añadida; los comentarios de notación no se ven en la salida, porque el transformer los consume. El de palabra, // [!code word:puerto], resalta cada aparición de puerto en el bloque con la clase .highlighted-word, útil para señalar un identificador concreto sin resaltar la línea entera.
Un transformer de notación solo añade clases; no da ningún color por sí mismo. Si activas transformerNotationHighlight pero no escribes CSS para .highlighted, no verás nada distinto, porque la clase existe pero nadie la estiliza. Marcar y pintar son dos pasos separados a propósito: el transformer pone la etiqueta, tu CSS decide qué significa visualmente esa etiqueta.
Rangos por meta y transformers en <Code />
No toda marca cabe bien en un comentario dentro de la línea. A veces quieres resaltar un rango —“las líneas 2 a 4”— desde fuera del código, y para eso está el transformer de meta, transformerMetaHighlight, que lee la cadena que escribes tras el lenguaje en la valla. Con él activado, abrir una valla con js seguido de {2,4-6} resalta esas líneas sin ensuciar el código con comentarios.
Los transformadores no son exclusivos de la configuración global: <Code /> acepta una prop transformers para aplicarlos por instancia, y una prop meta para pasarle esa misma cadena de rango que en una valla iría tras el lenguaje.
---
import { Code } from 'astro:components';
import { transformerMetaHighlight } from '@shikijs/transformers';
---
<Code
code={`linea uno\nlinea dos\nlinea tres`}
lang="txt"
meta="{2}"
transformers={[transformerMetaHighlight()]}
/>
La diferencia de alcance es la misma que ya conoces: lo que pones en shikiConfig.transformers cubre cada bloque del sitio, mientras que lo que pasas a un <Code /> concreto vale solo para esa instancia. Se combinan bien: una política global para lo habitual, y ajustes locales donde un bloque pida algo especial.
El CSS que da vida a las clases
Las clases que los transformadores añaden no significan nada hasta que tu CSS las interpreta. Este es el paso que muchos olvidan y por el que “no funciona el resaltado”: el marcado está, pero falta el estilo. Escribirlo es directo, apuntando a .astro-code y a las clases que cada transformer genera.
/* Linea resaltada: una banda de fondo que ocupa todo el ancho */
.astro-code .highlighted {
background-color: #ffffff14;
display: inline-block;
width: 100%;
}
/* Diff: verde para lo anadido, rojo atenuado para lo eliminado */
.astro-code .diff.add { background-color: #a6e3a11a; }
.astro-code .diff.remove { background-color: #f38ba81a; opacity: 0.6; }
/* Foco: difumina lo que no esta en foco cuando hay foco */
.astro-code.has-focused .line:not(.focused) {
filter: blur(2px);
opacity: 0.5;
transition: filter 0.2s, opacity 0.2s;
}
/* Palabra resaltada: un realce con esquinas suaves */
.astro-code .highlighted-word {
background-color: #f9e2af33;
border-radius: 3px;
padding: 0 2px;
}
flowchart TD TOK[Shiki tokeniza el codigo] --> HAST[construye el arbol HAST] HAST --> TR[los transformers recorren y anotan] TR --> CLS[clases highlighted focused diff add] CLS --> HTML[HTML con clases pero sin estilo] HTML --> CSS[tu CSS pinta cada clase] style TR fill:#89b4fa,color:#11111b style CSS fill:#a6e3a1,color:#11111b
Lo que Shiki hace con los transformadores es, en miniatura, exactamente lo que hace un compilador moderno, y verlo así eleva la técnica de un truco de documentación a un principio de diseño. Un compilador no traduce el texto de entrada directamente al código de salida; lo pasa primero a una representación intermedia, una estructura de datos que captura el significado del programa despojado de su forma superficial, y sobre esa representación ejecuta una serie de pasadas, cada una una transformación pequeña, independiente y componible que anota, optimiza o reescribe el árbol antes de emitir el resultado final. El árbol HAST de Shiki es precisamente esa representación intermedia: el código ya no es texto y todavía no es HTML, sino una estructura sobre la que se puede razonar y operar. Y cada transformer es una pasada: una función que entra al árbol, hace una cosa —marcar las líneas resaltadas, distinguir lo añadido de lo eliminado, envolver una palabra— y sale, sin saber ni importarle qué pasadas corrieron antes o correrán después. De esa arquitectura brotan tres virtudes que reconocerás de cualquier sistema bien diseñado. La primera es la composición: como cada pasada es independiente, las apilas en el orden que quieras y su efecto se acumula sin que se estorben. La segunda es la separación de fases: el transformer se ocupa del qué —qué líneas importan, qué se cambió— y lo deja anotado como intención semántica, mientras que el cómo se ve —el color de la banda, el grado del difuminado— se posterga por completo al CSS, la capa de presentación. Por eso la notación vive en un comentario dentro del código y no en un atributo de estilo: la intención pertenece al lado del programador que escribe el ejemplo, y la apariencia pertenece al lado del diseñador que define el tema, y ninguno pisa al otro. La tercera es la apertura: al exponer su árbol en lugar de encerrarlo, Shiki convierte el resaltado de una función cerrada en una tubería extensible, donde cualquiera puede escribir una pasada nueva para una necesidad que sus autores nunca previeron. Cuando dejas de ver un transformer como “una manera de subrayar líneas” y empiezas a verlo como “una pasada sobre una representación intermedia”, has adquirido una lente que sirve para leer compiladores, procesadores de documentos, tuberías de datos y la mitad de la infraestructura seria que existe. La humilde línea resaltada resulta ser una puerta a cómo se construyen los sistemas que transforman información por etapas.
- Instala
@shikijs/transformersy activa enshikiConfig.transformersel resaltado de línea, el foco y el diff. - Escribe un bloque con
// [!code highlight],// [!code ++]y// [!code --], y añade el CSS para.highlighted,.diff.addy.diff.remove. - Añade
.has-focused .line:not(.focused)con un difuminado y marca una línea con// [!code focus]para comprobar el efecto de atenuar el resto. - Usa
// [!code word:algo]para resaltar un identificador y razona por qué la intención vive en un comentario del código mientras la apariencia vive en tu CSS.