Skip to content

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:

ts
// 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
<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:

ts
// 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:

ts
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:

ts
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:

ts
// 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:

ts
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:

ts
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:

ts
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:

ts
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:

ts
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.

Bun por www.bunjs.com.cn editar