Skip to content

HTMLRewriter ti permette di usare selettori CSS per trasformare documenti HTML. Funziona con Request, Response e string. L'implementazione di Bun è basata su lol-html di Cloudflare.


Utilizzo

Un caso d'uso comune è riscrivere URL in contenuti HTML. Ecco un esempio che riscrive le sorgenti delle immagini e gli URL dei link per usare un dominio CDN:

ts
// Sostituisci tutte le immagini con un rickroll
const rewriter = new HTMLRewriter().on("img", {
  element(img) {
    // Famosa miniatura del video rickroll
    img.setAttribute("src", "https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg");

    // Avvolgi l'immagine in un link al video
    img.before('<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target="_blank">', {
      html: true,
    });
    img.after("</a>", { html: true });

    // Aggiungi del testo alt divertente
    img.setAttribute("alt", "Definitely not a rickroll");
  },
});

// Un documento HTML di esempio
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);

Questo sostituisce tutte le immagini con una miniatura di Rick Astley e avvolge ogni <img> in un link, producendo una diff come questa:

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>

Ora ogni immagine nella pagina verrà sostituita con una miniatura di Rick Astley e cliccando su qualsiasi immagine si verrà condotti a un video molto famoso.

Tipi di input

HTMLRewriter può trasformare HTML da varie fonti. L'input viene gestito automaticamente in base al suo tipo:

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

// Da stringa
rewriter.transform("<div>content</div>");

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

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

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

Nota che l'implementazione di HTMLRewriter di Cloudflare Workers supporta solo oggetti Response.

Handler di elementi

Il metodo on(selector, handlers) ti permette di registrare handler per elementi HTML che corrispondono a un selettore CSS. Gli handler vengono chiamati per ogni elemento corrispondente durante il parsing:

ts
rewriter.on("div.content", {
  // Gestisci elementi
  element(element) {
    element.setAttribute("class", "new-content");
    element.append("<p>New content</p>", { html: true });
  },
  // Gestisci nodi di testo
  text(text) {
    text.replace("new text");
  },
  // Gestisci commenti
  comments(comment) {
    comment.remove();
  },
});

Gli handler possono essere asincroni e restituire una Promise. Nota che le operazioni asincrone bloccheranno la trasformazione fino al loro completamento:

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

Supporto per selettori CSS

Il metodo on() supporta un'ampia gamma di selettori CSS:

ts
// Selettori di tag
rewriter.on("p", handler);

// Selettori di classe
rewriter.on("p.red", handler);

// Selettori di ID
rewriter.on("h1#header", handler);

// Selettori di attributo
rewriter.on("p[data-test]", handler); // Ha attributo
rewriter.on('p[data-test="one"]', handler); // Corrispondenza esatta
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); // Corrispondenza parola
rewriter.on('p[data-test^="a"]', handler); // Inizia con
rewriter.on('p[data-test$="1"]', handler); // Finisce con
rewriter.on('p[data-test*="b"]', handler); // Contiene
rewriter.on('p[data-test|="a"]', handler); // Separato da trattino

// Combinatori
rewriter.on("div span", handler); // Discendente
rewriter.on("div > span", handler); // Figlio diretto

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

// Selettore universale
rewriter.on("*", handler);

Operazioni sugli elementi

Gli elementi forniscono vari metodi per la manipolazione. Tutti i metodi di modifica restituiscono l'istanza dell'elemento per il chaining:

ts
rewriter.on("div", {
  element(el) {
    // Attributi
    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");

    // Manipolazione del contenuto
    el.setInnerContent("New content"); // Esegue escape dell'HTML per impostazione predefinita
    el.setInnerContent("<p>HTML content</p>", { html: true }); // Esegue il parsing dell'HTML
    el.setInnerContent(""); // Pulisce il contenuto

    // Manipolazione della posizione
    el.before("Content before").after("Content after").prepend("First child").append("Last child");

    // Inserimento di contenuto 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 });

    // Rimozione
    el.remove(); // Rimuove elemento e contenuti
    el.removeAndKeepContent(); // Rimuove solo i tag dell'elemento

    // Proprietà
    console.log(el.tagName); // Nome del tag in minuscolo
    console.log(el.namespaceURI); // URI del namespace dell'elemento
    console.log(el.selfClosing); // Se l'elemento è autochiudente (es. <div />)
    console.log(el.canHaveContent); // Se l'elemento può contenere contenuto (false per elementi void come <br>)
    console.log(el.removed); // Se l'elemento è stato rimosso

    // Iterazione degli attributi
    for (const [name, value] of el.attributes) {
      console.log(name, value);
    }

    // Gestione del tag di chiusura
    el.onEndTag(endTag => {
      endTag.before("Before end tag");
      endTag.after("After end tag");
      endTag.remove(); // Rimuove il tag di chiusura
      console.log(endTag.name); // Nome del tag in minuscolo
    });
  },
});

Operazioni sul testo

Gli handler di testo forniscono metodi per la manipolazione del testo. I chunk di testo rappresentano porzioni di contenuto di testo e forniscono informazioni sulla loro posizione nel nodo di testo:

ts
rewriter.on("p", {
  text(text) {
    // Contenuto
    console.log(text.text); // Contenuto del testo
    console.log(text.lastInTextNode); // Se questo è l'ultimo chunk
    console.log(text.removed); // Se il testo è stato rimosso

    // Manipolazione
    text.before("Before text").after("After text").replace("New text").remove();

    // Inserimento di contenuto HTML
    text
      .before("<span>before</span>", { html: true })
      .after("<span>after</span>", { html: true })
      .replace("<span>replace</span>", { html: true });
  },
});

Operazioni sui commenti

Gli handler di commenti permettono la manipolazione dei commenti con metodi simili ai nodi di testo:

ts
rewriter.on("*", {
  comments(comment) {
    // Contenuto
    console.log(comment.text); // Testo del commento
    comment.text = "New comment text"; // Imposta il testo del commento
    console.log(comment.removed); // Se il commento è stato rimosso

    // Manipolazione
    comment.before("Before comment").after("After comment").replace("New comment").remove();

    // Inserimento di contenuto HTML
    comment
      .before("<span>before</span>", { html: true })
      .after("<span>after</span>", { html: true })
      .replace("<span>replace</span>", { html: true });
  },
});

Handler del documento

Il metodo onDocument(handlers) ti permette di gestire eventi a livello di documento. Questi handler vengono chiamati per eventi che si verificano a livello di documento piuttosto che all'interno di elementi specifici:

ts
rewriter.onDocument({
  // Gestisci doctype
  doctype(doctype) {
    console.log(doctype.name); // "html"
    console.log(doctype.publicId); // identificatore pubblico se presente
    console.log(doctype.systemId); // identificatore di sistema se presente
  },
  // Gestisci nodi di testo
  text(text) {
    console.log(text.text);
  },
  // Gestisci commenti
  comments(comment) {
    console.log(comment.text);
  },
  // Gestisci la fine del documento
  end(end) {
    end.append("<!-- Footer -->", { html: true });
  },
});

Gestione della Response

Quando si trasforma una Response:

  • Il codice di stato, le intestazioni e altre proprietà della response vengono preservati
  • Il body viene trasformato mantenendo le capacità di streaming
  • La codifica del contenuto (come gzip) viene gestita automaticamente
  • Il body della response originale viene contrassegnato come usato dopo la trasformazione
  • Le intestazioni vengono clonate alla nuova response

Gestione degli errori

Le operazioni di HTMLRewriter possono lanciare errori in diversi casi:

  • Sintassi del selettore non valida nel metodo on()
  • Contenuto HTML non valido nei metodi di trasformazione
  • Errori di stream durante l'elaborazione dei body delle Response
  • Errori di allocazione della memoria
  • Tipi di input non validi (es. passare Symbol)
  • Errori di body già usato

Gli errori dovrebbero essere catturati e gestiti in modo appropriato:

ts
try {
  const result = rewriter.transform(input);
  // Processa il risultato
} catch (error) {
  console.error("Errore HTMLRewriter:", error);
}

Vedi anche

Puoi anche leggere la documentazione di Cloudflare, con cui questa API intende essere compatibile.

Bun a cura di www.bunjs.com.cn