Skip to content

HTMLRewriter を使用すると、CSS セレクターを使用して HTML ドキュメントを変換できます。RequestResponse、および string で動作します。Bun の実装は Cloudflare の lol-html に基づいています。


使用方法

一般的な使用例は HTML コンテンツ内の URL を書き換えることです。これは画像ソースとリンク URL を CDN ドメインを使用するように書き換える例です。

ts
// すべての画像をリックロールに置き換える
const rewriter = new HTMLRewriter().on("img", {
  element(img) {
    // 有名なリックロールビデオのサムネイル
    img.setAttribute("src", "https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg");

    // 画像をビデオへのリンクでラップ
    img.before('<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target="_blank">', {
      html: true,
    });
    img.after("</a>", { html: true });

    // 楽しい alt テキストを追加
    img.setAttribute("alt", "Definitely not a rickroll");
  },
});

// HTML ドキュメントの例
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
<html>
  <body>
    <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 を変換できます。入力はタイプに基づいて自動的に処理されます。

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

// 文字列から
rewriter.transform("<div>content</div>");

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

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

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

Cloudflare Workers の HTMLRewriter 実装は Response オブジェクトのみをサポートしていることに注意してください。

エレメントハンドラー

on(selector, handlers) メソッドを使用すると、CSS セレクターに一致する HTML エレメントのハンドラーを登録できます。ハンドラーは解析中に一致する各エレメントに対して呼び出されます。

ts
rewriter.on("div.content", {
  // エレメントを処理
  element(element) {
    element.setAttribute("class", "new-content");
    element.append("<p>New content</p>", { html: true });
  },
  // テキストノードを処理
  text(text) {
    text.replace("new text");
  },
  // コメントを処理
  comments(comment) {
    comment.remove();
  },
});

ハンドラーは非同期にでき、Promise を返すことができます。非同期操作は完了するまで変換をブロックすることに注意してください。

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

CSS セレクターのサポート

on() メソッドは幅広い CSS セレクターをサポートしています。

ts
// タグセレクター
rewriter.on("p", handler);

// クラスセレクター
rewriter.on("p.red", handler);

// ID セレクター
rewriter.on("h1#header", handler);

// 属性セレクター
rewriter.on("p[data-test]", handler); // 属性を持つ
rewriter.on('p[data-test="one"]', handler); // 完全一致
rewriter.on('p[data-test="one" i]', handler); // 大文字小文字を区別しない
rewriter.on('p[data-test="one" s]', handler); // 大文字小文字を区別する
rewriter.on('p[data-test~="two"]', handler); // 単語一致
rewriter.on('p[data-test^="a"]', handler); // 始まる
rewriter.on('p[data-test$="1"]', handler); // 終わる
rewriter.on('p[data-test*="b"]', handler); // 含む
rewriter.on('p[data-test|="a"]', handler); // ダッシュ区切り

// 結合子
rewriter.on("div span", handler); // 子孫
rewriter.on("div > span", handler); // 直接の子

// 擬似クラス
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);

// ユニバーサルセレクター
rewriter.on("*", handler);

エレメント操作

エレメントにはさまざまな操作メソッドが用意されています。すべての修正メソッドはチェーンのためにエレメントインスタンスを返します。

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

    // コンテンツ操作
    el.setInnerContent("New content"); // デフォルトで HTML をエスケープ
    el.setInnerContent("<p>HTML content</p>", { html: true }); // HTML を解析
    el.setInnerContent(""); // コンテンツをクリア

    // 位置操作
    el.before("Content before").after("Content after").prepend("First child").append("Last child");

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

    // 削除
    el.remove(); // エレメントとコンテンツを削除
    el.removeAndKeepContent(); // エレメントタグのみを削除

    // プロパティ
    console.log(el.tagName); // 小文字のタグ名
    console.log(el.namespaceURI); // エレメントのネームスペース URI
    console.log(el.selfClosing); // エレメントが自己閉じかどうか(例:<div />)
    console.log(el.canHaveContent); // エレメントがコンテンツを含めることができるかどうか(<br> のような void エレメントの場合は false)
    console.log(el.removed); // エレメントが削除されたかどうか

    // 属性イテレーション
    for (const [name, value] of el.attributes) {
      console.log(name, value);
    }

    // エンドタグ処理
    el.onEndTag(endTag => {
      endTag.before("Before end tag");
      endTag.after("After end tag");
      endTag.remove(); // エンドタグを削除
      console.log(endTag.name); // タグ名を小文字で
    });
  },
});

テキスト操作

テキストハンドラーはテキスト操作のためのメソッドを提供します。テキストチャンクはテキストコンテンツの部分を表し、テキストノード内の位置に関する情報を提供します。

ts
rewriter.on("p", {
  text(text) {
    // コンテンツ
    console.log(text.text); // テキストコンテンツ
    console.log(text.lastInTextNode); // これが最後のチャンクかどうか
    console.log(text.removed); // テキストが削除されたかどうか

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

    // HTML コンテンツ挿入
    text
      .before("<span>before</span>", { html: true })
      .after("<span>after</span>", { html: true })
      .replace("<span>replace</span>", { html: true });
  },
});

コメント操作

コメントハンドラーはテキストノードと同様のメソッドでコメント操作を許可します。

ts
rewriter.on("*", {
  comments(comment) {
    // コンテンツ
    console.log(comment.text); // コメントテキスト
    comment.text = "New comment text"; // コメントテキストを設定
    console.log(comment.removed); // コメントが削除されたかどうか

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

    // HTML コンテンツ挿入
    comment
      .before("<span>before</span>", { html: true })
      .after("<span>after</span>", { html: true })
      .replace("<span>replace</span>", { html: true });
  },
});

ドキュメントハンドラー

onDocument(handlers) メソッドを使用すると、ドキュメントレベルのイベントを処理できます。これらのハンドラーは特定のエレメント内ではなくドキュメントレベルで発生するイベントに対して呼び出されます。

ts
rewriter.onDocument({
  // doctype を処理
  doctype(doctype) {
    console.log(doctype.name); // "html"
    console.log(doctype.publicId); // 存在する場合の public 識別子
    console.log(doctype.systemId); // 存在する場合の system 識別子
  },
  // テキストノードを処理
  text(text) {
    console.log(text.text);
  },
  // コメントを処理
  comments(comment) {
    console.log(comment.text);
  },
  // ドキュメント終了を処理
  end(end) {
    end.append("<!-- Footer -->", { html: true });
  },
});

レスポンス処理

Response を変換する場合。

  • ステータスコード、ヘッダー、およびその他のレスポンスプロパティは保持されます
  • 本文はストリーミング機能を維持しながら変換されます
  • コンテンツエンコーディング(gzip など)は自動的に処理されます
  • 元のリクエスト本文は変換後に使用済みとしてマークされます
  • ヘッダーは新しいレスポンスにクローンされます

エラー処理

HTMLRewriter 操作はいくつかのケースでエラーをスローする可能性があります。

  • on() メソッドの無効なセレクター構文
  • 変換メソッドの無効な HTML コンテンツ
  • Response 本文の処理中のストリームエラー
  • メモリ割り当て失敗
  • 無効な入力タイプ(例:Symbol を渡す)
  • 本文が既に使用されているエラー

エラーは適切にキャッチして処理する必要があります。

ts
try {
  const result = rewriter.transform(input);
  // 結果を処理
} catch (error) {
  console.error("HTMLRewriter error:", error);
}

関連項目

この API が互換性を目指している Cloudflare ドキュメント も読むことができます。

Bun by www.bunjs.com.cn 編集