Skip to content

HTMLRewriter 를 사용하면 CSS 선택자를 사용하여 HTML 문서를 변환할 수 있습니다. Request, Response 는 물론 string 과도 함께 작동합니다. Bun 의 구현은 Cloudflare 의 lol-html 을 기반으로 합니다.


사용법

일반적인 사용 사례는 HTML 콘텐츠의 URL 을 재작성하는 것입니다. 다음은 이미지 소스와 링크 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 });

    // 재미있는 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
<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 을 변환할 수 있습니다. 입력은 타입에 따라 자동으로 처리됩니다.

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); // 엘리먼트가 콘텐츠를 포함할 수 있는지 여부 (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); // 소문자 태그 이름
    });
  },
});

텍스트 연산

텍스트 핸들러는 텍스트 조작을 위한 방법을 제공합니다. 텍스트 청크는 텍스트 노드의 텍스트 콘텐츠 부분을 나타내며 텍스트 노드 내 위치에 대한 정보를 제공합니다.

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

Response 를 변환할 때:

  • 상태 코드, 헤더 및 기타 Response 속성이 유지됩니다
  • 본체는 스트리밍 기능을 유지하면서 변환됩니다
  • 콘텐츠 인코딩 (gzip 등) 이 자동으로 처리됩니다
  • 원래 Response 본체는 변환 후 사용된 것으로 표시됩니다
  • 헤더는 새 Response 로 복제됩니다

오류 처리

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