Resaltado de sintaxis (¡y más!) con Prism en un sitio estático trucos CSS
Entonces, decidiste crear un blog con Siguiente.js. Como cualquier bloguero desarrollador, le gustaría tener fragmentos de código en sus publicaciones que estén bien formateados con resaltado de sintaxis. También es posible que desee mostrar los números de línea en fragmentos, e incluso puede llamar a ciertas líneas de código.
Esta publicación le mostrará cómo configurar esta configuración, así como algunos consejos y trucos para trabajar con estas otras funciones. Algunos de ellos son más difíciles de lo que cabría esperar.
Suposiciones
Usamos Iniciador de blog Next.js como base para nuestro proyecto, pero los mismos principios deben aplicarse a otros marcos. Este repositorio tiene instrucciones claras (y simples) para comenzar. Arma el blog y ¡vamos!
Otra cosa que usamos aquí es prisma.jsuna popular biblioteca de resaltado de sintaxis que incluso se usa aquí en CSS-Tricks. El comienzo del blog Next.js utiliza Nota para convertir markdown a markdown, entonces usaremos complemento de notas Prism.js para formatear nuestros fragmentos de código.
Integración básica de Prism.js
Comencemos integrando Prism.js en nuestro lanzador Next.js. Como ya sabemos que estamos usando el complemento remark-prism, lo primero que debe hacer es instalarlo con su administrador de paquetes favorito:
npm i remark-prism
ahora ve a markdownToHtml
archivo /lib
carpeta e incluir comentario-prisma:
import remarkPrism from "remark-prism";
// later ...
.use(remarkPrism, { plugins: ["line-numbers"] })
dependiendo de qué versión de nota-html que está utilizando, es posible que también deba cambiar su uso a .use(html, { sanitize: false })
.
Ahora todo el módulo debería verse así:
import { remark } from "remark";
import html from "remark-html";
import remarkPrism from "remark-prism";
export default async function markdownToHtml(markdown) {
const result = await remark()
.use(html, { sanitize: false })
.use(remarkPrism, { plugins: ["line-numbers"] })
.process(markdown);
return result.toString();
}
Agregar estilos y temas de Prism.js
Ahora importemos el CSS que Prism.js necesita para diseñar fragmentos de código pages/_app.js
archivo, importe la hoja de estilo principal Prism.js y la hoja de estilo para cualquier tema que desee usar. Uso el tema Tomorrow Night de Prism.js, así que lo que importo se ve así:
import "prismjs/themes/prism-tomorrow.css";
import "prismjs/plugins/line-numbers/prism-line-numbers.css";
import "../styles/prism-overrides.css";
Tenga en cuenta que también comencé un prism-overrides.css
hoja de estilo donde podemos cambiar algunas configuraciones predeterminadas. Esto será útil más adelante. Puede permanecer vacío por ahora.
Y con eso, ya tenemos algunos estilos básicos. El siguiente código en Markdown:
```js
class Shape {
draw() {
console.log("Uhhh maybe override me");
}
}
class Circle {
draw() {
console.log("I'm a circle! :D");
}
}
```
... debe estar bien formateado:
Añadir números de línea
Es posible que haya notado que el fragmento de código que generamos no muestra números de línea, aunque importamos el complemento que lo admite cuando importamos remark-prism. La solución está oculta en el LÉAME del prisma de comentarios:
Asegúrese de incluir el css apropiado en sus hojas de estilo.
En otras palabras, tenemos que forzar un .line-numbers
Clase CSS en el generado <pre>
etiqueta, que podemos hacer de la siguiente manera:
¡Y con eso ya tenemos los números de línea!
Tenga en cuenta que, según la versión de Prism.js que tengo y el tema Tomorrow Night que elegí, tuve que agregar esto a prism-overrides.css
archivo que comenzamos arriba:
.line-numbers span.line-numbers-rows {
margin-top: -1px;
}
Puede que no lo necesites, pero aquí está. ¡Tenemos números de línea!
líneas de subrayado
Nuestra próxima característica será un poco más de trabajo. Aquí es donde queremos la capacidad de subrayar o llamar a ciertas líneas de código en el fragmento.
Hay Complemento Prism.js para resaltar la línea; desafortunadamente, no está integrado con remark-prism. El complemento funciona analizando la posición del código formateado en el DOM y resaltando manualmente las líneas en función de esta información. Esto no es posible con el complemento remark-prism, ya que actualmente no se está ejecutando ningún complemento DOM. En última instancia, se trata de la generación de sitios estáticos. Next.js implementa nuestro Markdown a través de un paso de compilación y genera HTML para mostrar el blog. Todo este código Prism.js se ejecuta durante la generación de este sitio estático cuando no hay DOM.
¡Pero no tengas miedo! Hay una solución divertida que se ajusta exactamente a los trucos de CSS: podemos usar CSS simple (y una pizca de JavaScript) para enfatizar las líneas de código.
Permítanme aclarar que este es un trabajo no trivial. Si no necesita resaltar una línea, no dude en pasar a la siguiente sección. Pero si nada más, puede ser una demostración divertida de lo que es posible.
Nuestro principal CSS
Comencemos agregando el siguiente CSS al nuestro prism-overrides.css
hoja de estilo:
:root {
--highlight-background: rgb(0 0 0 / 0);
--highlight-width: 0;
}
.line-numbers span.line-numbers-rows > span {
position: relative;
}
.line-numbers span.line-numbers-rows > span::after {
content: " ";
background: var(--highlight-background);
width: var(--highlight-width);
position: absolute;
top: 0;
}
Predefinimos algunas propiedades personalizadas de CSS: color de fondo y ancho de resaltado. Por ahora, los establecemos en valores vacíos. Más tarde, sin embargo, estableceremos valores significativos de JavaScript para las líneas que quiero resaltar.
Luego establecemos el número de línea <span>
para position: relative
para que podamos agregar un ::after
pseudo-elemento de posicionamiento absoluto Este es el pseudo-elemento que usaremos para resaltar nuestras líneas.
Ahora agreguemos manualmente un atributo de datos a <pre>
etiqueta que se genera, luego lea esto en el código y use JavaScript para personalizar los estilos anteriores para resaltar líneas específicas de código. Podemos hacer esto de la misma manera que agregamos números de línea antes:
Esto causará nuestro <pre>
elemento a ser representado por un data-line="3,8-10"
atributo donde la línea 3 y la línea 8-10 están marcadas en el fragmento de código. Podemos separar los números de línea con comas o proporcionar rangos.
Veamos cómo podemos analizar esto en JavaScript y hacer que el resaltado funcione.
Leer líneas subrayadas
Dirigirse a components/post-body.tsx
Si este archivo es JavaScript para usted, siéntase libre de convertirlo a TypeScript (.tsx
), o simplemente ignorar todos mis escritos.
Primero, necesitaremos una pequeña importación:
import { useEffect, useRef } from "react";
Y tenemos que agregar un ref
a este componente:
const rootRef = useRef<HTMLDivElement>(null);
Luego lo aplicamos a root
elemento:
<div ref={rootRef} className="max-w-2xl mx-auto">
El siguiente código es un poco largo, pero no hace nada loco. Lo mostraré y luego lo revisaré.
useEffect(() => {
const allPres = rootRef.current.querySelectorAll("pre");
const cleanup: (() => void)[] = [];
for (const pre of allPres) {
const code = pre.firstElementChild;
if (!code || !/code/i.test(code.tagName)) {
continue;
}
const highlightRanges = pre.dataset.line;
const lineNumbersContainer = pre.querySelector(".line-numbers-rows");
if (!highlightRanges || !lineNumbersContainer) {
continue;
}
const runHighlight = () =>
highlightCode(pre, highlightRanges, lineNumbersContainer);
runHighlight();
const ro = new ResizeObserver(runHighlight);
ro.observe(pre);
cleanup.push(() => ro.disconnect());
}
return () => cleanup.forEach(f => f());
}, []);
Realizamos un efecto una vez que se muestra todo el contenido en la pantalla. Usamos querySelectorAll
tomar todo <pre>
elementos debajo de eso root
elemento; en otras palabras, cualquier entrada de blog que vea el usuario.
Para cada uno de ellos nos aseguramos de que haya <code>
elemento debajo de él y verifique tanto el contenedor de número de fila como el data-line
atributo. esto es lo que dataset.line
cheques Ver los documentos Para más información.
Si pasamos el segundo continue
entonces highlightRanges
es el conjunto de acentos que declaramos anteriormente, que en nuestro caso es "3,8-10"
donde lineNumbersContainer
es el recipiente con .line-numbers-rows
clase CSS.
Finalmente, declaramos un runHighlight
una función que llama a highlightCode
característica que te mostraré. Luego montamos un ResizeObserver
para ejecutar la misma función cada vez que nuestra publicación de blog cambia de tamaño, es decir si el usuario cambia el tamaño de la ventana del navegador.
EN highlightCode
función
Finalmente, veamos el nuestro. highlightCode
función:
function highlightCode(pre, highlightRanges, lineNumberRowsContainer) {
const ranges = highlightRanges.split(",").filter(val => val);
const preWidth = pre.scrollWidth;
for (const range of ranges) {
let [start, end] = range.split("-");
if (!start || !end) {
start = range;
end = range;
}
for (let i = +start; i <= +end; i++) {
const lineNumberSpan: HTMLSpanElement = lineNumberRowsContainer.querySelector(
`span:nth-child(${i})`
);
lineNumberSpan.style.setProperty(
"--highlight-background",
"rgb(100 100 100 / 0.5)"
);
lineNumberSpan.style.setProperty("--highlight-width", `${preWidth}px`);
}
}
}
Obtenemos cada rango y leemos el ancho de <pre>
Luego pasamos por cada rango, encontramos el número de línea correspondiente <span>
y establezca los valores de las propiedades CSS personalizadas para ellos. Establecemos qué color de resaltado queremos, y establecemos el ancho del total scrollWidth
El valor de <pre>
elemento. Solo lo guardé y lo usé. "rgb(100 100 100 / 0.5)"
pero siéntase libre de usar cualquier color que crea que se ve mejor para su blog.
Esto es lo que parece:
Marcar filas sin números de fila
Te habrás dado cuenta de que todo esto hasta ahora depende de la disponibilidad de números de línea. Pero, ¿y si queremos resaltar filas pero sin números de fila?
Una forma de implementar esto sería mantener todo igual y agregar una nueva opción para ocultar estos números de línea con CSS. Primero, agregaremos una nueva clase CSS, .hide-numbers
:
```js[class="line-numbers"][class="hide-numbers"][data-line="3,8-10"]
class Shape {
draw() {
console.log("Uhhh maybe override me");
}
}
class Circle {
draw() {
console.log("I'm a circle! :D");
}
}
```
Ahora agreguemos reglas CSS para ocultar los números de línea cuando .hide-numbers
aplica la clase:
.line-numbers.hide-numbers {
padding: 1em !important;
}
.hide-numbers .line-numbers-rows {
width: 0;
}
.hide-numbers .line-numbers-rows > span::before {
content: " ";
}
.hide-numbers .line-numbers-rows > span {
padding-left: 2.8em;
}
La primera regla anula el desplazamiento a la derecha de nuestro código base para dejar espacio para los números de línea. De forma predeterminada, la adición al tema de Prism.js que elegí es 1em
El complemento de número de línea lo aumenta a 3.8em
luego inserta números de fila con posicionamiento absoluto. Lo que hicimos devuelve el relleno a 1em
defecto.
La segunda regla toma el contenedor con los números de fila y lo aplasta para que no quede ancho. La tercera regla elimina todos los números de fila (se generan con ::before
pseudo elementos).
La última regla simplemente reemplaza el número de la línea ya vacía <span>
los elementos de nuevo donde deberían estar, para que el énfasis se pueda colocar como queramos. Nuevamente, para mi tema, los números de línea generalmente se agregan 3.8em
valor de la suma izquierda, que devolvimos al estándar 1em
.Estos nuevos estilos se suman a los otros 2.8em
entonces las cosas volvieron a donde deberían estar, pero con números de línea ocultos. Si usa diferentes complementos, es posible que necesite valores ligeramente diferentes.
Así es como se ve el resultado:
Función de copia del portapapeles
Antes de terminar, agreguemos un toque final: un botón que le permite a nuestro querido lector copiar el código de nuestro fragmento. Esta es una pequeña mejora agradable que evita que las personas tengan que seleccionar y copiar manualmente los fragmentos.
en realidad es un poco simple navigator.clipboard.writeText
API para esto. A este método le pasamos el texto que queremos copiar y listo, podemos inyectar un botón al lado de cada uno de nuestros <code>
elementos para enviar el texto del código a esta llamada API para copiarlo. ya nos estamos metiendo con ellos <code>
elementos para marcar líneas, así que integremos nuestro botón de copia en el portapapeles en el mismo lugar.
Primero, desde useEffect
código anterior, agreguemos una línea:
useEffect(() => {
const allPres = rootRef.current.querySelectorAll("pre");
const cleanup: (() => void)[] = [];
for (const pre of allPres) {
const code = pre.firstElementChild;
if (!code || !/code/i.test(code.tagName)) {
continue;
}
pre.appendChild(createCopyButton(code));
Presta atención a la última línea. Agregaremos nuestro botón justo en el DOM debajo del nuestro <pre>
elemento que ya es position: relative
lo que nos permite posicionar el botón más fácilmente.
vamos a ver que createCopyButton
la función se ve así:
function createCopyButton(codeEl) {
const button = document.createElement("button");
button.classList.add("prism-copy-button");
button.textContent = "Copy";
button.addEventListener("click", () => {
if (button.textContent === "Copied") {
return;
}
navigator.clipboard.writeText(codeEl.textContent || "");
button.textContent = "Copied";
button.disabled = true;
setTimeout(() => {
button.textContent = "Copy";
button.disabled = false;
}, 3000);
});
return button;
}
Mucho código, pero sobre todo plantilla. Creamos nuestro botón, luego le damos una clase CSS y algo de texto. Y después de esto, por supuesto, creamos un manipulador de clics para realizar la copia. Una vez que se realiza la copia, cambiamos el texto del botón y lo deshabilitamos durante unos segundos para ayudar al usuario a obtener comentarios de que funciona.
El trabajo real es a lo largo de esta línea:
navigator.clipboard.writeText(codeEl.textContent || "");
pasamos codeEl.textContent
que innerHTML
porque solo queremos el texto real que se muestra, no todo el marcado que agrega Prism.js para formatear bien nuestro código.
Ahora veamos cómo podemos diseñar este botón. No soy diseñador, pero esto es lo que se me ocurrió:
.prism-copy-button {
position: absolute;
top: 5px;
right: 5px;
width: 10ch;
background-color: rgb(100 100 100 / 0.5);
border-width: 0;
color: rgb(0, 0, 0);
cursor: pointer;
}
.prism-copy-button[disabled] {
cursor: default;
}
Que se parece a esto:
¡Y funciona! ¡Copia nuestro código e incluso guarda el formato (es decir, nuevas líneas y sangría)!
Deja una respuesta