HTMLRewriter 를 사용하면 CSS 선택자를 사용하여 HTML 문서를 변환할 수 있습니다. Request, Response 는 물론 string 과도 함께 작동합니다. Bun 의 구현은 Cloudflare 의 lol-html 을 기반으로 합니다.
사용법
일반적인 사용 사례는 HTML 콘텐츠의 URL 을 재작성하는 것입니다. 다음은 이미지 소스와 링크 URL 을 CDN 도메인으로 재작성하는 예제입니다.
// 모든 이미지를 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 });
// 재미있는 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> 을 링크로 감싸서 다음과 같은 diff 를 생성합니다.
<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 을 변환할 수 있습니다. 입력은 타입에 따라 자동으로 처리됩니다.
// 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 엘리먼트에 대한 핸들러를 등록할 수 있습니다. 핸들러는 파싱 중에 일치하는 각 엘리먼트에 대해 호출됩니다.
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 를 반환할 수 있습니다. 비동기 작업은 완료될 때까지 변환을 블로킹한다는 점에 유의하세요.
rewriter.on("div", {
async element(element) {
await Bun.sleep(1000);
element.setInnerContent("<span>replace</span>", { html: true });
},
});CSS 선택자 지원
on() 메서드는 다양한 CSS 선택자를 지원합니다.
// 태그 선택자
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);엘리먼트 연산
엘리먼트는 다양한 조작 방법을 제공합니다. 모든 수정 메서드는 체이닝을 위해 엘리먼트 인스턴스를 반환합니다.
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); // 엘리먼트가 콘텐츠를 포함할 수 있는지 여부 (void 엘리먼트인 <br> 등은 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); // 소문자 태그 이름
});
},
});텍스트 연산
텍스트 핸들러는 텍스트 조작을 위한 방법을 제공합니다. 텍스트 청크는 텍스트 노드의 텍스트 콘텐츠 부분을 나타내며 텍스트 노드 내 위치에 대한 정보를 제공합니다.
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 });
},
});주석 연산
주석 핸들러는 텍스트 노드와 유사한 방법으로 주석 조작을 허용합니다.
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) 메서드를 사용하면 문서 레벨 이벤트를 처리할 수 있습니다. 이러한 핸들러는 특정 엘리먼트 내부가 아닌 문서 레벨에서 발생하는 이벤트에 대해 호출됩니다.
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 처리
Response 를 변환할 때:
- 상태 코드, 헤더 및 기타 Response 속성이 유지됩니다
- 본체는 스트리밍 기능을 유지하면서 변환됩니다
- 콘텐츠 인코딩 (gzip 등) 이 자동으로 처리됩니다
- 원래 Response 본체는 변환 후 사용된 것으로 표시됩니다
- 헤더는 새 Response 로 복제됩니다
오류 처리
HTMLRewriter 연산은 여러 경우에 오류를 발생시킬 수 있습니다.
on()메서드의 잘못된 선택자 구문- 변환 메서드의 잘못된 HTML 콘텐츠
- Response 본체 처리 시 스트림 오류
- 메모리 할당 실패
- 잘못된 입력 타입 (예: Symbol 전달)
- 본체 이미 사용됨 오류
오류는 적절하게 캐치하고 처리해야 합니다.
try {
const result = rewriter.transform(input);
// 결과 처리
} catch (error) {
console.error("HTMLRewriter error:", error);
}참고
이 API 는 호환성을 목표로 하는 Cloudflare 문서 도 읽어보세요.