Bun

HTMLRewriter

HTMLRewriter 可讓您使用 CSS 選擇器來轉換 HTML 文件。它適用於 RequestResponse 以及 string。Bun 的實作基於 Cloudflare 的 lol-html

用法

常見的用例是在 HTML 內容中重寫 URL。這是一個範例,示範如何重寫圖片來源和連結 URL 以使用 CDN 網域。

// Replace all images with a rickroll
const rewriter = new HTMLRewriter().on("img", {
  element(img) {
    // Famous rickroll video thumbnail
    img.setAttribute(
      "src",
      "https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg",
    );

    // Wrap the image in a link to the video
    img.before(
      '<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target="_blank">',
      { html: true },
    );
    img.after("</a>", { html: true });

    // Add some fun alt text
    img.setAttribute("alt", "Definitely not a rickroll");
  },
});

// An example HTML document
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);

這會將所有圖片替換成 Rick Astley 的縮圖,並將每個 <img> 標籤包裝在連結中,產生如下的差異:

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

現在頁面上的每張圖片都將被替換成 Rick Astley 的縮圖,並且點擊任何圖片都會連結到 一個非常有名的影片

輸入類型

HTMLRewriter 可以轉換來自各種來源的 HTML。輸入會根據其類型自動處理。

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

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

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

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

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

請注意,Cloudflare Workers 實作的 HTMLRewriter 僅支援 Response 物件。

元素處理器

on(selector, handlers) 方法可讓您為符合 CSS 選擇器的 HTML 元素註冊處理器。這些處理器會在解析期間針對每個符合的元素調用。

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();
  },
});

處理器可以是異步的並返回 Promise。請注意,異步操作會阻塞轉換,直到它們完成。

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

CSS 選擇器支援

on() 方法支援各種 CSS 選擇器。

// Tag selectors
rewriter.on("p", handler);

// Class selectors
rewriter.on("p.red", handler);

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

// Attribute selectors
rewriter.on("p[data-test]", handler); // Has attribute
rewriter.on('p[data-test="one"]', handler); // Exact match
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); // Starts with
rewriter.on('p[data-test$="1"]', handler); // Ends with
rewriter.on('p[data-test*="b"]', handler); // Contains
rewriter.on('p[data-test|="a"]', handler); // Dash-separated

// Combinators
rewriter.on("div span", handler); // Descendant
rewriter.on("div > span", handler); // Direct child

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

// Universal selector
rewriter.on("*", handler);

元素操作

元素提供各種操作方法。所有修改方法都會返回元素實例以進行鏈式調用。

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

    // Content manipulation
    el.setInnerContent("New content"); // Escapes HTML by default
    el.setInnerContent("<p>HTML content</p>", { html: true }); // Parses HTML
    el.setInnerContent(""); // Clear content

    // Position manipulation
    el.before("Content before")
      .after("Content after")
      .prepend("First child")
      .append("Last child");

    // HTML content insertion
    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 });

    // Removal
    el.remove(); // Remove element and contents
    el.removeAndKeepContent(); // Remove only the element tags

    // Properties
    console.log(el.tagName); // Lowercase tag name
    console.log(el.namespaceURI); // Element's namespace URI
    console.log(el.selfClosing); // Whether element is self-closing (e.g. <div />)
    console.log(el.canHaveContent); // Whether element can contain content (false for void elements like <br>)
    console.log(el.removed); // Whether element was removed

    // Attributes iteration
    for (const [name, value] of el.attributes) {
      console.log(name, value);
    }

    // End tag handling
    el.onEndTag(endTag => {
      endTag.before("Before end tag");
      endTag.after("After end tag");
      endTag.remove(); // Remove the end tag
      console.log(endTag.name); // Tag name in lowercase
    });
  },
});

文字操作

文字處理器提供文字操作方法。文字塊代表文字內容的一部分,並提供有關它們在文字節點中位置的資訊。

rewriter.on("p", {
  text(text) {
    // Content
    console.log(text.text); // Text content
    console.log(text.lastInTextNode); // Whether this is the last chunk
    console.log(text.removed); // Whether text was removed

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

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

註解操作

註解處理器允許使用與文字節點類似的方法進行註解操作。

rewriter.on("*", {
  comments(comment) {
    // Content
    console.log(comment.text); // Comment text
    comment.text = "New comment text"; // Set comment text
    console.log(comment.removed); // Whether comment was removed

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

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

文件處理器

onDocument(handlers) 方法可讓您處理文件層級的事件。這些處理器會針對發生在文件層級而不是特定元素內的事件調用。

rewriter.onDocument({
  // Handle doctype
  doctype(doctype) {
    console.log(doctype.name); // "html"
    console.log(doctype.publicId); // public identifier if present
    console.log(doctype.systemId); // system identifier if present
  },
  // 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 });
  },
});

回應處理

當轉換 Response 時:

  • 狀態碼、標頭和其他回應屬性會被保留。
  • 主體在轉換的同時保持串流功能。
  • 內容編碼(如 gzip)會自動處理。
  • 原始回應主體在轉換後會被標記為已使用。
  • 標頭會被複製到新的回應。

錯誤處理

HTMLRewriter 操作在幾種情況下可能會拋出錯誤:

  • on() 方法中無效的選擇器語法。
  • 轉換方法中無效的 HTML 內容。
  • 處理 Response 主體時的串流錯誤。
  • 記憶體分配失敗。
  • 無效的輸入類型(例如,傳遞 Symbol)。
  • 主體已使用錯誤。

應捕獲並適當處理錯誤。

try {
  const result = rewriter.transform(input);
  // Process result
} catch (error) {
  console.error("HTMLRewriter error:", error);
}

另請參閱

您也可以閱讀 Cloudflare 文件,此 API 旨在與之相容。