HTMLRewriter te permite usar selectores CSS para transformar documentos HTML. Funciona con Request, Response, así como string. La implementación de Bun está basada en lol-html de Cloudflare.
Uso
Un caso de uso común es reescribir URLs en contenido HTML. Aquí hay un ejemplo que reescribe fuentes de imágenes y URLs de enlaces para usar un dominio CDN:
// Reemplazar todas las imágenes con un rickroll
const rewriter = new HTMLRewriter().on("img", {
element(img) {
// Famosa miniatura del video de rickroll
img.setAttribute("src", "https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg");
// Envolver la imagen en un enlace al video
img.before('<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target="_blank">', {
html: true,
});
img.after("</a>", { html: true });
// Agregar un texto alt divertido
img.setAttribute("alt", "Definitivamente no es un rickroll");
},
});
// Un documento HTML de ejemplo
const html = `
<html>
<body>
<img src="/cat.jpg">
<img src="dog.png">
<img src="https://example.com/bird.webp">
</body>
</html>
`;
const result = rewriter.transform(html);
console.log(result);Esto reemplaza todas las imágenes con una miniatura de Rick Astley y envuelve cada <img> en un enlace, produciendo un diff como este:
<html>
<body>
<img src="/cat.jpg" />
<img src="dog.png" />
<img src="https://example.com/bird.webp" />
<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target="_blank">
<img src="https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg" alt="Definitivamente no es un rickroll" />
</a>
<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target="_blank">
<img src="https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg" alt="Definitivamente no es un rickroll" />
</a>
<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target="_blank">
<img src="https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg" alt="Definitivamente no es un rickroll" />
</a>
</body>
</html>Ahora cada imagen en la página será reemplazada con una miniatura de Rick Astley, y al hacer clic en cualquier imagen llevará a un video muy famoso.
Tipos de entrada
HTMLRewriter puede transformar HTML de varias fuentes. La entrada se maneja automáticamente según su tipo:
// Desde Response
rewriter.transform(new Response("<div>content</div>"));
// Desde string
rewriter.transform("<div>content</div>");
// Desde ArrayBuffer
rewriter.transform(new TextEncoder().encode("<div>content</div>").buffer);
// Desde Blob
rewriter.transform(new Blob(["<div>content</div>"]));
// Desde File
rewriter.transform(Bun.file("index.html"));Ten en cuenta que la implementación de HTMLRewriter de Cloudflare Workers solo soporta objetos Response.
Manejadores de Elementos
El método on(selector, handlers) te permite registrar manejadores para elementos HTML que coinciden con un selector CSS. Los manejadores se llaman para cada elemento coincidente durante el análisis:
rewriter.on("div.content", {
// Manejar elementos
element(element) {
element.setAttribute("class", "new-content");
element.append("<p>Nuevo contenido</p>", { html: true });
},
// Manejar nodos de texto
text(text) {
text.replace("nuevo texto");
},
// Manejar comentarios
comments(comment) {
comment.remove();
},
});Los manejadores pueden ser asíncronos y devolver una Promesa. Ten en cuenta que las operaciones asíncronas bloquearán la transformación hasta que se completen:
rewriter.on("div", {
async element(element) {
await Bun.sleep(1000);
element.setInnerContent("<span>reemplazar</span>", { html: true });
},
});Soporte de Selectores CSS
El método on() soporta una amplia gama de selectores CSS:
// Selectores de etiqueta
rewriter.on("p", handler);
// Selectores de clase
rewriter.on("p.red", handler);
// Selectores de ID
rewriter.on("h1#header", handler);
// Selectores de atributo
rewriter.on("p[data-test]", handler); // Tiene atributo
rewriter.on('p[data-test="one"]', handler); // Coincidencia exacta
rewriter.on('p[data-test="one" i]', handler); // Insensible a mayúsculas
rewriter.on('p[data-test="one" s]', handler); // Sensible a mayúsculas
rewriter.on('p[data-test~="two"]', handler); // Coincidencia de palabra
rewriter.on('p[data-test^="a"]', handler); // Comienza con
rewriter.on('p[data-test$="1"]', handler); // Termina con
rewriter.on('p[data-test*="b"]', handler); // Contiene
rewriter.on('p[data-test|="a"]', handler); // Separado por guión
// Combinadores
rewriter.on("div span", handler); // Descendiente
rewriter.on("div > span", handler); // Hijo directo
// Pseudo-clases
rewriter.on("p:nth-child(2)", handler);
rewriter.on("p:first-child", handler);
rewriter.on("p:nth-of-type(2)", handler);
rewriter.on("p:first-of-type", handler);
rewriter.on("p:not(:first-child)", handler);
// Selector universal
rewriter.on("*", handler);Operaciones de Elemento
Los elementos proporcionan varios métodos para manipulación. Todos los métodos de modificación devuelven la instancia del elemento para encadenamiento:
rewriter.on("div", {
element(el) {
// Atributos
el.setAttribute("class", "new-class").setAttribute("data-id", "123");
const classAttr = el.getAttribute("class"); // "new-class"
const hasId = el.hasAttribute("id"); // boolean
el.removeAttribute("class");
// Manipulación de contenido
el.setInnerContent("Nuevo contenido"); // Escapa HTML por defecto
el.setInnerContent("<p>Contenido HTML</p>", { html: true }); // Analiza HTML
el.setInnerContent(""); // Limpiar contenido
// Manipulación de posición
el.before("Contenido antes").after("Contenido después").prepend("Primer hijo").append("Último hijo");
// Inserción de contenido HTML
el.before("<span>antes</span>", { html: true })
.after("<span>después</span>", { html: true })
.prepend("<span>primero</span>", { html: true })
.append("<span>último</span>", { html: true });
// Eliminación
el.remove(); // Eliminar elemento y contenidos
el.removeAndKeepContent(); // Eliminar solo las etiquetas del elemento
// Propiedades
console.log(el.tagName); // Nombre de etiqueta en minúsculas
console.log(el.namespaceURI); // URI del namespace del elemento
console.log(el.selfClosing); // Si el elemento es auto-cerrado (ej. <div />)
console.log(el.canHaveContent); // Si el elemento puede contener contenido (false para elementos void como <br>)
console.log(el.removed); // Si el elemento fue eliminado
// Iteración de atributos
for (const [name, value] of el.attributes) {
console.log(name, value);
}
// Manejo de etiqueta de cierre
el.onEndTag(endTag => {
endTag.before("Antes de la etiqueta de cierre");
endTag.after("Después de la etiqueta de cierre");
endTag.remove(); // Eliminar la etiqueta de cierre
console.log(endTag.name); // Nombre de etiqueta en minúsculas
});
},
});Operaciones de Texto
Los manejadores de texto proporcionan métodos para manipulación de texto. Los fragmentos de texto representan porciones de contenido de texto y proporcionan información sobre su posición en el nodo de texto:
rewriter.on("p", {
text(text) {
// Contenido
console.log(text.text); // Contenido de texto
console.log(text.lastInTextNode); // Si este es el último fragmento
console.log(text.removed); // Si el texto fue eliminado
// Manipulación
text.before("Texto antes").after("Texto después").replace("Nuevo texto").remove();
// Inserción de contenido HTML
text
.before("<span>antes</span>", { html: true })
.after("<span>después</span>", { html: true })
.replace("<span>reemplazar</span>", { html: true });
},
});Operaciones de Comentario
Los manejadores de comentarios permiten manipulación de comentarios con métodos similares a los nodos de texto:
rewriter.on("*", {
comments(comment) {
// Contenido
console.log(comment.text); // Texto del comentario
comment.text = "Nuevo texto de comentario"; // Establecer texto del comentario
console.log(comment.removed); // Si el comentario fue eliminado
// Manipulación
comment.before("Antes del comentario").after("Después del comentario").replace("Nuevo comentario").remove();
// Inserción de contenido HTML
comment
.before("<span>antes</span>", { html: true })
.after("<span>después</span>", { html: true })
.replace("<span>reemplazar</span>", { html: true });
},
});Manejadores de Documento
El método onDocument(handlers) te permite manejar eventos a nivel de documento. Estos manejadores se llaman para eventos que ocurren a nivel de documento en lugar de dentro de elementos específicos:
rewriter.onDocument({
// Manejar doctype
doctype(doctype) {
console.log(doctype.name); // "html"
console.log(doctype.publicId); // identificador público si está presente
console.log(doctype.systemId); // identificador de sistema si está presente
},
// Manejar nodos de texto
text(text) {
console.log(text.text);
},
// Manejar comentarios
comments(comment) {
console.log(comment.text);
},
// Manejar fin de documento
end(end) {
end.append("<!-- Pie de página -->", { html: true });
},
});Manejo de Response
Al transformar una Response:
- El código de estado, encabezados y otras propiedades de response se preservan
- El cuerpo se transforma manteniendo las capacidades de streaming
- La codificación de contenido (como gzip) se maneja automáticamente
- El cuerpo de la response original se marca como usado después de la transformación
- Los encabezados se clonan a la nueva response
Manejo de Errores
Las operaciones de HTMLRewriter pueden lanzar errores en varios casos:
- Sintaxis de selector inválida en el método
on() - Contenido HTML inválido en métodos de transformación
- Errores de stream al procesar cuerpos de Response
- Fallos de asignación de memoria
- Tipos de entrada inválidos (ej. pasar Symbol)
- Errores de cuerpo ya usado
Los errores deben capturarse y manejarse apropiadamente:
try {
const result = rewriter.transform(input);
// Procesar resultado
} catch (error) {
console.error("Error de HTMLRewriter:", error);
}Ver también
También puedes leer la documentación de Cloudflare, con la cual esta API pretende ser compatible.