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