Skip to content

HTMLRewriter позволяет использовать CSS-селекторы для трансформации HTML-документов. Он работает с Request, Response, а также string. Реализация Bun основана на lol-html от Cloudflare.


Использование

Распространённый случай использования — переписывание URL в HTML-контенте. Вот пример, который переписывает источники изображений и URL ссылок для использования домена CDN:

ts
// Заменить все изображения на rickroll
const rewriter = new HTMLRewriter().on("img", {
  element(img) {
    // Знаменитая миниатюра видео rickroll
    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 });

    // Добавить забавный альтернативный текст
    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);

Это заменяет все изображения на миниатюру Рика Эстли и оборачивает каждый <img> в ссылку, создавая diff вроде этого:

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>

Теперь каждое изображение на странице будет заменено на миниатюру Рика Эстли, и клик по любому изображению приведёт к очень известному видео.

Типы входных данных

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

Обратите внимание, что реализация HTMLRewriter в Cloudflare Workers поддерживает только объекты Response.

Обработчики элементов

Метод on(selector, handlers) позволяет регистрировать обработчики для HTML-элементов, соответствующих CSS-селектору. Обработчики вызываются для каждого соответствующего элемента во время разбора:

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); // Может ли элемент содержать контент (false для пустых элементов вроде <br>)
    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 identifier если присутствует
    console.log(doctype.systemId); // system identifier если присутствует
  },
  // Обработка текстовых узлов
  text(text) {
    console.log(text.text);
  },
  // Обработка комментариев
  comments(comment) {
    console.log(comment.text);
  },
  // Обработка конца документа
  end(end) {
    end.append("<!-- Footer -->", { html: true });
  },
});

Обработка Response

При трансформации Response:

  • Код состояния, заголовки и другие свойства response сохраняются
  • Тело трансформируется с сохранением возможностей потоковой передачи
  • Кодирование контента (например, gzip) обрабатывается автоматически
  • Исходное тело response помечается как использованное после трансформации
  • Заголовки клонируются в новый response

Обработка ошибок

Операции HTMLRewriter могут выбрасывать ошибки в нескольких случаях:

  • Неверный синтаксис селектора в методе on()
  • Неверный HTML-контент в методах трансформации
  • Ошибки потока при обработке тел Response
  • Ошибки выделения памяти
  • Неверные типы входных данных (например, передача Symbol)
  • Ошибки «тело уже использовано»

Ошибки должны быть пойманы и обработаны соответствующим образом:

ts
try {
  const result = rewriter.transform(input);
  // Обработать результат
} catch (error) {
  console.error("HTMLRewriter error:", error);
}

См. также

Вы также можете прочитать документацию Cloudflare, с которой совместим этот API.

Bun от www.bunjs.com.cn