Skip to content

HTMLRewriter permite que você use seletores CSS para transformar documentos HTML. Funciona com Request, Response, bem como string. A implementação do Bun é baseada no lol-html da Cloudflare.


Uso

Um caso de uso comum é reescrever URLs em conteúdo HTML. Aqui está um exemplo que reescreve fontes de imagens e URLs de links para usar um domínio CDN:

ts
// Substitui todas as imagens por um rickroll
const rewriter = new HTMLRewriter().on("img", {
  element(img) {
    // Famosa thumbnail do vídeo rickroll
    img.setAttribute("src", "https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg");

    // Envolve a imagem em um link para o vídeo
    img.before('<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target="_blank">', {
      html: true,
    });
    img.after("</a>", { html: true });

    // Adiciona um texto alt divertido
    img.setAttribute("alt", "Definitely not a rickroll");
  },
});

// Um documento HTML de exemplo
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);

Isso substitui todas as imagens por uma thumbnail de Rick Astley e envolve cada <img> em um link, produzindo um 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="Definitely not a rickroll" /> 
    </a> 
    <a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target="_blank"> 
      <img src="https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg" alt="Definitely not a rickroll" /> 
    </a> 
    <a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target="_blank"> 
      <img src="https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg" alt="Definitely not a rickroll" /> 
    </a> 
  </body>
</html>

Agora todas as imagens na página serão substituídas por uma thumbnail de Rick Astley, e clicar em qualquer imagem levará a um vídeo muito famoso.

Tipos de entrada

HTMLRewriter pode transformar HTML de várias fontes. A entrada é automaticamente tratada com base em seu tipo:

ts
// De Response
rewriter.transform(new Response("<div>content</div>"));

// De string
rewriter.transform("<div>content</div>");

// De ArrayBuffer
rewriter.transform(new TextEncoder().encode("<div>content</div>").buffer);

// De Blob
rewriter.transform(new Blob(["<div>content</div>"]));

// De File
rewriter.transform(Bun.file("index.html"));

Note que a implementação do HTMLRewriter dos Cloudflare Workers suporta apenas objetos Response.

Handlers de Elemento

O método on(selector, handlers) permite que você registre handlers para elementos HTML que correspondem a um seletor CSS. Os handlers são chamados para cada elemento correspondente durante o parsing:

ts
rewriter.on("div.content", {
  // Handle elements
  element(element) {
    element.setAttribute("class", "new-content");
    element.append("<p>New content</p>", { html: true });
  },
  // Handle text nodes
  text(text) {
    text.replace("new text");
  },
  // Handle comments
  comments(comment) {
    comment.remove();
  },
});

Os handlers podem ser assíncronos e retornar uma Promise. Note que operações assíncronas bloquearão a transformação até que completem:

ts
rewriter.on("div", {
  async element(element) {
    await Bun.sleep(1000);
    element.setInnerContent("<span>replace</span>", { html: true });
  },
});

Suporte a Seletores CSS

O método on() suporta uma ampla gama de seletores CSS:

ts
// Seletores de tag
rewriter.on("p", handler);

// Seletores de classe
rewriter.on("p.red", handler);

// Seletores de ID
rewriter.on("h1#header", handler);

// Seletores de atributo
rewriter.on("p[data-test]", handler); // Tem atributo
rewriter.on('p[data-test="one"]', handler); // Correspondência exata
rewriter.on('p[data-test="one" i]', handler); // Case-insensitive
rewriter.on('p[data-test="one" s]', handler); // Case-sensitive
rewriter.on('p[data-test~="two"]', handler); // Word match
rewriter.on('p[data-test^="a"]', handler); // Começa com
rewriter.on('p[data-test$="1"]', handler); // Termina com
rewriter.on('p[data-test*="b"]', handler); // Contém
rewriter.on('p[data-test|="a"]', handler); // Dash-separated

// Combinadores
rewriter.on("div span", handler); // Descendente
rewriter.on("div > span", handler); // Filho direto

// Pseudo-classes
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);

// Seletor universal
rewriter.on("*", handler);

Operações de Elemento

Elementos fornecem vários métodos para manipulação. Todos os métodos de modificação retornam a instância do elemento para encadeamento:

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");

    // Manipulação de conteúdo
    el.setInnerContent("New content"); // Escapa HTML por padrão
    el.setInnerContent("<p>HTML content</p>", { html: true }); // Analisa HTML
    el.setInnerContent(""); // Limpa conteúdo

    // Manipulação de posição
    el.before("Content before").after("Content after").prepend("First child").append("Last child");

    // Inserção de conteúdo HTML
    el.before("<span>before</span>", { html: true })
      .after("<span>after</span>", { html: true })
      .prepend("<span>first</span>", { html: true })
      .append("<span>last</span>", { html: true });

    // Remoção
    el.remove(); // Remove elemento e conteúdo
    el.removeAndKeepContent(); // Remove apenas as tags do elemento

    // Propriedades
    console.log(el.tagName); // Nome da tag em minúsculas
    console.log(el.namespaceURI); // URI do namespace do elemento
    console.log(el.selfClosing); // Se o elemento é auto-fechável (e.g. <div />)
    console.log(el.canHaveContent); // Se o elemento pode conter conteúdo (falso para elementos void como <br>)
    console.log(el.removed); // Se o elemento foi removido

    // Iteração de atributos
    for (const [name, value] of el.attributes) {
      console.log(name, value);
    }

    // Tratamento de tag de fechamento
    el.onEndTag(endTag => {
      endTag.before("Before end tag");
      endTag.after("After end tag");
      endTag.remove(); // Remove a tag de fechamento
      console.log(endTag.name); // Nome da tag em minúsculas
    });
  },
});

Operações de Texto

Handlers de texto fornecem métodos para manipulação de texto. Chunks de texto representam porções de conteúdo de texto e fornecem informações sobre sua posição no nó de texto:

ts
rewriter.on("p", {
  text(text) {
    // Conteúdo
    console.log(text.text); // Conteúdo do texto
    console.log(text.lastInTextNode); // Se este é o último chunk
    console.log(text.removed); // Se o texto foi removido

    // Manipulação
    text.before("Before text").after("After text").replace("New text").remove();

    // Inserção de conteúdo HTML
    text
      .before("<span>before</span>", { html: true })
      .after("<span>after</span>", { html: true })
      .replace("<span>replace</span>", { html: true });
  },
});

Operações de Comentário

Handlers de comentário permitem manipulação de comentários com métodos similares aos nós de texto:

ts
rewriter.on("*", {
  comments(comment) {
    // Conteúdo
    console.log(comment.text); // Texto do comentário
    comment.text = "New comment text"; // Define o texto do comentário
    console.log(comment.removed); // Se o comentário foi removido

    // Manipulação
    comment.before("Before comment").after("After comment").replace("New comment").remove();

    // Inserção de conteúdo HTML
    comment
      .before("<span>before</span>", { html: true })
      .after("<span>after</span>", { html: true })
      .replace("<span>replace</span>", { html: true });
  },
});

Handlers de Documento

O método onDocument(handlers) permite que você trate eventos de nível de documento. Estes handlers são chamados para eventos que ocorrem no nível do documento em vez de dentro de elementos específicos:

ts
rewriter.onDocument({
  // Handle doctype
  doctype(doctype) {
    console.log(doctype.name); // "html"
    console.log(doctype.publicId); // identificador público se presente
    console.log(doctype.systemId); // identificador de sistema se presente
  },
  // Handle text nodes
  text(text) {
    console.log(text.text);
  },
  // Handle comments
  comments(comment) {
    console.log(comment.text);
  },
  // Handle document end
  end(end) {
    end.append("<!-- Footer -->", { html: true });
  },
});

Tratamento de Response

Ao transformar um Response:

  • O código de status, headers e outras propriedades da response são preservados
  • O body é transformado mantendo as capacidades de streaming
  • Content-encoding (como gzip) é tratado automaticamente
  • O body da response original é marcado como usado após a transformação
  • Headers são clonados para a nova response

Tratamento de Erros

Operações do HTMLRewriter podem lançar erros em vários casos:

  • Sintaxe de seletor inválida no método on()
  • Conteúdo HTML inválido em métodos de transformação
  • Erros de stream ao processar bodies de Response
  • Falhas de alocação de memória
  • Tipos de entrada inválidos (e.g., passar Symbol)
  • Erros de body já usado

Erros devem ser capturados e tratados apropriadamente:

ts
try {
  const result = rewriter.transform(input);
  // Processa o resultado
} catch (error) {
  console.error("Erro do HTMLRewriter:", error);
}

Veja também

Você também pode ler a documentação da Cloudflare, com a qual esta API pretende ser compatível.

Bun by www.bunjs.com.cn edit